diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bddcb3095..ad76d09fa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -52,4 +52,6 @@ submit the change with your pull request. - Kevin Yang / [kevieyang](https://github.com/kevieyang) - Justin Hamman / [juzham](https://github.com/juzham) - Rocky Jaiswal / [rocky-jaiswal](https://github.com/rocky-jaiswal) +- Robert Landreaux / [robertlandreaux](https://github.com/robertlandreaux) +- Savant Krishna / [sksavant](https://github.com/sksavant) diff --git a/Gemfile b/Gemfile index d15f48067..c36bd3640 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,8 @@ gem 'gibbon' # for Mailchimp newsletter subscriptions gem 'csv_shaper' # CSV export gem 'ruby-units' # for unit conversion +gem 'comfortable_mexican_sofa', '~> 1.12.0' # content management system + # vendored activemerchant for testing- needed for bogus paypal # gateway monkeypatch gem 'activemerchant', '1.33.0', diff --git a/Gemfile.lock b/Gemfile.lock index 09f8547c7..df217e647 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,6 +33,8 @@ GEM activesupport (= 4.1.9) builder (~> 3.1) erubis (~> 2.7.0) + active_link_to (1.0.2) + actionpack activemodel (4.1.9) activesupport (= 4.1.9) builder (~> 3.1) @@ -48,6 +50,9 @@ GEM tzinfo (~> 1.1) addressable (2.3.6) arel (5.0.1.20140414130214) + autoprefixer-rails (5.1.1) + execjs + json bcrypt (3.1.9) better_errors (2.0.0) coderay (>= 1.0.0) @@ -59,6 +64,10 @@ GEM bonsai-elasticsearch-rails (0.0.4) bootstrap-datepicker-rails (1.3.0.2) railties (>= 3.0) + bootstrap-sass (3.3.3) + autoprefixer-rails (>= 5.0.0.1) + sass (>= 3.2.19) + bootstrap_form (2.2.0) builder (3.2.2) byebug (3.5.1) columnize (~> 0.8) @@ -74,7 +83,13 @@ GEM capybara-email (2.4.0) capybara (~> 2.4) mail + climate_control (0.0.3) + activesupport (>= 3.0) cliver (0.3.2) + cocaine (0.5.5) + climate_control (>= 0.0.3, < 1.0) + codemirror-rails (4.8) + railties (>= 3.0, < 5) coderay (1.1.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) @@ -84,6 +99,21 @@ GEM execjs coffee-script-source (1.8.0) columnize (0.9.0) + comfortable_mexican_sofa (1.12.7) + active_link_to (>= 1.0.0) + bootstrap-sass (>= 3.2.0) + bootstrap_form (>= 2.2.0) + codemirror-rails (>= 3.0.0) + coffee-rails (>= 3.1.0) + haml-rails (>= 0.3.0) + jquery-rails (>= 3.0.0) + jquery-ui-rails (>= 5.0.0) + kramdown (>= 1.0.0) + paperclip (>= 4.0.0) + plupload-rails (>= 1.2.1) + rails (>= 4.0.0, < 5) + rails-i18n (>= 4.0.0) + sass-rails (>= 4.0.3) commonjs (0.2.7) coveralls (0.7.1) multi_json (~> 1.3) @@ -187,6 +217,7 @@ GEM sprockets-rails json (1.8.2) kgio (2.9.2) + kramdown (1.5.0) launchy (2.4.3) addressable (~> 2.3) leaflet-markercluster-rails (0.7.0) @@ -229,7 +260,14 @@ GEM multi_json (~> 1.3) omniauth-oauth (~> 1.0) orm_adapter (0.5.0) + paperclip (4.2.1) + activemodel (>= 3.0.0) + activesupport (>= 3.0.0) + cocaine (~> 0.5.3) + mime-types pg (0.17.1) + plupload-rails (1.2.1) + rails (>= 3.1) poltergeist (1.5.1) capybara (~> 2.1) cliver (~> 0.3.1) @@ -254,6 +292,9 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.1.9) sprockets-rails (~> 2.0) + rails-i18n (4.0.3) + i18n (~> 0.6) + railties (~> 4.0) rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging @@ -367,6 +408,7 @@ DEPENDENCIES capybara capybara-email coffee-rails (~> 4.1.0) + comfortable_mexican_sofa (~> 1.12.0) coveralls csv_shaper dalli diff --git a/app/assets/javascripts/comfy/admin/cms/custom.js.coffee b/app/assets/javascripts/comfy/admin/cms/custom.js.coffee new file mode 100644 index 000000000..d40d4c9c3 --- /dev/null +++ b/app/assets/javascripts/comfy/admin/cms/custom.js.coffee @@ -0,0 +1 @@ +# Custom JS for the admin area \ No newline at end of file diff --git a/app/assets/stylesheets/comfy/admin/cms/custom.sass b/app/assets/stylesheets/comfy/admin/cms/custom.sass new file mode 100644 index 000000000..328d897f1 --- /dev/null +++ b/app/assets/stylesheets/comfy/admin/cms/custom.sass @@ -0,0 +1 @@ +// custom CSS for admin area \ No newline at end of file diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 9e6bb005a..fc0ce9bb5 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -9,10 +9,12 @@ class CropsController < ApplicationController @sort = params[:sort] if @sort == 'alpha' # alphabetical order - @crops = Crop.includes(:scientific_names, {:plantings => :photos}).paginate(:page => params[:page]) + @crops = Crop.includes(:scientific_names, {:plantings => :photos}) + @paginated_crops = @crops.paginate(:page => params[:page]) else # default to sorting by popularity - @crops = Crop.popular.includes(:scientific_names, {:plantings => :photos}).paginate(:page => params[:page]) + @crops = Crop.popular.includes(:scientific_names, {:plantings => :photos}) + @paginated_crops = @crops.paginate(:page => params[:page]) end respond_to do |format| @@ -32,7 +34,18 @@ class CropsController < ApplicationController # GET /crops/wrangle def wrangle - @crops = Crop.recent.paginate(:page => params[:page]) + @approval_status = params[:approval_status] + case @approval_status + when "pending" + @crops = Crop.pending_approval + when "rejected" + @crops = Crop.rejected + else + @crops = Crop.recent + end + + @crops = @crops.paginate(:page => params[:page]) + @crop_wranglers = Role.crop_wranglers respond_to do |format| format.html @@ -50,8 +63,9 @@ class CropsController < ApplicationController # GET /crops/search def search @search = params[:search] - @all_matches = Crop.search(params[:search]) - exact_match = Crop.find_by_name(params[:search]) + @all_matches = Crop.search(@search) + @paginated_matches = @all_matches.paginate(:page => params[:page]) + exact_match = Crop.find_by_name(@search) if exact_match @all_matches.delete(exact_match) @all_matches.unshift(exact_match) @@ -59,7 +73,7 @@ class CropsController < ApplicationController respond_to do |format| format.html - format.json { render :json => Crop.search(params[:term]) } + format.json { render :json => Crop.search(@search) } end end @@ -95,17 +109,35 @@ class CropsController < ApplicationController # GET /crops/1/edit def edit @crop = Crop.find(params[:id]) + + (3 - @crop.scientific_names.length).times do + @crop.scientific_names.build + end end # POST /crops # POST /crops.json def create - params[:crop][:creator_id] = current_member.id @crop = Crop.new(crop_params) + if current_member.has_role? :crop_wrangler + @crop.creator = current_member + success_msg = "Crop was successfully created." + else + @crop.requester = current_member + @crop.approval_status = "pending" + success_msg = "Crop was successfully requested." + end + respond_to do |format| if @crop.save - format.html { redirect_to @crop, notice: 'Crop was successfully created.' } + unless current_member.has_role? :crop_wrangler + Role.crop_wranglers.each do |w| + Notifier.new_crop_request(w, @crop).deliver! + end + end + + format.html { redirect_to @crop, notice: success_msg } format.json { render json: @crop, status: :created, location: @crop } else format.html { render action: "new" } @@ -119,8 +151,18 @@ class CropsController < ApplicationController def update @crop = Crop.find(params[:id]) + previous_status = @crop.approval_status + + @crop.creator = current_member if previous_status == "pending" + respond_to do |format| if @crop.update(crop_params) + if previous_status == "pending" + requester = @crop.requester + new_status = @crop.approval_status + Notifier.crop_request_approved(requester, @crop).deliver! if new_status == "approved" + Notifier.crop_request_rejected(requester, @crop).deliver! if new_status == "rejected" + end format.html { redirect_to @crop, notice: 'Crop was successfully updated.' } format.json { head :no_content } else @@ -145,6 +187,6 @@ class CropsController < ApplicationController private def crop_params - params.require(:crop).permit(:en_wikipedia_url, :name, :parent_id, :creator_id, :scientific_names_attributes => [:scientific_name]) + params.require(:crop).permit(:en_wikipedia_url, :name, :parent_id, :creator_id, :approval_status, :request_notes, :reason_for_rejection, :rejection_notes, :scientific_names_attributes => [:scientific_name, :_destroy, :id]) end end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index 8ca1c4c24..744e93f4c 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -6,7 +6,12 @@ class MembersController < ApplicationController after_action :expire_cache_fragments, :only => :create def index - @members = Member.confirmed.paginate(:page => params[:page]) + @sort = params[:sort] + if @sort == 'recently_joined' + @members = Member.confirmed.recently_joined.paginate(:page => params[:page]) + else + @members = Member.confirmed.paginate(:page => params[:page]) + end respond_to do |format| format.html # index.html.haml diff --git a/app/controllers/seeds_controller.rb b/app/controllers/seeds_controller.rb index ac5dfb0c3..a2e79afaf 100644 --- a/app/controllers/seeds_controller.rb +++ b/app/controllers/seeds_controller.rb @@ -110,7 +110,9 @@ class SeedsController < ApplicationController private def seed_params - params.require(:seed).permit(:owner_id, :crop_id, :description, :quantity, :plant_before, - :tradable_to, :slug) + params.require(:seed).permit( + :owner_id, :crop_id, :description, :quantity, :plant_before, + :days_until_maturity_min, :days_until_maturity_max, :organic, :gmo, + :heirloom, :tradable_to, :slug) end end diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index 0d6aa6db3..53bae3092 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -22,4 +22,19 @@ class Notifier < ActionMailer::Base end end + def new_crop_request(member, request) + @member, @request = member, request + mail(:to => @member.email, :subject => "#{@request.requester.login_name} has requested #{@request.name} as a new crop") + end + + def crop_request_approved(member, crop) + @member, @crop = member, crop + mail(:to => @member.email, :subject => "#{crop.name.capitalize} has been approved") + end + + def crop_request_rejected(member, crop) + @member, @crop = member, crop + mail(:to => @member.email, :subject => "#{crop.name.capitalize} has been rejected") + end + end diff --git a/app/models/ability.rb b/app/models/ability.rb index a7b77b623..1997ddf49 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -21,7 +21,24 @@ class Ability cannot :read, Account cannot :read, AccountType + # nobody should be able to view unapproved crops unless they + # are wranglers or admins + cannot :read, Crop + can :read, Crop, :approval_status => "approved" + # scientific names should only be viewable if associated crop is approved + cannot :read, ScientificName + can :read, ScientificName do |sn| + sn.crop.approved? + end + # ... same for alternate names + cannot :read, AlternateName + can :read, AlternateName do |an| + an.crop.approved? + end + if member + # members can see even rejected or pending crops if they requested it + can :read, Crop, :requester_id => member.id # managing your own user settings can :update, Member, :id => member.id @@ -45,6 +62,9 @@ class Ability can :manage, AlternateName end + # any member can create a crop provisionally + can :create, Crop + # can create & destroy their own authentications against other sites. can :create, Authentication can :destroy, Authentication, :member_id => member.id diff --git a/app/models/crop.rb b/app/models/crop.rb index ff996143f..0571feecd 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -14,6 +14,7 @@ class Crop < ActiveRecord::Base has_many :harvests has_many :plant_parts, -> { uniq }, :through => :harvests belongs_to :creator, :class_name => 'Member' + belongs_to :requester, :class_name => 'Member' belongs_to :parent, :class_name => 'Crop' has_many :varieties, :class_name => 'Crop', :foreign_key => 'parent_id' @@ -21,17 +22,31 @@ class Crop < ActiveRecord::Base before_destroy {|crop| crop.posts.clear} default_scope { order("lower(name) asc") } - scope :recent, -> { reorder("created_at desc") } - scope :toplevel, -> { where(:parent_id => nil) } - scope :popular, -> { reorder("plantings_count desc, lower(name) asc") } - scope :randomized, -> { reorder('random()') } # ok on sqlite and psql, but not on mysql + scope :recent, -> { where(:approval_status => "approved").reorder("created_at desc") } + scope :toplevel, -> { where(:approval_status => "approved", :parent_id => nil) } + scope :popular, -> { where(:approval_status => "approved").reorder("plantings_count desc, lower(name) asc") } + scope :randomized, -> { where(:approval_status => "approved").reorder('random()') } # ok on sqlite and psql, but not on mysql + scope :pending_approval, -> { where(:approval_status => "pending") } + scope :rejected, -> { where(:approval_status => "rejected") } + ## Wikipedia urls are only necessary when approving a crop validates :en_wikipedia_url, :format => { :with => /\Ahttps?:\/\/en\.wikipedia\.org\/wiki/, :message => 'is not a valid English Wikipedia URL' - } - + }, + :if => :approved? + + ## Reasons are only necessary when rejecting + validates :reason_for_rejection, :presence => true, :if => :rejected? + + ## This validation addresses a race condition + validate :approval_status_cannot_be_changed_again + + validate :must_be_rejected_if_rejected_reasons_present + + validate :must_have_meaningful_reason_for_rejection + #################################### # Elastic search configuration include Elasticsearch::Model @@ -81,13 +96,18 @@ class Crop < ActiveRecord::Base include: { scientific_names: { only: :scientific_name }, alternate_names: { only: :name } - }) + }) end + # update the Elasticsearch index (only if we're using it in this + # environment) def update_index(name_obj) - __elasticsearch__.index_document if ENV['GROWSTUFF_ELASTICSEARCH'] == "true" + if ENV["GROWSTUFF_ELASTICSEARCH"] == "true" + __elasticsearch__.index_document + end end - #################################### + + # End Elasticsearch section def to_s return name @@ -160,6 +180,26 @@ class Crop < ActiveRecord::Base return true end + def pending? + approval_status == "pending" + end + + def approved? + approval_status == "approved" + end + + def rejected? + approval_status == "rejected" + end + + def approval_statuses + [ 'rejected', 'pending', 'approved' ] + end + + def reasons_for_rejection + [ "already in database", "not edible", "not enough information", "other" ] + end + # Crop.interesting # returns a list of interesting crops, for use on the homepage etc def Crop.interesting @@ -276,7 +316,31 @@ class Crop < ActiveRecord::Base ) return response.records.to_a else - where("name ILIKE ?", "%#{query}%") + where("name ILIKE ?", "%#{query}%").load end end + + # Custom validations + + def approval_status_cannot_be_changed_again + previous = previous_changes.include?(:approval_status) ? previous_changes.approval_status : {} + if previous.include?(:rejected) || previous.include?(:approved) + errors.add(:approval_status, "has already been set to #{approval_status}") + end + end + + def must_be_rejected_if_rejected_reasons_present + unless rejected? + if reason_for_rejection.present? || rejection_notes.present? + errors.add(:approval_status, "must be rejected if a reason for rejection is present") + end + end + end + + def must_have_meaningful_reason_for_rejection + if reason_for_rejection == "other" && rejection_notes.blank? + errors.add(:rejection_notes, "must be added if the reason for rejection is \"other\"") + end + end + end diff --git a/app/models/garden.rb b/app/models/garden.rb index 4e08a36d5..0514f2a96 100644 --- a/app/models/garden.rb +++ b/app/models/garden.rb @@ -34,7 +34,9 @@ class Garden < ActiveRecord::Base } validates :area, - :numericality => { :only_integer => false, :greater_than_or_equal_to => 0 }, + :numericality => { + :only_integer => false, + :greater_than_or_equal_to => 0 }, :allow_nil => true AREA_UNITS_VALUES = { diff --git a/app/models/harvest.rb b/app/models/harvest.rb index c565b2c91..4339a30fa 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -20,10 +20,14 @@ class Harvest < ActiveRecord::Base default_scope { order('created_at DESC') } + validates :crop, :approved => true + validates :crop, :presence => {:message => "must be present and exist in our database"} validates :quantity, - :numericality => { :only_integer => false }, + :numericality => { + :only_integer => false, + :greater_than_or_equal_to => 0 }, :allow_nil => true UNITS_VALUES = { diff --git a/app/models/member.rb b/app/models/member.rb index f23e7e45f..6c00fb8ce 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -32,6 +32,7 @@ class Member < ActiveRecord::Base scope :confirmed, -> { where('confirmed_at IS NOT NULL') } scope :located, -> { where("location <> '' and latitude IS NOT NULL and longitude IS NOT NULL") } scope :recently_signed_in, -> { reorder('updated_at DESC') } + scope :recently_joined, -> { reorder("confirmed_at desc") } scope :wants_newsletter, -> { where(:newsletter => true) } has_many :follows, :class_name => "Follow", :foreign_key => "follower_id" diff --git a/app/models/planting.rb b/app/models/planting.rb index e9c11368b..958ba625f 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -30,10 +30,14 @@ class Planting < ActiveRecord::Base default_scope { order("created_at desc") } - validates :crop_id, :presence => {:message => "must be present and exist in our database"} + validates :crop, :approved => true + + validates :crop, :presence => {:message => "must be present and exist in our database"} validates :quantity, - :numericality => { :only_integer => true }, + :numericality => { + :only_integer => true, + :greater_than_or_equal_to => 0 }, :allow_nil => true SUNNINESS_VALUES = %w(sun semi-shade shade) diff --git a/app/models/product.rb b/app/models/product.rb index 876207f47..68d483665 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -2,8 +2,11 @@ class Product < ActiveRecord::Base has_and_belongs_to_many :orders belongs_to :account_type - validates :paid_months, :numericality => { :only_integer => true, - :allow_nil => true } + validates :paid_months, + :numericality => { + :only_integer => true, + :greater_than_or_equal_to => 0 }, + :allow_nil => true def to_s name diff --git a/app/models/seed.rb b/app/models/seed.rb index c7128da72..9fa005907 100644 --- a/app/models/seed.rb +++ b/app/models/seed.rb @@ -7,9 +7,23 @@ class Seed < ActiveRecord::Base default_scope { order("created_at desc") } + validates :crop, :approved => true + validates :crop, :presence => {:message => "must be present and exist in our database"} validates :quantity, - :numericality => { :only_integer => true }, + :numericality => { + :only_integer => true, + :greater_than_or_equal_to => 0 }, + :allow_nil => true + validates :days_until_maturity_min, + :numericality => { + :only_integer => true, + :greater_than_or_equal_to => 0 }, + :allow_nil => true + validates :days_until_maturity_max, + :numericality => { + :only_integer => true, + :greater_than_or_equal_to => 0 }, :allow_nil => true scope :tradable, -> { where("tradable_to != 'nowhere'") } @@ -20,6 +34,32 @@ class Seed < ActiveRecord::Base :allow_nil => false, :allow_blank => false + ORGANIC_VALUES = [ + 'certified organic', + 'non-certified organic', + 'conventional/non-organic', + 'unknown'] + validates :organic, :inclusion => { :in => ORGANIC_VALUES, + :message => "You must say whether the seeds are organic or not, or that you don't know" }, + :allow_nil => false, + :allow_blank => false + + GMO_VALUES = [ + 'certified GMO-free', + 'non-certified GMO-free', + 'GMO', + 'unknown'] + validates :gmo, :inclusion => { :in => GMO_VALUES, + :message => "You must say whether the seeds are genetically modified or not, or that you don't know" }, + :allow_nil => false, + :allow_blank => false + + HEIRLOOM_VALUES = %w(heirloom hybrid unknown) + validates :heirloom, :inclusion => { :in => HEIRLOOM_VALUES, + :message => "You must say whether the seeds are heirloom, hybrid, or unknown" }, + :allow_nil => false, + :allow_blank => false + def tradable? if self.tradable_to == 'nowhere' return false diff --git a/app/validators/approved_validator.rb b/app/validators/approved_validator.rb new file mode 100644 index 000000000..4016058aa --- /dev/null +++ b/app/validators/approved_validator.rb @@ -0,0 +1,7 @@ +class ApprovedValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless record.crop.try(:approved?) + record.errors[attribute] << (options[:message] || 'must be approved') + end + end +end diff --git a/app/views/admin/index.html.haml b/app/views/admin/index.html.haml index 18f0a043e..958ed39ed 100644 --- a/app/views/admin/index.html.haml +++ b/app/views/admin/index.html.haml @@ -8,6 +8,7 @@ %li= link_to "Roles", roles_path %li= link_to "Forums", forums_path %li= link_to "Newsletter subscribers", admin_newsletter_path + %li= link_to "CMS", comfy_admin_cms_path %h2 Orders diff --git a/app/views/crops/_form.html.haml b/app/views/crops/_form.html.haml index 0c8fc8c45..e7be7cd62 100644 --- a/app/views/crops/_form.html.haml +++ b/app/views/crops/_form.html.haml @@ -6,40 +6,87 @@ - @crop.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. + - if can? :wrangle, @crop + %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 :name, :class => 'control-label col-md-2' .col-md-8 = f.text_field :name, :class => 'form-control' - %span.help-block Name in US English; singular; capitalize proper nouns only. + %span.help-block + - if can? :wrangle, @crop + Name in US English; singular; capitalize proper nouns only. + - else + Please provide the common name for the crop, in English, if you know it. + .form-group = f.label :en_wikipedia_url, 'Wikipedia URL', :class => 'control-label col-md-2' .col-md-8 = f.text_field :en_wikipedia_url, :class => 'form-control' - %span.help-block Link to this crop's page on the English language Wikipedia. - .form-group - = f.label :parent_id, 'Parent crop', :class => 'control-label col-md-2' - .col-md-8 - = collection_select(:crop, :parent_id, Crop.all, :id, :name, {:include_blank => true}, :class => 'form-control') - %span.help-block Optional. For setting up crop hierarchies for varieties etc. - %p - %span.help-block - You may enter up to 3 scientific names for a crop. Most crops will have only one. - = f.fields_for :scientific_names do |sn| + %span.help-block + - if can? :wrangle, @crop + Link to this crop's page on the English language Wikipedia. + - else + Please provide a link to the crop's page on the English language Wikipedia. Crops without Wikipedia pages cannot be added to our database at this time. + + - if can? :wrangle, @crop + .form-group - = sn.label :scientific_name, "Scientific name", :class => 'control-label col-md-2' + = f.label :parent_id, 'Parent crop', :class => 'control-label col-md-2' .col-md-8 - = sn.text_field :scientific_name, :class => 'form-control' - .col-md-2 - - if sn.object && sn.object.persisted? - %label.checkbox - = sn.check_box :_destroy - = sn.label :_destroy, "Delete" + = collection_select(:crop, :parent_id, Crop.all, :id, :name, {:include_blank => true}, :class => 'form-control') + %span.help-block Optional. For setting up crop hierarchies for varieties etc. + + %p + %span.help-block + You may enter up to 3 scientific names for a crop. Most crops will have only one. + = f.fields_for :scientific_names do |sn| + .form-group + = sn.label :scientific_name, "Scientific name", :class => 'control-label col-md-2' + .col-md-8 + = sn.text_field :scientific_name, :class => 'form-control' + .col-md-2 + - if sn.object && sn.object.persisted? + %label.checkbox + = sn.check_box :_destroy + = sn.label :_destroy, "Delete" + + - unless @crop.new_record? + .form-group + = f.label :approval_status, 'Approval Status', :class=> 'control-label col-md-2' + .col-md-8 + = f.select(:approval_status, @crop.approval_statuses, {}, {:class => 'form-control'}) + + .form-group + = f.label :reason_for_rejection, 'Reason for rejection', :class => 'control-label col-md-2' + .col-md-8 + = f.select(:reason_for_rejection, @crop.reasons_for_rejection, {:include_blank => true}, {:class => 'form-control'}) + %p + %span.help-block + Please provide additional notes why this crop request was rejected if the above reasons do not apply. + + .form-group + = f.label :rejection_notes, 'Rejection Notes', :class => 'control-label col-md-2' + .col-md-8= f.text_area :rejection_notes, :rows => 6, :class => 'form-control' + + - else + %p + %span.help-block + Provide any additional information that might help us assess your request. + + .form-group + = f.label :request_notes, 'Comments', :class => 'control-label col-md-2' + .col-md-8= f.text_area :request_notes, :rows => 6, :class => 'form-control' + + %p + %span.help-block + When you submit this form, your suggestion will be sent to our team of #{link_to 'volunteer crop wranglers', 'http://talk.growstuff.org/c/crop-wrangling'} for review. We'll let you know the outcome as soon as we can. + .form-group .form-actions.col-md-offset-2.col-md-8 = f.submit 'Save', :class => 'btn btn-primary' diff --git a/app/views/crops/edit.html.haml b/app/views/crops/edit.html.haml index 135f74a7c..dca7fb221 100644 --- a/app/views/crops/edit.html.haml +++ b/app/views/crops/edit.html.haml @@ -1,4 +1,4 @@ -- content_for :title, "Editing crop" +- content_for :title, "Review crop: #{@crop.name}" %p Added by diff --git a/app/views/crops/index.html.haml b/app/views/crops/index.html.haml index 1b5663c25..d1be6fa49 100644 --- a/app/views/crops/index.html.haml +++ b/app/views/crops/index.html.haml @@ -1,5 +1,8 @@ -- content_for :title, "Crops" +- content_for :title, "Browse crops" +- content_for :subtitle, "#{@crops.size} total" +- if can? :wrangle, Crop + = link_to 'Wrangle Crops', wrangle_crops_path, :class => 'btn btn-primary' %p #{ENV['GROWSTUFF_SITE_NAME']} tracks who's growing what, where. View any crop page to see which of our members have planted it and find @@ -12,11 +15,10 @@ = submit_tag "Show", :class => 'btn btn-primary' %div.pagination - = page_entries_info @crops, :model => "crops" - = will_paginate @crops + = will_paginate @paginated_crops .row - - @crops.each do |crop| + - @paginated_crops.each do |crop| .col-md-2.six-across = render :partial => "thumbnail", :locals => { :crop => crop } @@ -25,8 +27,7 @@ = link_to 'New Crop', new_crop_path, {:class => 'btn btn-primary'} %div.pagination - = page_entries_info @crops, :model => "crops" - = will_paginate @crops + = will_paginate @paginated_crops %ul.list-inline diff --git a/app/views/crops/new.html.haml b/app/views/crops/new.html.haml index 63382ee95..f472c4791 100644 --- a/app/views/crops/new.html.haml +++ b/app/views/crops/new.html.haml @@ -1,3 +1,14 @@ -- content_for :title, "New crop" +- content_for :title, (can?(:wrangle, @crop) ? "New crop" : "Suggest a crop") + +- unless can? :wrangler, @crop + + %p Thanks for taking the time to suggest a crop! Our crop database is managed by volunteers, and we appreciate your help. Here are some things to consider when suggesting a new crop: + + %ul + %li First, you might want to #{link_to 'search our crops', crops_search_path} to make sure we don't have it already, perhaps under an alternate name. + + %li The Growstuff database only contains edible crops. In future we hope to support other crops, but for now, if your suggestion is not edible we won't be able to add it. + + %li At this time, we are only adding crops which have a Wikipedia page. If you want to add a specific variety of crop that doesn't have its own Wikipedia entry, please use the more general form of the crop instead and put the name of your variety in the notes/description. = render 'form' diff --git a/app/views/crops/search.html.haml b/app/views/crops/search.html.haml index 2ed095402..9b57e1b7a 100644 --- a/app/views/crops/search.html.haml +++ b/app/views/crops/search.html.haml @@ -1,4 +1,16 @@ -- content_for :title, "Crops matching #{@search}" +- if @search + - content_for :title, "Crops matching \"#{@search}\"" + - if @all_matches + - content_for :subtitle, "#{@all_matches.size} total" +- else + - content_for :title, "Crop search" + +%div + = form_tag crops_search_path, :method => :get, :id => 'crop-search', :class => 'form-inline' do + .form-group + = label_tag :search, "Search crops:", :class => 'sr-only' + = text_field_tag 'search', nil, :class => 'search-query input-medium form-control', :placeholder => 'Search crops', :value => @search + = submit_tag "Search", :class => 'btn btn-primary' - if @all_matches.empty? %h2 No results found @@ -9,10 +21,15 @@ = link_to "browsing our crop database", crops_path instead. -- else - %div#all_matches +- else + %div.pagination + = will_paginate @paginated_matches + + %div#paginated_matches .row - - @all_matches.each do |c| + - @paginated_matches.each do |c| .col-md-2.six-across = render :partial => "thumbnail", :locals => { :crop => c } + %div.pagination + = will_paginate @paginated_matches diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 8ef425ecd..bda1804d2 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -1,14 +1,28 @@ - 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 @crop.pending? + .alert.alert-danger + %b This crop is currently pending approval. + %p This crop was requested by #{@crop.requester} #{time_ago_in_words(@crop.created_at)} ago. + - unless @crop.request_notes.blank? + %p + Request notes: #{@crop.request_notes} - - if can? :create, Seed - = link_to 'Add seeds to stash', new_seed_path(:params => { :crop_id => @crop.id }), :class => 'btn btn-default' +- if @crop.rejected? + .alert.alert-danger + %b This crop was rejected for the following reason: #{@crop.reason_for_rejection == "other" ? @crop.rejection_notes : @crop.reason_for_rejection}. + +- if @crop.approved? + - 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 diff --git a/app/views/crops/wrangle.html.haml b/app/views/crops/wrangle.html.haml index b26f29fc9..549a737a1 100644 --- a/app/views/crops/wrangle.html.haml +++ b/app/views/crops/wrangle.html.haml @@ -15,18 +15,38 @@ %li.crop_wrangler = link_to crop_wrangler.login_name, crop_wrangler -%h2 Recently added crops +.tabbable + %ul.nav.nav-tabs + %li{:class => @approval_status.blank? ? 'active' : ''} + = link_to "Recently added", wrangle_crops_path + %li{:class => @approval_status == "pending" ? 'active' : ''} + = link_to "Pending approval", wrangle_crops_path(:approval_status => "pending") + %li{:class => @approval_status == "rejected" ? 'active' : ''} + = link_to "Rejected", wrangle_crops_path(:approval_status => "rejected") + +%h2 + - if @approval_status == "pending" + Requested Crops + - elsif @approval_status == "rejected" + Rejected Crops + - else + Recently added crops + %div.pagination = page_entries_info @crops, :model => "crops" = will_paginate @crops -%table.table.table-striped +%table{:class => "table table-striped", :id => @approval_status.blank? ? 'recently-added-crops' : "#{@approval_status}-crops"} %tr %th System name %th English Wikipedia URL %th Scientific names - %th Added by + %th Requested by + - if @approval_status == "rejected" + %th Rejected by + - if @approval_status != "rejected" && @approval_status != "pending" + %th Added by %th When - @crops.each do |c| %tr @@ -36,7 +56,9 @@ - c.scientific_names.each do |s| = link_to s.scientific_name, s %br/ - %td= link_to c.creator, c.creator + %td= c.requester.present? ? (link_to c.requester, c.requester) : "N/A" + - unless @approval_status == "pending" + %td= c.creator.present? ? (link_to c.creator, c.creator) : "N/A" %td = distance_of_time_in_words(c.created_at, Time.zone.now) ago. @@ -44,3 +66,5 @@ %div.pagination = page_entries_info @crops, :model => "crops" = will_paginate @crops + + diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 3feaaa57b..e7dba1d25 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -2,28 +2,8 @@ .container .row .col-md-4#about-growstuff - %ul - %li= link_to t('about'), "http://wiki.growstuff.org/index.php/About%20Growstuff" - %li= link_to t('our_values'), "http://wiki.growstuff.org/index.php/Values" - %li= link_to t('open_source'), "https://github.com/Growstuff/growstuff" - %li= link_to t('growstuff_team'), "http://wiki.growstuff.org/index.php/Team" - %li= link_to t('get_involved'), "http://wiki.growstuff.org/index.php/Get_involved" + != cms_snippet_content(:footer1) .col-md-4#policies - %ul - %li= link_to t('terms_of_service'), url_for(:controller => '/policy', :action => 'tos') - %li= link_to t('privacy_policy'), url_for(:controller => '/policy', :action => 'privacy') - %li= link_to t('data_use_policy'), url_for(:controller => '/policy', :action => 'api') - %li= link_to t('community_guidelines'), url_for(:controller => '/policy', :action => 'community') + != cms_snippet_content(:footer2) .col-md-4#contact - %ul - %li= link_to t('support_'), url_for(:controller => '/support') - %li= link_to t('contact'), url_for(:controller => '/about', :action => 'contact') - %p - = link_to('http://twitter.com/growstufforg', :target => "_blank") do - = image_tag("twitter_32.png", :alt => 'Twitter: @growstufforg') -   - = link_to('https://www.facebook.com/Growstufforg', :target => "_blank") do - = image_tag("facebook_32.png", :alt => 'Facebook') -   - = link_to('http://blog.growstuff.org/', :target => "_blank") do - = image_tag("blog_32.png", :alt => 'Growstuff Blog') + != cms_snippet_content(:footer3) diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 691de3681..7560ebeec 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -65,12 +65,14 @@ %li= link_to "Sign out", destroy_member_session_path, :method => :delete - else - %li= link_to 'Sign in', new_member_session_path - %li= link_to 'Sign up', new_member_registration_path + %li= link_to 'Sign in', new_member_session_path, :id => 'navbar-signin' + %li= link_to 'Sign up', new_member_registration_path, :id => 'navbar-signup' - = form_tag crops_search_path, :method => :get, :class => 'navbar-form pull-right' do + = form_tag crops_search_path, :method => :get, :id => 'navbar-search', :class => 'navbar-form pull-right' do .input + = label_tag :search, "Search crop database:", :class => 'sr-only' = text_field_tag 'search', nil, :class => 'search-query input-medium form-control', :placeholder => 'Search crops' + = submit_tag "Search", :class => 'btn sr-only' - # anchor tag for accessibility link to skip the navigation menu %a{:name => 'skipnav'} diff --git a/app/views/members/index.html.haml b/app/views/members/index.html.haml index 83a663d47..36ba70887 100644 --- a/app/views/members/index.html.haml +++ b/app/views/members/index.html.haml @@ -1,5 +1,11 @@ = content_for :title, "#{ENV['GROWSTUFF_SITE_NAME']} members" += form_tag(members_path, :method => :get, :class => 'form-inline', :role => 'form') do + .form-group + = label_tag :sort, "Sort by:", :class => 'sr-only' + = select_tag "sort", options_for_select({"Sort alphabetically" => 'alpha', "Sort by recently joined" => "recently_joined"}, @sort || 'alpha'), :class => 'form-control' + = submit_tag "Show", :class => 'btn btn-primary' + %div.pagination = page_entries_info @members, :model => "members" = will_paginate @members diff --git a/app/views/notifier/_signature.html.haml b/app/views/notifier/_signature.html.haml new file mode 100644 index 000000000..14a7d47a7 --- /dev/null +++ b/app/views/notifier/_signature.html.haml @@ -0,0 +1,6 @@ +- site_name = ENV['GROWSTUFF_SITE_NAME'] + +%p + The #{site_name} team. + %br/ + =link_to root_url, root_url diff --git a/app/views/notifier/crop_request_approved.html.haml b/app/views/notifier/crop_request_approved.html.haml new file mode 100644 index 000000000..5fea20246 --- /dev/null +++ b/app/views/notifier/crop_request_approved.html.haml @@ -0,0 +1,16 @@ +- site_name = ENV['GROWSTUFF_SITE_NAME'] +%p Hello #{@member.login_name}, + +%p + Your request for the new crop: #{link_to @crop.name, crop_url(@crop)} has been approved! + +%ul + %li + = link_to "Plant #{@crop.name}", new_planting_url(crop_id: @crop.id) + %li + = link_to "Harvest #{@crop.name}", new_harvest_url(crop_id: @crop.id) + %li + = link_to "Stash seeds for #{@crop.name}", new_seed_url(crop_id: @crop.id) + += render :partial => 'signature' + diff --git a/app/views/notifier/crop_request_rejected.html.haml b/app/views/notifier/crop_request_rejected.html.haml new file mode 100644 index 000000000..c58e2546b --- /dev/null +++ b/app/views/notifier/crop_request_rejected.html.haml @@ -0,0 +1,9 @@ +- site_name = ENV['GROWSTUFF_SITE_NAME'] +%p Hello #{@member.login_name}, + +%p + Your request for the new crop: #{link_to @crop.name, crop_url(@crop)} has been rejected for the following reason. + + = @crop.reason_for_rejection + += render :partial => 'signature' \ No newline at end of file diff --git a/app/views/notifier/new_crop_request.html.haml b/app/views/notifier/new_crop_request.html.haml new file mode 100644 index 000000000..163c898f4 --- /dev/null +++ b/app/views/notifier/new_crop_request.html.haml @@ -0,0 +1,21 @@ +- site_name = ENV['GROWSTUFF_SITE_NAME'] + +%p Hello #{@member.login_name}, + +%p + #{@request.requester.login_name} has requested a new crop on #{site_name}. + +%ul + %li Name: #{@request.name} + %li Wikipedia URL: #{@request.en_wikipedia_url.present? ? @request.en_wikipedia_url : "not specified"} + +%p + As a crop wrangler, you can #{link_to "approve or reject this request", edit_crop_url(@request)}. + +%p + Or, discuss this and other crop wrangling issues in our #{link_to "crop wrangling forum", "http://talk.growstuff.org/c/crop-wrangling"}. + +%p + Thanks for your help! + += render :partial => 'signature' \ No newline at end of file diff --git a/app/views/notifier/notify.html.haml b/app/views/notifier/notify.html.haml index b6e962fb3..ed9cf32f2 100644 --- a/app/views/notifier/notify.html.haml +++ b/app/views/notifier/notify.html.haml @@ -20,7 +20,4 @@ %br/ = link_to "Turn off these notifications", edit_member_registration_url -%p - The #{site_name} team. - %br/ - =link_to root_url, root_url += render :partial => 'signature' \ No newline at end of file diff --git a/app/views/notifier/planting_reminder.html.haml b/app/views/notifier/planting_reminder.html.haml index 7edf063b5..4dea05123 100644 --- a/app/views/notifier/planting_reminder.html.haml +++ b/app/views/notifier/planting_reminder.html.haml @@ -59,10 +59,7 @@ %h2 See you soon on #{site_name}! -%p - The #{site_name} team. - %br/ - =link_to root_url, root_url += render :partial => 'signature' %hr/ %p diff --git a/app/views/seeds/_days_until_maturity.html.haml b/app/views/seeds/_days_until_maturity.html.haml new file mode 100644 index 000000000..b8db4a203 --- /dev/null +++ b/app/views/seeds/_days_until_maturity.html.haml @@ -0,0 +1,9 @@ +- if seed.days_until_maturity_min.blank? + - if seed.days_until_maturity_max.blank? + unknown + - else + = seed.days_until_maturity_max.to_s +- elsif seed.days_until_maturity_max.blank? + = seed.days_until_maturity_min.to_s +- else + != "#{seed.days_until_maturity_min}–#{seed.days_until_maturity_max}" diff --git a/app/views/seeds/_form.html.haml b/app/views/seeds/_form.html.haml index 642a5de01..8d8eeaa97 100644 --- a/app/views/seeds/_form.html.haml +++ b/app/views/seeds/_form.html.haml @@ -21,6 +21,29 @@ = f.label :plant_before, 'Plant before:', :class => 'control-label col-md-2' .col-md-2 = f.text_field :plant_before, :class => 'add-datepicker form-control', :value => @seed.plant_before ? @seed.plant_before.to_s(:ymd) : '' + .form-group + = f.label :days_until_maturity_min, 'Days until maturity:', :class => 'control-label col-md-2' + %fieldset + .col-md-2 + = f.number_field :days_until_maturity_min, :class => 'form-control' + .col-md-1 + = f.label :days_until_maturity_max, 'to', :class => 'control-label' + .col-md-2 + = f.number_field :days_until_maturity_max, :class => 'form-control' + .col-md-1 + = f.label :dummy, 'days', :class => 'control-label' + .form-group + = f.label :organic, 'Organic?', :class => 'control-label col-md-2' + .col-md-8 + = f.select(:organic, Seed::ORGANIC_VALUES, {}, :class => 'form-control', :default => 'unknown') + .form-group + = f.label :gmo, 'GMO?', :class => 'control-label col-md-2' + .col-md-8 + = f.select(:gmo, Seed::GMO_VALUES, {}, :class => 'form-control', :default => 'unknown') + .form-group + = f.label :heirloom, 'Heirloom?', :class => 'control-label col-md-2' + .col-md-8 + = f.select(:heirloom, Seed::HEIRLOOM_VALUES, {}, :class => 'form-control', :default => 'unknown') .form-group = f.label :description, 'Description:', :class => 'control-label col-md-2' .col-md-8 diff --git a/app/views/seeds/show.html.haml b/app/views/seeds/show.html.haml index 7db91c1a6..c2f3399ea 100644 --- a/app/views/seeds/show.html.haml +++ b/app/views/seeds/show.html.haml @@ -13,6 +13,18 @@ %p %b Plant before: = @seed.plant_before.to_s + %p + %b Days until maturity: + = render :partial => 'days_until_maturity', :locals => { :seed => @seed } + %p + %b Organic? + = @seed.organic + %p + %b GMO? + = @seed.gmo + %p + %b Heirloom? + = @seed.heirloom %p %b Will trade: = @seed.tradable_to diff --git a/config/initializers/comfortable_mexican_sofa.rb b/config/initializers/comfortable_mexican_sofa.rb new file mode 100644 index 000000000..fe6e80ace --- /dev/null +++ b/config/initializers/comfortable_mexican_sofa.rb @@ -0,0 +1,103 @@ +# encoding: utf-8 + +ComfortableMexicanSofa.configure do |config| + # Title of the admin area + # config.cms_title = 'ComfortableMexicanSofa CMS Engine' + + # Controller that is inherited from CmsAdmin::BaseController + # config.base_controller = 'ApplicationController' + + # Module responsible for authentication. You can replace it with your own. + # It simply needs to have #authenticate method. See http_auth.rb for reference. + config.admin_auth = 'CmsDeviseAuth' + + # Module responsible for authorization on admin side. It should have #authorize + # method that returns true or false based on params and loaded instance + # variables available for a given controller. + # config.admin_authorization = 'ComfyAdminAuthorization' + + # Module responsible for public authentication. Similar to the above. You also + # will have access to @cms_site, @cms_layout, @cms_page so you can use them in + # your logic. Default module doesn't do anything. + # config.public_auth = 'ComfyPublicAuthentication' + + # When arriving at /cms-admin you may chose to redirect to arbirtary path, + # for example '/cms-admin/users' + # config.admin_route_redirect = '' + + # File uploads use Paperclip and can support filesystem or s3 uploads. Override + # the upload method and appropriate settings based on Paperclip. For S3 see: + # http://rdoc.info/gems/paperclip/2.3.8/Paperclip/Storage/S3, and for + # filesystem see: http://rdoc.info/gems/paperclip/2.3.8/Paperclip/Storage/Filesystem + # If you are using S3 and HTTPS, pass :s3_protocol => '' to have URLs that use the protocol of the page + # config.upload_file_options = {:url => '/system/:class/:id/:attachment/:style/:filename'} + + # Sofa allows you to setup entire site from files. Database is updated with each + # request (if necessary). Please note that database entries are destroyed if there's + # no corresponding file. Fixtures are disabled by default. + # config.enable_fixtures = false + + # Path where fixtures can be located. + # config.fixtures_path = File.expand_path('db/cms_fixtures', Rails.root) + + # Importing fixtures into Database + # To load fixtures into the database just run this rake task: + # local: $ rake comfortable_mexican_sofa:fixtures:import FROM=example.local TO=localhost + # Heroku: $ heroku run rake comfortable_mexican_sofa:fixtures:import FROM=example.local TO=yourapp.herokuapp.com + # From indicates folder the fixtures are in and to is the Site hostname you have defined in the database. + + # Exporting fixtures into Files + # If you need to dump database contents into fixture files run: + # local: $ rake comfortable_mexican_sofa:fixtures:export FROM=localhost TO=example.local + # Heroku: $ heroku run rake comfortable_mexican_sofa:fixtures:export FROM=yourapp.herokuapp.com TO=example.local + # This will create example.local folder and dump all content from example.com Site. + + # Content for Layouts, Pages and Snippets has a revision history. You can revert + # a previous version using this system. You can control how many revisions per + # object you want to keep. Set it to 0 if you wish to turn this feature off. + # config.revisions_limit = 25 + + # Locale definitions. If you want to define your own locale merge + # {:locale => 'Locale Title'} with this. + # config.locales = {:en => 'English', :es => 'Español'} + + # Admin interface will respect the locale of the site being managed. However you can + # force it to English by setting this to `:en` + # config.admin_locale = nil + + # A class that is included as a sweeper to admin base controller if it's set + # config.admin_cache_sweeper = nil + + # By default you cannot have irb code inside your layouts/pages/snippets. + # Generally this is to prevent putting something like this: + # <% User.delete_all %> but if you really want to allow it... + # config.allow_irb = false + + # Whitelist of all helper methods that can be used via {{cms:helper}} tag. By default + # all helpers are allowed except `eval`, `send`, `call` and few others. Empty array + # will prevent rendering of all helpers. + # config.allowed_helpers = nil + + # Whitelist of partials paths that can be used via {{cms:partial}} tag. All partials + # are accessible by default. Empty array will prevent rendering of all partials. + # config.allowed_partials = nil + + # Site aliases, if you want to have aliases for your site. Good for harmonizing + # production env with dev/testing envs. + # e.g. config.hostname_aliases = {'host.com' => 'host.inv', 'host_a.com' => ['host.lvh.me', 'host.dev']} + # Default is nil (not used) + # config.hostname_aliases = nil + + # Reveal partials that can be overwritten in the admin area. + # Default is false. + # config.reveal_cms_partials = false + +end + +module CmsDeviseAuth + def authenticate + unless current_member && current_member.has_role?(:admin) + redirect_to root_path, :alert => 'Permission denied. Please sign in as an admin user to use the CMS admin area.' + end + end +end diff --git a/config/routes.rb b/config/routes.rb index ceca68687..69680ae2d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Growstuff::Application.routes.draw do + resources :plant_parts devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" } @@ -84,6 +85,8 @@ Growstuff::Application.routes.draw do get '/admin/newsletter' => 'admin#newsletter', :as => :admin_newsletter get '/admin/:action' => 'admin#:action' - +# CMS stuff -- must remain LAST + comfy_route :cms_admin, :path => '/cms/admin' + comfy_route :cms, :path => '/', :sitemap => false end diff --git a/db/migrate/20150124110540_add_properties_to_seeds.rb b/db/migrate/20150124110540_add_properties_to_seeds.rb new file mode 100644 index 000000000..e2fa64fe7 --- /dev/null +++ b/db/migrate/20150124110540_add_properties_to_seeds.rb @@ -0,0 +1,9 @@ +class AddPropertiesToSeeds < ActiveRecord::Migration + def change + add_column :seeds, :days_until_maturity_min, :integer + add_column :seeds, :days_until_maturity_max, :integer + add_column :seeds, :organic, :text, :default => 'unknown' + add_column :seeds, :gmo, :text, :default => 'unknown' + add_column :seeds, :heirloom, :text, :default => 'unknown' + end +end diff --git a/db/migrate/20150130224814_add_requester_to_crops.rb b/db/migrate/20150130224814_add_requester_to_crops.rb new file mode 100644 index 000000000..f284a6f68 --- /dev/null +++ b/db/migrate/20150130224814_add_requester_to_crops.rb @@ -0,0 +1,6 @@ +class AddRequesterToCrops < ActiveRecord::Migration + def change + add_column :crops, :requester_id, :integer + add_index :crops, :requester_id + end +end diff --git a/db/migrate/20150201052245_create_cms.rb b/db/migrate/20150201052245_create_cms.rb new file mode 100644 index 000000000..9e999488f --- /dev/null +++ b/db/migrate/20150201052245_create_cms.rb @@ -0,0 +1,140 @@ +class CreateCms < ActiveRecord::Migration + + def self.up + + text_limit = case ActiveRecord::Base.connection.adapter_name + when 'PostgreSQL' + { } + else + { :limit => 16777215 } + end + + # -- Sites -------------------------------------------------------------- + create_table :comfy_cms_sites do |t| + t.string :label, :null => false + t.string :identifier, :null => false + t.string :hostname, :null => false + t.string :path + t.string :locale, :null => false, :default => 'en' + t.boolean :is_mirrored, :null => false, :default => false + end + add_index :comfy_cms_sites, :hostname + add_index :comfy_cms_sites, :is_mirrored + + # -- Layouts ------------------------------------------------------------ + create_table :comfy_cms_layouts do |t| + t.integer :site_id, :null => false + t.integer :parent_id + t.string :app_layout + t.string :label, :null => false + t.string :identifier, :null => false + t.text :content, text_limit + t.text :css, text_limit + t.text :js, text_limit + t.integer :position, :null => false, :default => 0 + t.boolean :is_shared, :null => false, :default => false + t.timestamps + end + add_index :comfy_cms_layouts, [:parent_id, :position] + add_index :comfy_cms_layouts, [:site_id, :identifier], :unique => true + + # -- Pages -------------------------------------------------------------- + create_table :comfy_cms_pages do |t| + t.integer :site_id, :null => false + t.integer :layout_id + t.integer :parent_id + t.integer :target_page_id + t.string :label, :null => false + t.string :slug + t.string :full_path, :null => false + t.text :content_cache, text_limit + t.integer :position, :null => false, :default => 0 + t.integer :children_count, :null => false, :default => 0 + t.boolean :is_published, :null => false, :default => true + t.boolean :is_shared, :null => false, :default => false + t.timestamps + end + add_index :comfy_cms_pages, [:site_id, :full_path] + add_index :comfy_cms_pages, [:parent_id, :position] + + # -- Page Blocks -------------------------------------------------------- + create_table :comfy_cms_blocks do |t| + t.string :identifier, :null => false + t.text :content, text_limit + t.references :blockable, :polymorphic => true + t.timestamps + end + add_index :comfy_cms_blocks, [:identifier] + add_index :comfy_cms_blocks, [:blockable_id, :blockable_type] + + # -- Snippets ----------------------------------------------------------- + create_table :comfy_cms_snippets do |t| + t.integer :site_id, :null => false + t.string :label, :null => false + t.string :identifier, :null => false + t.text :content, text_limit + t.integer :position, :null => false, :default => 0 + t.boolean :is_shared, :null => false, :default => false + t.timestamps + end + add_index :comfy_cms_snippets, [:site_id, :identifier], :unique => true + add_index :comfy_cms_snippets, [:site_id, :position] + + # -- Files -------------------------------------------------------------- + create_table :comfy_cms_files do |t| + t.integer :site_id, :null => false + t.integer :block_id + t.string :label, :null => false + t.string :file_file_name, :null => false + t.string :file_content_type, :null => false + t.integer :file_file_size, :null => false + t.string :description, :limit => 2048 + t.integer :position, :null => false, :default => 0 + t.timestamps + end + add_index :comfy_cms_files, [:site_id, :label] + add_index :comfy_cms_files, [:site_id, :file_file_name] + add_index :comfy_cms_files, [:site_id, :position] + add_index :comfy_cms_files, [:site_id, :block_id] + + # -- Revisions ----------------------------------------------------------- + create_table :comfy_cms_revisions, :force => true do |t| + t.string :record_type, :null => false + t.integer :record_id, :null => false + t.text :data, text_limit + t.datetime :created_at + end + add_index :comfy_cms_revisions, [:record_type, :record_id, :created_at], + :name => 'index_cms_revisions_on_rtype_and_rid_and_created_at' + + # -- Categories --------------------------------------------------------- + create_table :comfy_cms_categories, :force => true do |t| + t.integer :site_id, :null => false + t.string :label, :null => false + t.string :categorized_type, :null => false + end + add_index :comfy_cms_categories, [:site_id, :categorized_type, :label], :unique => true, + :name => 'index_cms_categories_on_site_id_and_cat_type_and_label' + + create_table :comfy_cms_categorizations, :force => true do |t| + t.integer :category_id, :null => false + t.string :categorized_type, :null => false + t.integer :categorized_id, :null => false + end + add_index :comfy_cms_categorizations, [:category_id, :categorized_type, :categorized_id], :unique => true, + :name => 'index_cms_categorizations_on_cat_id_and_catd_type_and_catd_id' + end + + def self.down + drop_table :comfy_cms_sites + drop_table :comfy_cms_layouts + drop_table :comfy_cms_pages + drop_table :comfy_cms_snippets + drop_table :comfy_cms_blocks + drop_table :comfy_cms_files + drop_table :comfy_cms_revisions + drop_table :comfy_cms_categories + drop_table :comfy_cms_categorizations + end +end + diff --git a/db/migrate/20150201053200_add_approval_status_to_crops.rb b/db/migrate/20150201053200_add_approval_status_to_crops.rb new file mode 100644 index 000000000..07b1e0879 --- /dev/null +++ b/db/migrate/20150201053200_add_approval_status_to_crops.rb @@ -0,0 +1,5 @@ +class AddApprovalStatusToCrops < ActiveRecord::Migration + def change + add_column :crops, :approval_status, :string, default: "approved" + end +end diff --git a/db/migrate/20150201062506_add_reason_for_rejection_to_crops.rb b/db/migrate/20150201062506_add_reason_for_rejection_to_crops.rb new file mode 100644 index 000000000..0fd283a67 --- /dev/null +++ b/db/migrate/20150201062506_add_reason_for_rejection_to_crops.rb @@ -0,0 +1,5 @@ +class AddReasonForRejectionToCrops < ActiveRecord::Migration + def change + add_column :crops, :reason_for_rejection, :text + end +end diff --git a/db/migrate/20150201064502_add_request_notes_to_crops.rb b/db/migrate/20150201064502_add_request_notes_to_crops.rb new file mode 100644 index 000000000..0720bf4cb --- /dev/null +++ b/db/migrate/20150201064502_add_request_notes_to_crops.rb @@ -0,0 +1,5 @@ +class AddRequestNotesToCrops < ActiveRecord::Migration + def change + add_column :crops, :request_notes, :text + end +end diff --git a/db/migrate/20150209105410_add_rejection_notes_to_crops.rb b/db/migrate/20150209105410_add_rejection_notes_to_crops.rb new file mode 100644 index 000000000..59239fd55 --- /dev/null +++ b/db/migrate/20150209105410_add_rejection_notes_to_crops.rb @@ -0,0 +1,5 @@ +class AddRejectionNotesToCrops < ActiveRecord::Migration + def change + add_column :crops, :rejection_notes, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index bf80810c7..7f8c479ce 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150129034206) do +ActiveRecord::Schema.define(version: 20150209105410) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -20,16 +20,16 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.string "name", null: false t.boolean "is_paid" t.boolean "is_permanent_paid" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "accounts", force: true do |t| t.integer "member_id", null: false t.integer "account_type_id" t.datetime "paid_until" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "alternate_names", force: true do |t| @@ -46,33 +46,158 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.string "uid" t.string "token" t.string "secret" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "name" end add_index "authentications", ["member_id"], name: "index_authentications_on_member_id", using: :btree + create_table "comfy_cms_blocks", force: true do |t| + t.string "identifier", null: false + t.text "content" + t.integer "blockable_id" + t.string "blockable_type" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "comfy_cms_blocks", ["blockable_id", "blockable_type"], name: "index_comfy_cms_blocks_on_blockable_id_and_blockable_type", using: :btree + add_index "comfy_cms_blocks", ["identifier"], name: "index_comfy_cms_blocks_on_identifier", using: :btree + + create_table "comfy_cms_categories", force: true do |t| + t.integer "site_id", null: false + t.string "label", null: false + t.string "categorized_type", null: false + end + + add_index "comfy_cms_categories", ["site_id", "categorized_type", "label"], name: "index_cms_categories_on_site_id_and_cat_type_and_label", unique: true, using: :btree + + create_table "comfy_cms_categorizations", force: true do |t| + t.integer "category_id", null: false + t.string "categorized_type", null: false + t.integer "categorized_id", null: false + end + + add_index "comfy_cms_categorizations", ["category_id", "categorized_type", "categorized_id"], name: "index_cms_categorizations_on_cat_id_and_catd_type_and_catd_id", unique: true, using: :btree + + create_table "comfy_cms_files", force: true do |t| + t.integer "site_id", null: false + t.integer "block_id" + t.string "label", null: false + t.string "file_file_name", null: false + t.string "file_content_type", null: false + t.integer "file_file_size", null: false + t.string "description", limit: 2048 + t.integer "position", default: 0, null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "comfy_cms_files", ["site_id", "block_id"], name: "index_comfy_cms_files_on_site_id_and_block_id", using: :btree + add_index "comfy_cms_files", ["site_id", "file_file_name"], name: "index_comfy_cms_files_on_site_id_and_file_file_name", using: :btree + add_index "comfy_cms_files", ["site_id", "label"], name: "index_comfy_cms_files_on_site_id_and_label", using: :btree + add_index "comfy_cms_files", ["site_id", "position"], name: "index_comfy_cms_files_on_site_id_and_position", using: :btree + + create_table "comfy_cms_layouts", force: true do |t| + t.integer "site_id", null: false + t.integer "parent_id" + t.string "app_layout" + t.string "label", null: false + t.string "identifier", null: false + t.text "content" + t.text "css" + t.text "js" + t.integer "position", default: 0, null: false + t.boolean "is_shared", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "comfy_cms_layouts", ["parent_id", "position"], name: "index_comfy_cms_layouts_on_parent_id_and_position", using: :btree + add_index "comfy_cms_layouts", ["site_id", "identifier"], name: "index_comfy_cms_layouts_on_site_id_and_identifier", unique: true, using: :btree + + create_table "comfy_cms_pages", force: true do |t| + t.integer "site_id", null: false + t.integer "layout_id" + t.integer "parent_id" + t.integer "target_page_id" + t.string "label", null: false + t.string "slug" + t.string "full_path", null: false + t.text "content_cache" + t.integer "position", default: 0, null: false + t.integer "children_count", default: 0, null: false + t.boolean "is_published", default: true, null: false + t.boolean "is_shared", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "comfy_cms_pages", ["parent_id", "position"], name: "index_comfy_cms_pages_on_parent_id_and_position", using: :btree + add_index "comfy_cms_pages", ["site_id", "full_path"], name: "index_comfy_cms_pages_on_site_id_and_full_path", using: :btree + + create_table "comfy_cms_revisions", force: true do |t| + t.string "record_type", null: false + t.integer "record_id", null: false + t.text "data" + t.datetime "created_at" + end + + add_index "comfy_cms_revisions", ["record_type", "record_id", "created_at"], name: "index_cms_revisions_on_rtype_and_rid_and_created_at", using: :btree + + create_table "comfy_cms_sites", force: true do |t| + t.string "label", null: false + t.string "identifier", null: false + t.string "hostname", null: false + t.string "path" + t.string "locale", default: "en", null: false + t.boolean "is_mirrored", default: false, null: false + end + + add_index "comfy_cms_sites", ["hostname"], name: "index_comfy_cms_sites_on_hostname", using: :btree + add_index "comfy_cms_sites", ["is_mirrored"], name: "index_comfy_cms_sites_on_is_mirrored", using: :btree + + create_table "comfy_cms_snippets", force: true do |t| + t.integer "site_id", null: false + t.string "label", null: false + t.string "identifier", null: false + t.text "content" + t.integer "position", default: 0, null: false + t.boolean "is_shared", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "comfy_cms_snippets", ["site_id", "identifier"], name: "index_comfy_cms_snippets_on_site_id_and_identifier", unique: true, using: :btree + add_index "comfy_cms_snippets", ["site_id", "position"], name: "index_comfy_cms_snippets_on_site_id_and_position", using: :btree + create_table "comments", force: true do |t| t.integer "post_id", null: false t.integer "author_id", null: false t.text "body", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "crops", force: true do |t| - t.string "name", null: false + t.string "name", null: false t.string "en_wikipedia_url" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.integer "parent_id" - t.integer "plantings_count", default: 0 + t.integer "plantings_count", default: 0 t.integer "creator_id" + t.integer "requester_id" + t.string "approval_status", default: "approved" + t.text "reason_for_rejection" + t.text "request_notes" + t.text "rejection_notes" end add_index "crops", ["name"], name: "index_crops_on_name", using: :btree + add_index "crops", ["requester_id"], name: "index_crops_on_requester_id", using: :btree add_index "crops", ["slug"], name: "index_crops_on_slug", unique: true, using: :btree create_table "crops_posts", id: false, force: true do |t| @@ -94,8 +219,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.string "name", null: false t.text "description", null: false t.integer "owner_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" end @@ -105,8 +230,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.string "name", null: false t.integer "owner_id" t.string "slug", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.text "description" t.boolean "active", default: true t.string "location" @@ -116,7 +241,7 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.string "area_unit" end - add_index "gardens", ["owner_id"], name: "index_gardens_on_user_id", using: :btree + add_index "gardens", ["owner_id"], name: "index_gardens_on_owner_id", using: :btree add_index "gardens", ["slug"], name: "index_gardens_on_slug", unique: true, using: :btree create_table "gardens_photos", id: false, force: true do |t| @@ -133,8 +258,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.decimal "quantity" t.string "unit" t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.decimal "weight_quantity" t.string "weight_unit" @@ -167,8 +292,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.integer "failed_attempts", default: 0 t.string "unlock_token" t.datetime "locked_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "login_name" t.string "slug" t.boolean "tos_agreement" @@ -183,11 +308,11 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.boolean "send_planting_reminder", default: true end - add_index "members", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree - add_index "members", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "members", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - add_index "members", ["slug"], name: "index_users_on_slug", unique: true, using: :btree - add_index "members", ["unlock_token"], name: "index_users_on_unlock_token", unique: true, using: :btree + add_index "members", ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true, using: :btree + add_index "members", ["email"], name: "index_members_on_email", unique: true, using: :btree + add_index "members", ["reset_password_token"], name: "index_members_on_reset_password_token", unique: true, using: :btree + add_index "members", ["slug"], name: "index_members_on_slug", unique: true, using: :btree + add_index "members", ["unlock_token"], name: "index_members_on_unlock_token", unique: true, using: :btree create_table "members_roles", id: false, force: true do |t| t.integer "member_id" @@ -201,8 +326,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.text "body" t.boolean "read", default: false t.integer "post_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "order_items", force: true do |t| @@ -210,13 +335,13 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.integer "product_id" t.integer "price" t.integer "quantity" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "orders", force: true do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.datetime "completed_at" t.integer "member_id" t.string "paypal_express_token" @@ -233,8 +358,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.integer "owner_id", null: false t.string "thumbnail_url", null: false t.string "fullsize_url", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "title", null: false t.string "license_name", null: false t.string "license_url" @@ -249,8 +374,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do create_table "plant_parts", force: true do |t| t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" end @@ -260,8 +385,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.date "planted_at" t.integer "quantity" t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.string "sunniness" t.string "planted_from" @@ -276,21 +401,22 @@ ActiveRecord::Schema.define(version: 20150129034206) do t.integer "author_id", null: false t.string "subject", null: false t.text "body", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" 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", using: :btree - add_index "posts", ["slug"], name: "index_updates_on_slug", unique: true, using: :btree + add_index "posts", ["created_at", "author_id"], name: "index_posts_on_created_at_and_author_id", using: :btree + add_index "posts", ["slug"], name: "index_posts_on_slug", unique: true, using: :btree create_table "products", force: true do |t| t.string "name", null: false t.text "description", null: false t.integer "min_price", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.integer "account_type_id" t.integer "paid_months" t.integer "recommended_price" @@ -299,8 +425,8 @@ ActiveRecord::Schema.define(version: 20150129034206) do create_table "roles", force: true do |t| t.string "name", null: false t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" end @@ -309,21 +435,26 @@ ActiveRecord::Schema.define(version: 20150129034206) do create_table "scientific_names", force: true do |t| t.string "scientific_name", null: false t.integer "crop_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.integer "creator_id" end create_table "seeds", force: true do |t| - t.integer "owner_id", null: false - t.integer "crop_id", null: false + t.integer "owner_id", null: false + t.integer "crop_id", null: false t.text "description" t.integer "quantity" t.date "plant_before" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "tradable_to", default: "nowhere" + t.datetime "created_at" + t.datetime "updated_at" + t.string "tradable_to", default: "nowhere" t.string "slug" + t.integer "days_until_maturity_min" + t.integer "days_until_maturity_max" + t.text "organic", default: "unknown" + t.text "gmo", default: "unknown" + t.text "heirloom", default: "unknown" end add_index "seeds", ["slug"], name: "index_seeds_on_slug", unique: true, using: :btree diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 35365ae1e..bfb7b8642 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -319,6 +319,7 @@ namespace :growstuff do task :populate_si_weight => :environment do Harvest.find_each do |h| h.set_si_weight + h.save end end diff --git a/spec/controllers/crops_controller_spec.rb b/spec/controllers/crops_controller_spec.rb index 5cfe9f4d1..54226c27c 100644 --- a/spec/controllers/crops_controller_spec.rb +++ b/spec/controllers/crops_controller_spec.rb @@ -7,7 +7,8 @@ describe CropsController do def valid_attributes { :name => "Tomato", - :en_wikipedia_url => 'http://en.wikipedia.org/wiki/Tomato' + :en_wikipedia_url => 'http://en.wikipedia.org/wiki/Tomato', + :approval_status => 'approved' } end diff --git a/spec/factories/crop.rb b/spec/factories/crop.rb index c6839e983..4be8c4f09 100644 --- a/spec/factories/crop.rb +++ b/spec/factories/crop.rb @@ -3,6 +3,7 @@ FactoryGirl.define do factory :crop do name "magic bean" en_wikipedia_url "http://en.wikipedia.org/wiki/Magic_bean" + approval_status "approved" creator factory :tomato do @@ -54,6 +55,21 @@ FactoryGirl.define do name "Swiss chard" end + #for testing crop request + factory :crop_request do + name "Ultra berry" + en_wikipedia_url "" + approval_status "pending" + association :requester, factory: :member + request_notes "Please approve this even though it's fake." + end + + factory :rejected_crop do + name "Fail bean" + approval_status "rejected" + reason_for_rejection "Totally fake" + end + end end diff --git a/spec/factories/seeds.rb b/spec/factories/seeds.rb index bd5944d1b..323cdf2f2 100644 --- a/spec/factories/seeds.rb +++ b/spec/factories/seeds.rb @@ -8,6 +8,11 @@ FactoryGirl.define do quantity 1 plant_before "2013-07-15" tradable_to 'nowhere' + organic 'unknown' + gmo 'unknown' + heirloom 'unknown' + days_until_maturity_min nil + days_until_maturity_max nil factory :tradable_seed do tradable_to "locally" diff --git a/spec/features/cms_spec.rb b/spec/features/cms_spec.rb new file mode 100644 index 000000000..fb3f2ca93 --- /dev/null +++ b/spec/features/cms_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +feature "cms admin" do + before(:each) do + @member = FactoryGirl.create(:member) + @admin_member = FactoryGirl.create(:admin_member) + end + + scenario "can't view CMS admin if not signed in" do + visit comfy_admin_cms_path + current_path.should == root_path + page.should have_content("Please sign in as an admin user") + end + + scenario "can't view CMS admin if not an admin member" do + # sign in as an ordinary member + visit root_path + click_link 'navbar-signin' + fill_in 'Login', :with => @member.email + fill_in 'Password', :with => @member.password + click_button 'Sign in' + visit comfy_admin_cms_path + current_path.should == root_path + page.should have_content("Please sign in as an admin user") + end + + scenario "admin members can view CMS admin area" do + visit root_path + # now we sign in as an admin member + click_link 'navbar-signin' + fill_in 'Login', :with => @admin_member.email + fill_in 'Password', :with => @admin_member.password + click_button 'Sign in' + visit comfy_admin_cms_path + current_path.should match /#{comfy_admin_cms_path}/ # match any CMS admin page + end +end diff --git a/spec/features/crops/browse_crops_spec.rb b/spec/features/crops/browse_crops_spec.rb new file mode 100644 index 000000000..5df5dbd36 --- /dev/null +++ b/spec/features/crops/browse_crops_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +feature "browse crops" do + + let(:tomato) { FactoryGirl.create(:tomato) } + let(:maize) { FactoryGirl.create(:maize) } + + scenario "has a form for sorting by" do + visit crops_path + expect(page).to have_css "select#sort" + end + + scenario "shows a list of crops" do + crop1 = tomato + visit crops_path + expect(page).to have_content crop1.name + end + +end diff --git a/spec/features/crops/crop_search_spec.rb b/spec/features/crops/crop_search_spec.rb new file mode 100644 index 000000000..48e68dfc2 --- /dev/null +++ b/spec/features/crops/crop_search_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature "crop search" do + scenario "search results show the search term in title" do + visit root_path + within "form#navbar-search" do + fill_in "search", with: "tomato" + click_button "Search" + end + expect(page).to have_css "h1", text: "Crops matching \"tomato\"" + end + + scenario "search page with no search term shows suitable title" do + visit crops_search_path + expect(page).to have_css "h1", text: "Crop search" + end + + scenario "search page has a search form on it" do + visit crops_search_path + expect(page).to have_css "form#crop-search" + end +end diff --git a/spec/features/crops/crop_wranglers_spec.rb b/spec/features/crops/crop_wranglers_spec.rb index 0e0b08fbe..1b2af3f06 100644 --- a/spec/features/crops/crop_wranglers_spec.rb +++ b/spec/features/crops/crop_wranglers_spec.rb @@ -5,6 +5,8 @@ feature "crop wranglers" do let!(:crop_wranglers) { FactoryGirl.create_list(:crop_wrangling_member, 3) } let(:wrangler){crop_wranglers.first} let!(:crops) { FactoryGirl.create_list(:crop, 2) } + let!(:requested_crop) { FactoryGirl.create(:crop_request) } + let!(:rejected_crop) { FactoryGirl.create(:rejected_crop) } background do login_as(wrangler) @@ -25,7 +27,7 @@ feature "crop wranglers" do scenario "can see list of crops with extra detail of who created a crop" do visit root_path click_link 'Crop Wrangling' - within '.table' do + within '#recently-added-crops' do expect(page).to have_content "#{crops.first.creator.login_name}" end end @@ -48,6 +50,24 @@ feature "crop wranglers" do expect(page).to have_content 'Crop was successfully created' expect(page).to have_content 'planticus maximus' end + + scenario "View pending crops" do + visit wrangle_crops_path(:approval_status => "pending") + within "#pending-crops" do + click_link "Ultra berry" + end + expect(page).to have_content "This crop is currently pending approval." + expect(page).to have_content "Please approve this even though it's fake." + end + + scenario "View rejected crops" do + visit wrangle_crops_path(:approval_status => "rejected") + within "#rejected-crops" do + click_link "Fail bean" + end + expect(page).to have_content "This crop was rejected for the following reason: Totally fake" + end + end diff --git a/spec/features/crops/crop_wrangling_button_spec.rb b/spec/features/crops/crop_wrangling_button_spec.rb new file mode 100644 index 000000000..26ef46e30 --- /dev/null +++ b/spec/features/crops/crop_wrangling_button_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +feature "crop wrangling button" do + + let(:crop_wrangler) { FactoryGirl.create(:crop_wrangling_member) } + + context "crop wrangling button" do + + background do + login_as(crop_wrangler) + visit crops_path + end + + scenario "has a link to crop wrangling page" do + expect(page).to have_link "Wrangle Crops", :href => wrangle_crops_path + end + + end + + let(:member) { FactoryGirl.create(:member) } + + context "crop wrangling button" do + + background do + login_as(member) + visit crops_path + end + + scenario "has no link to crop wrangling page" do + expect(page).to have_no_link "Wrangle Crops", :href => wrangle_crops_path + end + end + end diff --git a/spec/features/crops/request_new_crop_spec.rb b/spec/features/crops/request_new_crop_spec.rb new file mode 100644 index 000000000..0a1cac2a7 --- /dev/null +++ b/spec/features/crops/request_new_crop_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +feature "Requesting a new crop" do + + context "As a regular member" do + + let(:member) { FactoryGirl.create(:member) } + let!(:wrangler) { FactoryGirl.create(:crop_wrangling_member) } + + before { login_as member } + + scenario "Submit request" do + visit new_crop_path + fill_in "Name", with: "Couch potato" + fill_in "Comments", with: "Couch potatoes are real for real." + click_button "Save" + expect(page).to have_content "Crop was successfully requested." + end + + end + + context "As a crop wrangler" do + + let(:wrangler) { FactoryGirl.create(:crop_wrangling_member) } + let!(:crop) { FactoryGirl.create(:crop_request) } + let!(:already_approved) { FactoryGirl.create(:crop) } + + before { login_as wrangler } + + scenario "Approve a request" do + visit edit_crop_path(crop) + select "approved", from: "Approval Status" + click_button "Save" + expect(page).to have_content "En wikipedia url is not a valid English Wikipedia URL" + fill_in "Wikipedia URL", with: "http://en.wikipedia.org/wiki/Aung_San_Suu_Kyi" + click_button "Save" + expect(page).to have_content "Crop was successfully updated." + end + + scenario "Rejecting a crop" do + visit edit_crop_path(crop) + select "rejected", from: "Approval Status" + select "not edible", from: "Reason for rejection" + click_button "Save" + expect(page).to have_content "Crop was successfully updated." + end + + end + +end \ No newline at end of file diff --git a/spec/features/footer_spec.rb b/spec/features/footer_spec.rb index 749d948ba..400b153a4 100644 --- a/spec/features/footer_spec.rb +++ b/spec/features/footer_spec.rb @@ -2,19 +2,11 @@ require 'rails_helper' feature "footer" do - scenario "has three columns" do + scenario "footer is on home page" do visit root_path - expect(page).to have_css 'footer #about-growstuff' - expect(page).to have_css 'footer #policies' - expect(page).to have_css 'footer #contact' + expect(page).to have_css 'footer' end # NB: not testing specific content in the footer since I'm going to put them # in the CMS and they'll be variable. - - scenario "contact page has Twitter link" do - visit root_path - click_link 'Contact' - page.should have_link '@growstufforg', :href => 'http://twitter.com/growstufforg' - end end diff --git a/spec/features/members_list_spec.rb b/spec/features/members_list_spec.rb new file mode 100644 index 000000000..68cbb6d1f --- /dev/null +++ b/spec/features/members_list_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +feature "members list" do + + context "list all members" do + let! (:member1) { FactoryGirl.create(:member, :login_name => "Archaeopteryx", :confirmed_at => Time.zone.parse('2013-02-10')) } + let! (:member2) { FactoryGirl.create(:member, :login_name => "Zephyrosaurus", :confirmed_at => Time.zone.parse('2014-01-11')) } + let! (:member3) { FactoryGirl.create(:member, :login_name => "Testingname", :confirmed_at => Time.zone.parse('2014-05-09')) } + + scenario "default alphabetical sort" do + visit members_path + expect(page).to have_css "#sort" + expect(page).to have_selector "form" + click_button('Show') + all_links = page.all("#maincontainer p") + expect(all_links.first).to have_text member1.login_name + expect(all_links.last).to have_text member2.login_name + end + + scenario "recently joined sort" do + visit members_path + expect(page).to have_css "#sort" + expect(page).to have_selector "form" + select("recently", :from => 'sort') + click_button('Show') + all_links = page.all("#maincontainer p") + expect(all_links.first).to have_text member3.login_name + expect(all_links.last).to have_text member1.login_name + + end + + + end + +end diff --git a/spec/features/planting_reminder_spec.rb b/spec/features/planting_reminder_spec.rb index 3b8c88bf8..3d694c013 100644 --- a/spec/features/planting_reminder_spec.rb +++ b/spec/features/planting_reminder_spec.rb @@ -5,6 +5,12 @@ feature "Planting reminder email", :js => true do let(:member) { FactoryGirl.create(:member) } let(:mail) { Notifier.planting_reminder(member) } + # Unfortunately, we can't use the default url options for ActionMailer as configured in + # test.rb, since this isn't a mailer spec. + def self.default_url_options + { host: 'localhost', port: 8080 } + end + scenario "has a greeting" do expect(mail).to have_content "Hello" end @@ -34,10 +40,8 @@ feature "Planting reminder email", :js => true do scenario "lists plantings" do expect(mail).to have_content "most recent plantings you've told us about" - 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_link @p1.to_s, planting_url(@p1) + expect(mail).to have_link @p2.to_s, planting_url(@p2) expect(mail).to have_content "keep your garden records up to date" end end @@ -65,10 +69,8 @@ feature "Planting reminder email", :js => true do 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_link @h1.to_s, harvest_url(@h1) + expect(mail).to have_link @h2.to_s, harvest_url(@h2) expect(mail).to have_content "Harvested anything else lately?" end diff --git a/spec/features/seeds/adding_seeds_spec.rb b/spec/features/seeds/adding_seeds_spec.rb index 8cdd9c847..b14622d47 100644 --- a/spec/features/seeds/adding_seeds_spec.rb +++ b/spec/features/seeds/adding_seeds_spec.rb @@ -18,12 +18,23 @@ feature "Seeds", :js => true do within "form#new_seed" do fill_in "Quantity:", :with => 42 fill_in "Plant before:", :with => "2014-06-15" + fill_in "Days until maturity:", :with => 999 + fill_in "to", :with => 1999 + select "certified organic", :from => "Organic?" + select "non-certified GMO-free", :from => "GMO?" + select "heirloom", :from => "Heirloom?" fill_in "Description", :with => "It's killer." select "internationally", :from => "Will trade:" click_button "Save" end expect(page).to have_content "Successfully added maize seed to your stash" + expect(page).to have_content "Quantity: 42" + expect(page).to have_content "Days until maturity: 999–1999" + expect(page).to have_content "certified organic" + expect(page).to have_content "non-certified GMO-free" + expect(page).to have_content "Heirloom? heirloom" + expect(page).to have_content "It's killer." end scenario "Adding a seed from crop page" do diff --git a/spec/features/seeds/misc_seeds_spec.rb b/spec/features/seeds/misc_seeds_spec.rb index 11f1d6c09..1e434a754 100644 --- a/spec/features/seeds/misc_seeds_spec.rb +++ b/spec/features/seeds/misc_seeds_spec.rb @@ -45,5 +45,30 @@ feature "seeds" do click_link 'Delete' current_path.should eq seeds_path end + + scenario "view seeds with max and min days until maturity" do + seed = FactoryGirl.create(:seed, :days_until_maturity_min => 5, :days_until_maturity_max => 7) + visit seed_path(seed) + expect(page).to have_content "Days until maturity: 5–7" + end + + scenario "view seeds with only max days until maturity" do + seed = FactoryGirl.create(:seed, :days_until_maturity_max => 7) + visit seed_path(seed) + expect(page).to have_content "Days until maturity: 7" + end + + scenario "view seeds with only min days until maturity" do + seed = FactoryGirl.create(:seed, :days_until_maturity_min => 5) + visit seed_path(seed) + expect(page).to have_content "Days until maturity: 5" + end + + scenario "view seeds with neither max nor min days until maturity" do + seed = FactoryGirl.create(:seed) + visit seed_path(seed) + expect(page).to have_content "Days until maturity: unknown" + end + end end diff --git a/spec/mailers/notifier_spec.rb b/spec/mailers/notifier_spec.rb index 4c7ac59a3..ce552f99b 100644 --- a/spec/mailers/notifier_spec.rb +++ b/spec/mailers/notifier_spec.rb @@ -44,4 +44,84 @@ describe Notifier do end end + + + describe "new crop request" do + let(:member) { FactoryGirl.create(:crop_wrangling_member) } + let(:crop) { FactoryGirl.create(:crop_request) } + let(:mail) { Notifier.new_crop_request(member, crop) } + + it 'sets the subject correctly' do + mail.subject.should == "#{crop.requester.login_name} has requested Ultra berry as a new crop" + 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 requested crop URL' do + mail.body.encoded.should match crop_url(crop) + end + end + + describe "crop approved" do + let(:member) { FactoryGirl.create(:member) } + let(:crop) { FactoryGirl.create(:crop) } + let(:mail) { Notifier.crop_request_approved(member, crop) } + + it 'sets the subject correctly' do + expect(mail.subject).to eq "Magic bean has been approved" + end + + it 'comes from noreply@growstuff.org' do + expect(mail.from).to eq ['noreply@growstuff.org'] + end + + it 'sends the mail to the recipient of the notification' do + expect(mail.to).to eq [member.email] + end + + it 'includes the approved crop URL' do + expect(mail.body.encoded).to match crop_url(crop) + end + + it 'includes links to plant, harvest and stash seeds for the new crop' do + expect(mail.body.encoded).to match "#{new_planting_url}\\?crop_id=#{crop.id}" + expect(mail.body.encoded).to match "#{new_harvest_url}\\?crop_id=#{crop.id}" + expect(mail.body.encoded).to match "#{new_seed_url}\\?crop_id=#{crop.id}" + end + + end + + describe "crop rejected" do + let(:member) { FactoryGirl.create(:member) } + let(:crop) { FactoryGirl.create(:rejected_crop) } + let(:mail) { Notifier.crop_request_rejected(member, crop) } + + it 'sets the subject correctly' do + expect(mail.subject).to eq "Fail bean has been rejected" + end + + it 'comes from noreply@growstuff.org' do + expect(mail.from).to eq ['noreply@growstuff.org'] + end + + it 'sends the mail to the recipient of the notification' do + expect(mail.to).to eq [member.email] + end + + it 'includes the rejected crop URL' do + expect(mail.body.encoded).to match crop_url(crop) + end + + it 'includes the reason for rejection' do + expect(mail.body.encoded).to match "Totally fake" + end + end + + end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 4eccf8916..87d8a3b1d 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -45,11 +45,14 @@ describe Ability do context "standard member" do it "can't manage crops" do - @ability.should_not be_able_to(:create, Crop) @ability.should_not be_able_to(:update, @crop) @ability.should_not be_able_to(:destroy, @crop) end + it "can request crops" do + @ability.should be_able_to(:create, Crop) + end + it "can read crops" do @ability.should be_able_to(:read, @crop) end diff --git a/spec/models/seed_spec.rb b/spec/models/seed_spec.rb index cfd73bf04..747dbee71 100644 --- a/spec/models/seed_spec.rb +++ b/spec/models/seed_spec.rb @@ -88,6 +88,49 @@ describe Seed do end end + context 'organic, gmo, heirloom' do + it 'all valid organic values should work' do + ['certified organic', 'non-certified organic', + 'conventional/non-organic', 'unknown'].each do |t| + @seed = FactoryGirl.build(:seed, :organic => t) + @seed.should be_valid + end + end + + it 'all valid GMO values should work' do + ['certified GMO-free', 'non-certified GMO-free', + 'GMO', 'unknown'].each do |t| + @seed = FactoryGirl.build(:seed, :gmo => t) + @seed.should be_valid + end + end + + it 'all valid heirloom values should work' do + %w(heirloom hybrid unknown).each do |t| + @seed = FactoryGirl.build(:seed, :heirloom => t) + @seed.should be_valid + end + end + + it 'should refuse invalid organic/GMO/heirloom values' do + [:organic, :gmo, :heirloom].each do |field| + @seed = FactoryGirl.build(:seed, field => 'not valid') + @seed.should_not be_valid + @seed.errors[field].should_not be_empty + end + end + + it 'should not allow nil or blank values' do + [:organic, :gmo, :heirloom].each do |field| + @seed = FactoryGirl.build(:seed, field => nil) + @seed.should_not be_valid + @seed = FactoryGirl.build(:seed, field => '') + @seed.should_not be_valid + end + end + end + + context 'interesting' do it 'lists interesting seeds' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9618ca895..784cfba6f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -25,7 +25,7 @@ require 'capybara' require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist Capybara.app_host = 'http://localhost' -Capybara.server_port = 8080 +Capybara.server_port = 8081 include Warden::Test::Helpers diff --git a/spec/views/crops/index.html.haml_spec.rb b/spec/views/crops/index.html.haml_spec.rb index 69792ba76..33ab3e07e 100644 --- a/spec/views/crops/index.html.haml_spec.rb +++ b/spec/views/crops/index.html.haml_spec.rb @@ -8,24 +8,11 @@ describe "crops/index" do total_entries = 2 @tomato = FactoryGirl.create(:tomato) @maize = FactoryGirl.create(:maize) - crops = WillPaginate::Collection.create(page, per_page, total_entries) do |pager| + assign(:crops, [@tomato, @maize]) + paginated_crops = WillPaginate::Collection.create(page, per_page, total_entries) do |pager| pager.replace([ @tomato, @maize ]) end - assign(:crops, crops) - end - - it "has a form for sorting by" do - render - assert_select "form" - assert_select "select#sort" - assert_select "option[value=alpha]" - assert_select "option[value=popular]" - end - - it "renders a list of crops" do - render - assert_select "a", :text => @maize.name - assert_select "a", :text => @tomato.name + assign(:paginated_crops, paginated_crops) end it "shows photos where available" do diff --git a/spec/views/crops/search.html.haml_spec.rb b/spec/views/crops/search.html.haml_spec.rb deleted file mode 100644 index 8458f254e..000000000 --- a/spec/views/crops/search.html.haml_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'rails_helper' - -describe "crops/search" do - before(:each) do - controller.stub(:current_user) { nil } - end - - context "has results" do - - before :each do - @tomato = FactoryGirl.create(:tomato) - @roma = FactoryGirl.create(:crop, :name => 'Roma tomato', :parent => @tomato) - assign(:search, 'tomato') - assign(:all_matches, [@tomato, @roma]) - render - end - - it "shows exact matches" do - assert_select "div#all_matches" do - assert_select "a[href=#{crop_path(@tomato)}]" - end - end - - it "shows partial matches" do - assert_select "div#all_matches" do - assert_select "a[href=#{crop_path(@roma)}]" - end - end - end - - context "no results" do - before :each do - assign(:all_matches, []) - assign(:search, 'tomato') - render - end - - it "tells you there are no matches" do - rendered.should have_content "No results found" - end - - it "links to browse crops" do - assert_select "a", :href => crops_path - rendered.should have_content "Try browsing our crop database instead" - end - end - -end