diff --git a/Gemfile b/Gemfile index c16f8fcf9..cfa8ef248 100644 --- a/Gemfile +++ b/Gemfile @@ -106,7 +106,7 @@ gem 'rake', '>= 10.0.0' gem "responders" # allows soft delete. Used for members. -gem "paranoia", "~> 2.2" +gem 'discard', '~> 1.0' gem 'xmlrpc' # fixes rake error - can be removed if not needed later @@ -115,6 +115,9 @@ gem 'puma' gem 'loofah', '>= 2.2.1' gem 'rack-protection', '>= 2.0.1' +# Member to member messaging system +gem 'mailboxer' + group :production do gem 'bonsai-elasticsearch-rails' # Integration with Bonsa-Elasticsearch on heroku gem 'dalli' @@ -132,25 +135,28 @@ group :development do end group :development, :test do - gem 'bullet' # performance tuning by finding unnecesary queries - gem 'byebug' # debugging - gem 'capybara' # integration tests - gem 'capybara-email' # integration tests for email - gem 'capybara-screenshot' # for test debugging - gem 'coveralls', require: false # coverage analysis + gem 'bullet' # performance tuning by finding unnecesary queries + gem 'byebug' # debugging + gem 'capybara' # integration tests + gem 'capybara-email' # integration tests for email + gem 'capybara-screenshot' # for test debugging gem 'database_cleaner' - gem 'factory_bot_rails' # for creating test data + gem 'factory_bot_rails' # for creating test data gem 'faker' - gem 'haml-i18n-extractor' - gem 'haml-rails' # HTML templating language - gem 'haml_lint', '>= 0.25.1' # Checks haml files for goodness - gem 'i18n-tasks' # adds tests for finding missing and unused translations + gem 'haml-rails' # HTML templating language gem 'rspec-activemodel-mocks' - gem 'rspec-rails' # unit testing framework - gem 'rubocop', '~> 0.71' + gem 'rspec-rails' # unit testing framework gem 'rubocop-rails' gem 'rubocop-rspec' - gem 'webrat' # provides HTML matchers for view tests + gem 'webrat' # provides HTML matchers for view tests + + # cli utils + gem 'coveralls', require: false # coverage analysis + gem 'haml-i18n-extractor', require: false + gem 'haml_lint', '>= 0.25.1', require: false # Checks haml files for goodness + gem 'i18n-tasks', require: false # adds tests for finding missing and unused translations + gem 'rspectre', require: false # finds unused code in specs + gem 'rubocop', '~> 0.71', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 04f8c0570..9cc261184 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ GEM remote: https://rubygems.org/ remote: https://rails-assets.org/ specs: + abstract_type (0.0.7) actioncable (5.2.2.1) actionpack (= 5.2.2.1) nio4r (~> 2.0) @@ -53,8 +54,15 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + adamantium (0.2.0) + ice_nine (~> 0.11.0) + memoizable (~> 0.4.0) addressable (2.6.0) public_suffix (>= 2.0.2, < 4.0) + anima (0.3.1) + abstract_type (~> 0.0.7) + adamantium (~> 0.2) + equalizer (~> 0.0.11) arel (9.0.0) ast (2.4.0) autoprefixer-rails (9.6.0) @@ -100,6 +108,10 @@ GEM capybara-screenshot (1.0.23) capybara (>= 1.0, < 4) launchy + carrierwave (1.3.1) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) + mime-types (>= 1.16) chartkick (3.2.1) childprocess (1.0.1) rake (< 13.0) @@ -126,6 +138,9 @@ GEM sassc-rails (>= 2.0.0) comfy_bootstrap_form (4.0.6) rails (>= 5.0.0) + concord (0.1.5) + adamantium (~> 0.2.0) + equalizer (~> 0.0.9) concurrent-ruby (1.1.5) connection_pool (2.2.2) coveralls (0.8.19) @@ -139,13 +154,15 @@ GEM activesupport (>= 3.0.0) dalli (2.7.10) database_cleaner (1.7.0) - devise (4.6.2) + devise (4.7.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) + railties (>= 4.1.0) responders warden (~> 1.2.3) diff-lcs (1.3) + discard (1.1.0) + activerecord (>= 4.2, < 7) docile (1.1.5) elasticsearch (6.8.0) elasticsearch-api (= 6.8.0) @@ -160,6 +177,7 @@ GEM elasticsearch-transport (6.8.0) faraday multi_json + equalizer (0.0.11) erubi (1.8.0) erubis (2.7.0) excon (0.64.0) @@ -205,7 +223,7 @@ GEM haml (>= 4.0.6, < 6.0) html2haml (>= 1.0.1) railties (>= 5.1) - haml_lint (0.32.0) + haml_lint (0.33.0) haml (>= 4.0, < 5.2) rainbow rake (>= 10, < 13) @@ -238,6 +256,7 @@ GEM rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) + ice_nine (0.11.2) jaro_winkler (1.5.3) jquery-rails (4.3.5) rails-dom-testing (>= 1, < 3) @@ -283,6 +302,9 @@ GEM nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) + mailboxer (0.15.1) + carrierwave (>= 0.5.8) + rails (>= 5.0.0) marcel (0.3.3) mimemagic (~> 0.3.2) material-sass (4.1.1) @@ -291,6 +313,8 @@ GEM material_icons (2.2.1) railties (>= 3.2) memcachier (0.0.2) + memoizable (0.4.2) + thread_safe (~> 0.3, >= 0.3.1) method_source (0.9.2) mime-types (3.2.2) mime-types-data (~> 3.2015) @@ -315,7 +339,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oj (3.8.1) + oj (3.9.0) omniauth (1.9.0) hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) @@ -335,8 +359,6 @@ GEM rack orm_adapter (0.5.0) parallel (1.17.0) - paranoia (2.4.2) - activerecord (>= 4.0, < 6.1) parser (2.6.3.0) ast (~> 2.4.0) percy-capybara (4.0.2) @@ -345,6 +367,7 @@ GEM heroics (~> 0.0.25) moneta (~> 1.0.0) popper_js (1.14.5) + procto (0.0.3) public_suffix (3.1.1) puma (4.1.0) nio4r (~> 2.0) @@ -376,7 +399,7 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.1.0) + rails-html-sanitizer (1.2.0) loofah (~> 2.2, >= 2.2.2) rails-i18n (5.1.3) i18n (>= 0.7, < 2) @@ -403,6 +426,10 @@ GEM responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) rspec-activemodel-mocks (1.1.0) activemodel (>= 3.0) activesupport (>= 3.0) @@ -424,6 +451,12 @@ GEM rspec-mocks (~> 3.8.0) rspec-support (~> 3.8.0) rspec-support (3.8.2) + rspectre (0.0.1) + anima (~> 0.3) + concord (~> 0.1) + parser (~> 2.3) + rspec (~> 3.0) + unparser (~> 0.2) rubocop (0.74.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) @@ -447,8 +480,8 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.7) - railties (>= 4.0.0, < 6) + sass-rails (5.1.0) + railties (>= 5.2.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) @@ -509,6 +542,14 @@ GEM kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.12.1) + unparser (0.4.5) + abstract_type (~> 0.0.7) + adamantium (~> 0.2.0) + concord (~> 0.1.5) + diff-lcs (~> 1.3) + equalizer (~> 0.0.9) + parser (~> 2.6.3) + procto (~> 0.0.2) warden (1.2.8) rack (>= 2.0.6) webdrivers (4.1.2) @@ -522,7 +563,7 @@ GEM websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.4) - will_paginate (3.1.7) + will_paginate (3.1.8) will_paginate-bootstrap4 (0.2.2) will_paginate (~> 3.0, >= 3.0.0) xmlrpc (0.3.0) @@ -559,6 +600,7 @@ DEPENDENCIES dalli database_cleaner devise + discard (~> 1.0) elasticsearch (< 7.0.0) factory_bot_rails faker @@ -584,6 +626,7 @@ DEPENDENCIES letter_opener listen loofah (>= 2.2.1) + mailboxer material-sass (= 4.1.1) material_icons memcachier @@ -593,7 +636,6 @@ DEPENDENCIES omniauth-facebook omniauth-flickr (>= 0.0.15) omniauth-twitter - paranoia (~> 2.2) percy-capybara (~> 4.0.0) pg (< 1.0.0) platform-api @@ -607,6 +649,7 @@ DEPENDENCIES responders rspec-activemodel-mocks rspec-rails + rspectre rubocop (~> 0.71) rubocop-rails rubocop-rspec diff --git a/app/assets/stylesheets/_members.scss b/app/assets/stylesheets/_members.scss index aef8571ed..758aff496 100644 --- a/app/assets/stylesheets/_members.scss +++ b/app/assets/stylesheets/_members.scss @@ -15,3 +15,26 @@ padding-left: 1em; width: 15em; } + + +.member-chip { + background-color: lighten($green, 30%); + border-radius: 25px; + display: inline-block; + font-size: 1em; + height: 30px; + line-height: 30px; + padding: 0 25px; + + a { + color: $black; + } + + img { + border-radius: 50%; + float: left; + height: 30px; + margin: 0 10px 0 -25px; + width: 30px; + } +} diff --git a/app/controllers/admin/members_controller.rb b/app/controllers/admin/members_controller.rb index ca35129b3..483a3b699 100644 --- a/app/controllers/admin/members_controller.rb +++ b/app/controllers/admin/members_controller.rb @@ -3,17 +3,23 @@ module Admin before_action :auth! def index - @members = Member.order(:login_name).paginate(page: params[:page]) + @members = Member.all + @members = @members.where("login_name ILIKE ?", "%#{search_term}%") unless search_term.nil? + @members = @members.order(:login_name).paginate(page: params[:page]) end def destroy @member = Member.find_by!(slug: params[:slug]) - @member.destroy + @member.discard redirect_to admin_members_path end private + def search_term + params[:q] + end + def auth! authorize! :manage, :all end diff --git a/app/controllers/conversations_controller.rb b/app/controllers/conversations_controller.rb new file mode 100644 index 000000000..67cf92211 --- /dev/null +++ b/app/controllers/conversations_controller.rb @@ -0,0 +1,62 @@ +class ConversationsController < ApplicationController + respond_to :html + before_action :authenticate_member! + before_action :set_box + before_action :check_current_subject_in_conversation, only: %i(show update destroy) + + def index + @conversations = if @box.eql? "inbox" + mailbox.inbox + elsif @box.eql? "sent" + mailbox.sentbox + else + mailbox.trash + end.paginate(page: params[:page]) + + respond_with @conversations + end + + def show + @receipts = mailbox.receipts_for(@conversation) + @receipts.mark_as_read + @participants = @conversation.participants + end + + def update + @conversation.untrash(current_member) + redirect_to conversations_path(box: params[:box]) + end + + def destroy + @conversation = Mailboxer::Conversation.find(params[:id]) + @conversation.move_to_trash(current_member) + redirect_to conversations_path(box: params[:box]) + end + + private + + def mailbox + current_member.mailbox + end + + def set_box + @boxes = { + 'inbox' => { 'total' => mailbox.inbox.size, 'unread' => current_member.receipts.where(is_read: false).count }, + 'sent' => { 'total' => mailbox.sentbox.size, 'unread' => 0 }, + 'trash' => { 'total' => mailbox.trash.size, 'unread' => 0 } + } + @box = if params[:box].blank? || !@boxes.keys.include?(params[:box]) + 'inbox' + else + params[:box] + end + end + + def check_current_subject_in_conversation + @conversation = Mailboxer::Conversation.find_by(id: params[:id]) + return unless @conversation.nil? || !@conversation.is_participant?(current_member) + + redirect_to conversations_path(box: box) + nil + end +end diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 89e45a163..8a02e195c 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -139,7 +139,7 @@ class CropsController < ApplicationController def notify_wranglers return if current_member.role? :crop_wrangler - Role.crop_wranglers.each do |w| + Role.crop_wranglers&.each do |w| Notifier.new_crop_request(w, @crop).deliver_now! end end diff --git a/app/controllers/gardens_controller.rb b/app/controllers/gardens_controller.rb index 791c6d7b4..f9037923a 100644 --- a/app/controllers/gardens_controller.rb +++ b/app/controllers/gardens_controller.rb @@ -14,7 +14,6 @@ class GardensController < ApplicationController @gardens = @gardens.active unless @show_all @gardens = @gardens.where(owner: @owner) if @owner.present? @gardens = @gardens.joins(:owner).order(:name).paginate(page: params[:page]) - @gardens = @gardens.includes(:owner, plantings: [:owner, crop: :parent]) respond_with(@gardens) end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index d044836ed..552a71bd0 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -13,7 +13,7 @@ class MembersController < ApplicationController end def show - @member = Member.confirmed.find_by!(slug: params[:slug]) + @member = Member.confirmed.kept.find_by!(slug: params[:slug]) @twitter_auth = @member.auth('twitter') @flickr_auth = @member.auth('flickr') @facebook_auth = @member.auth('facebook') @@ -89,6 +89,6 @@ class MembersController < ApplicationController Member.recently_joined else Member.order(:login_name) - end.confirmed.paginate(page: params[:page]) + end.kept.confirmed.paginate(page: params[:page]) end end diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb new file mode 100644 index 000000000..21bb4d0b2 --- /dev/null +++ b/app/controllers/messages_controller.rb @@ -0,0 +1,38 @@ +class MessagesController < ApplicationController + respond_to :html, :json + before_action :authenticate_member! + def index + redirect_to conversations_path(box: @box) + end + + def show + if (@message = Message.find_by(id: params[:id])) && (@conversation = @message.conversation) + if @conversation.is_participant?(current_member) + redirect_to conversation_path(@conversation, box: @box, anchor: "message_" + @message.id.to_s) + return + end + end + redirect_to conversations_path(box: @box) + end + + def new + return if params[:recipient_id].blank? + + @recipient = Member.find_by(id: params[:recipient_id]) + return if @recipient.nil? + end + + def create + if params[:conversation_id].present? + @conversation = Mailboxer::Conversation.find(params[:conversation_id]) + current_member.reply_to_conversation(@conversation, params[:body]) + redirect_to conversation_path(@conversation) + else + recipient = Member.find(params[:recipient_id]) + body = params[:body] + subject = params[:subject] + @conversation = current_member.send_message(recipient, body, subject) + redirect_to conversations_path(box: 'sentbox') + end + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb deleted file mode 100644 index 9b93b7324..000000000 --- a/app/controllers/notifications_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -class NotificationsController < ApplicationController - include NotificationsHelper - before_action :authenticate_member! - load_and_authorize_resource - respond_to :html - - # GET /notifications - def index - @notifications = Notification.by_recipient(current_member).order(created_at: :desc).paginate(page: params[:page], per_page: 30) - end - - # GET /notifications/1 - def show - @notification.read = true - @notification.save - @reply_link = reply_link(@notification) - end - - # GET /notifications/new - - def new - @notification = Notification.new - @recipient = Member.find_by(id: params[:recipient_id]) - @subject = params[:subject] || "" - end - - # GET /notifications/1/reply - def reply - @notification = Notification.new - @sender_notification = Notification.find_by!(id: params[:notification_id], recipient: current_member) - @sender_notification.read = true - @sender_notification.save - @recipient = @sender_notification.sender - @subject = if @sender_notification.subject.start_with? 'Re: ' - @sender_notification.subject - else - "Re: #{@sender_notification.subject}" - end - end - - # DELETE /notifications/1 - def destroy - @notification.destroy - redirect_to notifications_url - end - - # POST /notifications - def create - params[:notification][:sender_id] = current_member.id - @notification = Notification.new(notification_params) - @recipient = Member.find_by(id: params[:notification][:recipient_id]) - - if @notification.save - redirect_to notifications_path, notice: 'Message was successfully sent.' - else - render action: "new" - end - end - - private - - def notification_params - params.require(:notification).permit(:sender_id, :recipient_id, :subject, :body, :post_id, :read) - end -end diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index d6d3a164e..1bbbefc93 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -31,12 +31,7 @@ class PlantingsController < ApplicationController end def show - @planting = Planting.includes(:owner, :crop, :garden, :photos) - .friendly - .find(params[:id]) - @photos = @planting.photos - .includes(:owner) - .order(date_taken: :desc) + @photos = @planting.photos.includes(:owner).order(date_taken: :desc) respond_with @planting end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b9ed73dfe..24a8ee8e8 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -37,9 +37,11 @@ class RegistrationsController < Devise::RegistrationsController end def destroy - if @member.destroy_with_password(params.require(:member)[:current_password]) + if @member.valid_password?(params.require(:member)[:current_password]) + @member.discard redirect_to root_path else + @member.errors.add(:current_password, 'Incorrect password') render "edit" end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb deleted file mode 100644 index 2e551b3cb..000000000 --- a/app/helpers/notifications_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module NotificationsHelper - def reply_link(notification) - if notification.post - # comment on the post in question - new_comment_url(post_id: notification.post.id) - else - # by default, reply link sends a PM in return - notification_reply_url(notification) - end - end -end diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index 0386450f1..e39ced825 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -1,5 +1,5 @@ class Notifier < ApplicationMailer - include NotificationsHelper + # include NotificationsHelper default from: "Growstuff <#{ENV['GROWSTUFF_EMAIL']}>" def verifier diff --git a/app/models/comment.rb b/app/models/comment.rb index 3ff8eef4f..9b0882350 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,5 @@ class Comment < ApplicationRecord - belongs_to :author, -> { with_deleted }, class_name: 'Member', inverse_of: :comments + belongs_to :author, class_name: 'Member', inverse_of: :comments belongs_to :post scope :post_order, -> { reorder("created_at ASC") } # for display on post page diff --git a/app/models/concerns/member_flickr.rb b/app/models/concerns/member_flickr.rb new file mode 100644 index 000000000..2f1e22eec --- /dev/null +++ b/app/models/concerns/member_flickr.rb @@ -0,0 +1,51 @@ +module MemberFlickr + extend ActiveSupport::Concern + + included do # rubocop:disable Metrics/BlockLength + # Authenticates against Flickr and returns an object we can use for subsequent api calls + def flickr + if @flickr.nil? + flickr_auth = auth('flickr') + if flickr_auth + FlickRaw.api_key = ENV['GROWSTUFF_FLICKR_KEY'] + FlickRaw.shared_secret = ENV['GROWSTUFF_FLICKR_SECRET'] + @flickr = FlickRaw::Flickr.new + @flickr.access_token = flickr_auth.token + @flickr.access_secret = flickr_auth.secret + end + end + @flickr + end + + # Fetches a collection of photos from Flickr + # Returns a [[page of photos], total] pair. + # Total is needed for pagination. + def flickr_photos(page_num = 1, set = nil) + result = if set + flickr.photosets.getPhotos( + photoset_id: set, + page: page_num, + per_page: 30 + ) + else + flickr.people.getPhotos( + user_id: 'me', + page: page_num, + per_page: 30 + ) + end + return [result.photo, result.total] if result + + [[], 0] + end + + # Returns a hash of Flickr photosets' ids and titles + def flickr_sets + sets = {} + flickr.photosets.getList.each do |p| + sets[p.title] = p.id + end + sets + end + end +end diff --git a/app/models/concerns/member_newsletter.rb b/app/models/concerns/member_newsletter.rb new file mode 100644 index 000000000..938ba2237 --- /dev/null +++ b/app/models/concerns/member_newsletter.rb @@ -0,0 +1,46 @@ +module MemberNewsletter + extend ActiveSupport::Concern + + included do # rubocop:disable Metrics/BlockLength + after_save :update_newsletter_subscription + before_destroy :newsletter_unsubscribe + + scope :wants_newsletter, -> { where(newsletter: true) } + + def update_newsletter_subscription + return unless will_save_change_to_attribute?(:confirmed) || will_save_change_to_attribute?(:newsletter) + + if newsletter + newsletter_subscribe if confirmed_just_now? || requested_newsletter_just_now? + elsif confirmed_at + newsletter_unsubscribe + end + end + + def confirmed_just_now? + will_save_change_to_attribute?(:confirmed_at) + end + + def requested_newsletter_just_now? + confirmed_at && will_save_change_to_attribute?(:newsletter) + end + + def newsletter_subscribe(gibbon = Gibbon::API.new, testing = false) + return true if Rails.env.test? && !testing + + gibbon.lists.subscribe( + id: Rails.application.config.newsletter_list_id, + email: { email: email }, + merge_vars: { login_name: login_name }, + double_optin: false # they already confirmed their email with us + ) + end + + def newsletter_unsubscribe(gibbon = Gibbon::API.new, testing = false) + return true if Rails.env.test? && !testing + + gibbon.lists.unsubscribe(id: Rails.application.config.newsletter_list_id, + email: { email: email }) + end + end +end diff --git a/app/models/concerns/ownable.rb b/app/models/concerns/ownable.rb index eea4c9ac7..9328743fd 100644 --- a/app/models/concerns/ownable.rb +++ b/app/models/concerns/ownable.rb @@ -4,5 +4,7 @@ module Ownable included do belongs_to :owner, class_name: 'Member', # rubocop:disable Rails/InverseOf foreign_key: 'owner_id', counter_cache: true + + default_scope { joins(:owner).merge(Member.kept) } # Ensures the owner still exists end end diff --git a/app/models/crop.rb b/app/models/crop.rb index 12eb01975..e0166744d 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -17,7 +17,7 @@ class Crop < ApplicationRecord has_many :harvests, dependent: :destroy has_many :photo_associations, dependent: :destroy has_many :photos, through: :photo_associations - has_many :plant_parts, -> { distinct.order("plant_parts.name") }, through: :harvests + has_many :plant_parts, -> { joins_members.distinct.order("plant_parts.name") }, through: :harvests belongs_to :creator, class_name: 'Member', optional: true, inverse_of: :created_crops belongs_to :requester, class_name: 'Member', optional: true, inverse_of: :requested_crops belongs_to :parent, class_name: 'Crop', optional: true, inverse_of: :varieties @@ -34,6 +34,7 @@ class Crop < ApplicationRecord scope :rejected, -> { where(approval_status: "rejected") } scope :interesting, -> { approved.has_photos } scope :has_photos, -> { includes(:photos).where.not(photos: { id: nil }) } + scope :joins_members, -> { joins("INNER JOIN members ON members.id = harvests.owner_id") } # Special scope to control if it's in the search index scope :search_import, -> { approved } diff --git a/app/models/garden.rb b/app/models/garden.rb index 8eb27cc78..0131242f9 100644 --- a/app/models/garden.rb +++ b/app/models/garden.rb @@ -16,7 +16,6 @@ class Garden < ApplicationRecord after_validation :empty_unwanted_geocodes after_save :mark_inactive_garden_plantings_as_finished - default_scope { joins(:owner) } # Ensures owner exists scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } scope :order_by_name, -> { order(Arel.sql("lower(name) asc")) } @@ -59,22 +58,6 @@ class Garden < ApplicationRecord "#{owner.login_name}-#{name}".downcase.tr(' ', '-') end - # featured plantings returns the most recent 4 plantings for a garden, - # choosing them so that no crop is repeated. - def featured_plantings - unique_plantings = [] - seen_crops = [] - - plantings.includes(:garden, :crop, :owner, :harvests).order(created_at: :desc).each do |p| - unless seen_crops.include?(p.crop) - unique_plantings.push(p) - seen_crops.push(p.crop) - end - end - - unique_plantings[0..3] - end - def to_s name end diff --git a/app/models/harvest.rb b/app/models/harvest.rb index 6dffbca6a..8a78ad4fc 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -39,7 +39,6 @@ class Harvest < ApplicationRecord ## ## Scopes - default_scope { joins(:owner) } # Ensures owner exists scope :interesting, -> { has_photos.one_per_owner } scope :recent, -> { order(created_at: :desc) } scope :one_per_owner, lambda { diff --git a/app/models/member.rb b/app/models/member.rb index 175ee4a6c..e809c5370 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,9 +1,11 @@ class Member < ApplicationRecord - acts_as_paranoid # implements soft deletion - before_destroy :newsletter_unsubscribe + include Discard::Model + acts_as_messageable # messages can be sent to this model include Geocodable - extend FriendlyId + include MemberFlickr + include MemberNewsletter + extend FriendlyId friendly_id :login_name, use: %i(slugged finders) # @@ -46,7 +48,6 @@ class Member < ApplicationRecord scope :located, -> { where.not(location: '').where.not(latitude: nil).where.not(longitude: nil) } scope :recently_signed_in, -> { reorder(updated_at: :desc) } scope :recently_joined, -> { reorder(confirmed_at: :desc) } - scope :wants_newsletter, -> { where(newsletter: true) } scope :interesting, -> { confirmed.located.recently_signed_in.has_plantings } scope :has_plantings, -> { joins(:plantings).group("members.id") } @@ -57,6 +58,11 @@ class Member < ApplicationRecord :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable, :timeoutable, :omniauthable + # discarded (deleted) member cannot log in + def active_for_authentication? + super && !discarded? + end + # set up geocoding geocoded_by :location @@ -86,7 +92,6 @@ class Member < ApplicationRecord # Triggers after_validation :geocode after_validation :empty_unwanted_geocodes - after_save :update_newsletter_subscription # Give each new member a default garden # we use find_or_create to avoid accidentally creating a second one, @@ -103,13 +108,17 @@ class Member < ApplicationRecord end def to_s - login_name + discarded? ? 'deleted' : login_name end def to_param slug end + def mailboxer_email(_messageable) + send_notification_email ? email : false + end + def role?(role_sym) roles.any? { |r| r.name.gsub(/\s+/, "_").underscore.to_sym == role_sym } end @@ -118,50 +127,8 @@ class Member < ApplicationRecord authentications.find_by(provider: provider) end - # Authenticates against Flickr and returns an object we can use for subsequent api calls - def flickr - if @flickr.nil? - flickr_auth = auth('flickr') - if flickr_auth - FlickRaw.api_key = ENV['GROWSTUFF_FLICKR_KEY'] - FlickRaw.shared_secret = ENV['GROWSTUFF_FLICKR_SECRET'] - @flickr = FlickRaw::Flickr.new - @flickr.access_token = flickr_auth.token - @flickr.access_secret = flickr_auth.secret - end - end - @flickr - end - - # Fetches a collection of photos from Flickr - # Returns a [[page of photos], total] pair. - # Total is needed for pagination. - def flickr_photos(page_num = 1, set = nil) - result = if set - flickr.photosets.getPhotos( - photoset_id: set, - page: page_num, - per_page: 30 - ) - else - flickr.people.getPhotos( - user_id: 'me', - page: page_num, - per_page: 30 - ) - end - return [result.photo, result.total] if result - - [[], 0] - end - - # Returns a hash of Flickr photosets' ids and titles - def flickr_sets - sets = {} - flickr.photosets.getList.each do |p| - sets[p.title] = p.id - end - sets + def unread_count + receipts.where(is_read: false).count end def self.login_name_or_email(login) @@ -181,42 +148,6 @@ class Member < ApplicationRecord nearby_members end - def update_newsletter_subscription - return unless will_save_change_to_attribute?(:confirmed) || will_save_change_to_attribute?(:newsletter) - - if newsletter - newsletter_subscribe if confirmed_just_now? || requested_newsletter_just_now? - elsif confirmed_at - newsletter_unsubscribe - end - end - - def confirmed_just_now? - will_save_change_to_attribute?(:confirmed_at) - end - - def requested_newsletter_just_now? - confirmed_at && will_save_change_to_attribute?(:newsletter) - end - - def newsletter_subscribe(gibbon = Gibbon::API.new, testing = false) - return true if Rails.env.test? && !testing - - gibbon.lists.subscribe( - id: Rails.application.config.newsletter_list_id, - email: { email: email }, - merge_vars: { login_name: login_name }, - double_optin: false # they already confirmed their email with us - ) - end - - def newsletter_unsubscribe(gibbon = Gibbon::API.new, testing = false) - return true if Rails.env.test? && !testing - - gibbon.lists.unsubscribe(id: Rails.application.config.newsletter_list_id, - email: { email: email }) - end - def already_following?(member) follows.exists?(followed_id: member.id) end diff --git a/app/models/notification.rb b/app/models/notification.rb index d4e8db676..466fe73be 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -9,17 +9,15 @@ class Notification < ApplicationRecord scope :by_recipient, ->(recipient) { where(recipient_id: recipient) } before_create :replace_blank_subject - after_create :send_email + after_create :send_message - def self.unread_count - unread.size + def send_message + sender.send_message(recipient, body, subject) if recipient.send_notification_email end + private + def replace_blank_subject self.subject = "(no subject)" if subject.nil? || subject =~ /^\s*$/ end - - def send_email - Notifier.notify(self).deliver_now! if recipient.send_notification_email - end end diff --git a/app/models/photo.rb b/app/models/photo.rb index de44ebbbd..8ed9c2f57 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -15,7 +15,6 @@ class Photo < ApplicationRecord source_type: type end - default_scope { joins(:owner) } # Ensures the owner still exists scope :by_crop, ->(crop) { joins(:photo_associations).where(photo_associations: { crop: crop }) } scope :by_model, lambda { |model_name| joins(:photo_associations).where(photo_associations: { photographable_type: model_name.to_s }) diff --git a/app/models/plant_part.rb b/app/models/plant_part.rb index 7af288706..6fd57bff5 100644 --- a/app/models/plant_part.rb +++ b/app/models/plant_part.rb @@ -3,10 +3,12 @@ class PlantPart < ApplicationRecord friendly_id :name, use: %i(slugged finders) has_many :harvests, dependent: :destroy - has_many :crops, -> { distinct }, through: :harvests + has_many :crops, -> { joins_members.distinct }, through: :harvests validates :name, presence: true, uniqueness: true + scope :joins_members, -> { joins("INNER JOIN members ON members.id = harvests.owner_id") } + def to_s name end diff --git a/app/models/planting.rb b/app/models/planting.rb index 67bdd281d..0555e4ac5 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -32,7 +32,6 @@ class Planting < ApplicationRecord ## ## Scopes - default_scope { joins(:owner) } # Ensures the owner still exists scope :active, -> { where('finished <> true').where('finished_at IS NULL OR finished_at < ?', Time.zone.now) } scope :interesting, -> { has_photos.one_per_owner.order(planted_at: :desc) } scope :recent, -> { order(created_at: :desc) } @@ -47,6 +46,7 @@ class Planting < ApplicationRecord delegate :name, :en_wikipedia_url, :default_scientific_name, :plantings_count, to: :crop, prefix: true + delegate :annual?, to: :crop ## ## Validations validates :garden, presence: true diff --git a/app/models/post.rb b/app/models/post.rb index 0427ada70..685ffee19 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -19,7 +19,7 @@ class Post < ApplicationRecord after_save :update_crops_posts_association after_create :send_notification - default_scope { joins(:author) } # Ensures the owner still exists + default_scope { joins(:author).merge(Member.kept) } # Ensures the owner still exists # # Validations diff --git a/app/models/seed.rb b/app/models/seed.rb index b335127c0..8e0544b52 100644 --- a/app/models/seed.rb +++ b/app/models/seed.rb @@ -48,7 +48,7 @@ class Seed < ApplicationRecord # # Scopes - default_scope { joins(:owner) } # Ensure owner exists + default_scope { joins(:owner).merge(Member.kept) } # Ensure owner exists scope :tradable, -> { where.not(tradable_to: 'nowhere') } scope :interesting, -> { tradable.has_location } scope :has_location, -> { joins(:owner).where.not("members.location": nil) } diff --git a/app/views/admin/index.html.haml b/app/views/admin/index.html.haml index 5586cfec3..a0a458c56 100644 --- a/app/views/admin/index.html.haml +++ b/app/views/admin/index.html.haml @@ -3,25 +3,41 @@ - content_for :breadcrumbs do %li.breadcrumb-item.active= link_to 'Admin', admin_path -%h2 Manage +%h1 Manage #{ENV['GROWSTUFF_SITE_NAME']} .row .col-md-4 - %h2 Site admin - - %ul#site_admin - %li= link_to "Roles", roles_path - %li= link_to "Forums", forums_path - %li= link_to "CMS", comfy_admin_cms_path - %li= link_to t('.garden_types'), garden_types_path + .card + .card-header + %h2 + = icon 'fas', 'tablet-alt' + Site admin + .card-body + %nav.nav#site_admin + %li= link_to "Roles", roles_path, class: 'nav-link' + %li= link_to "Forums", forums_path, class: 'nav-link' + %li= link_to "CMS", comfy_admin_cms_path, class: 'nav-link' + %li= link_to t('.garden_types'), garden_types_path, class: 'nav-link' .col-md-4 - %h2 Crop data admin - %ul - %li= link_to "Alternate names", alternate_names_path - %li= link_to "Scientific names", scientific_names_path + .card + .card-header + %h2 + = icon 'fas', 'seedling' + Crop data admin + .card-body + %nav.nav#crop_admin + %li= link_to "Crop Wrangling", wrangle_crops_path, class: 'nav-link' + %li= link_to "Alternate names", alternate_names_path, class: 'nav-link' + %li= link_to "Scientific names", scientific_names_path, class: 'nav-link' + .col-md-4 - %h2 Member admin - %ul - %li= link_to "Newsletter subscribers", admin_newsletter_path - %li= link_to "Members", admin_members_path + .card + .card-header + %h2 + = icon 'fas', 'user' + Member admin + .card-body + %nav.nav#member_admin + %li= link_to "Newsletter subscribers", admin_newsletter_path, class: 'nav-link' + %li= link_to "Members", admin_members_path, class: 'nav-link' diff --git a/app/views/admin/members/index.html.haml b/app/views/admin/members/index.html.haml index 522c659c6..0a8d440ad 100644 --- a/app/views/admin/members/index.html.haml +++ b/app/views/admin/members/index.html.haml @@ -1,6 +1,12 @@ -.pagination - = page_entries_info @members - = will_paginate @members +.row + .col-md-6 + = bootstrap_form_tag url: admin_members_path, method: :get, layout: :horizontal do |f| + .input-group.md-form.form-sm + .input-group-prepend + %span.input-group-text= icon 'fas', 'user' + = f.text_field(:q, 'aria-label': 'search members', placeholder: 'search members', hide_label: true) + = f.submit 'search', class: 'btn btn-secondary' + %table.table.table-striped %thead @@ -11,10 +17,11 @@ - @members.each do |member| %tr %td= render 'members/tiny', member: member - %td - = link_to member, member + %td= member.login_name %td= member.email %td = link_to admin_member_path(member), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-light text-danger' do = icon 'fas', 'ban' Ban member += page_entries_info @members += will_paginate @members diff --git a/app/views/comments/_single.html.haml b/app/views/comments/_single.html.haml index b29502adf..30db5ad15 100644 --- a/app/views/comments/_single.html.haml +++ b/app/views/comments/_single.html.haml @@ -1,10 +1,11 @@ .card.comment .card-body .row - .col-md-1= render "members/tiny", member: comment.author - .col-md-11.border-left + .col + = render "members/tiny", member: comment.author - if can?(:edit, comment) || can?(:destroy, comment) - .dropdown.float-right + %hr/ + .dropdown %button#comment-edit-button.btn.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", type: "button"} Actions .dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "comment-edit-button"} - if can? :edit, comment @@ -16,9 +17,10 @@ data: { confirm: 'Are you sure?' }, class: 'dropdown-item text-danger' do = delete_icon Delete + .col-md-9.border-left .comment-meta.text-muted Posted by - - if comment.author.deleted? + - if comment.author.discarded? Member Deleted - else = link_to comment.author.login_name, member_path(comment.author) diff --git a/app/views/conversations/index.haml b/app/views/conversations/index.haml new file mode 100644 index 000000000..81f38c125 --- /dev/null +++ b/app/views/conversations/index.haml @@ -0,0 +1,57 @@ +- content_for :title, "Inbox" +- content_for :breadcrumbs do + %li.breadcrumb-item= link_to 'Conversations', conversations_path + %li.breadcrumb-item.active= link_to @box, conversations_path(box: @box) + +.row + .col-md-2 + .col-md-10 + %h1= @box + - unless @conversations.empty? + = will_paginate @conversations +.row + .col-md-2.list-group + - @boxes.each do |box_name, counts| + = link_to conversations_path(box: box_name), class: "nav-link list-group-item d-flex justify-content-between #{box_name == @box ? 'active' : ''} #{box_name}" do + - if counts['unread'].positive? + %span.badge.badge-info=counts['unread'] + - else + %span + %span + = icon 'fas', box_name + = box_name + + .col-md-10 + .list-group + - @conversations.each do |conversation| + .list-group-item + .row + .col-md-1 + - if conversation.receipts_for(current_member).last.is_unread? + %h1= icon 'far', 'envelope' + - else + %h1.text-muted= icon 'far', 'envelope-open' + .col-md-10 + .text-right.float-right + - conversation.recipients.each do |member| + - if member != current_member + = render 'members/tiny', member: member + = link_to conversation_path(conversation) do + .conversation + %h5.mb-2.h5 + - if conversation.receipts_for(current_member).last.is_unread? + %strong= conversation.subject + - else + = conversation.subject + %small + #{time_ago_in_words conversation.messages.last.created_at} ago + %span.text-muted= conversation.messages.last.created_at + = truncate(strip_tags(conversation.messages.last.body), length: 150, separator: ' ', omission: '... ') + .col-md-1 + - if @box == 'trash' + = link_to conversation_path(conversation, box: @box), method: :put, class: 'restore' do + = icon 'fas', 'trash-restore' + - else + = link_to delete_icon, conversation_path(conversation, box: @box), method: :delete, class: 'delete text-danger' + - unless @conversations.empty? + = will_paginate @conversations diff --git a/app/views/conversations/show.html.haml b/app/views/conversations/show.html.haml new file mode 100644 index 000000000..308686a71 --- /dev/null +++ b/app/views/conversations/show.html.haml @@ -0,0 +1,33 @@ +- content_for :breadcrumbs do + %li.breadcrumb-item= link_to 'Conversations', conversations_path + %li.breadcrumb-item.active= link_to @conversation.subject, conversation_path(@conversation) + +.row + .col-md-2 + .col-md-10 + %h1= @conversation.subject +.row + .col-md-2 + .card + .card-header + %h6 Participants + %ul.list-group.list-group-flush + - @participants.each do |member| + %li.list-group-item= render 'members/tiny', member: member + .col-md-10 + .card + %ul.list-group.list-group-flush + - @conversation.messages.order(:created_at).each do |message| + %li.list-group-item + .col-md-8.text-muted + = comment_icon + on #{message.created_at} + .col-md-4.text-right.float-right + = render 'members/tiny', member: message.sender + .col-12 + %p.text-justify + :growstuff_markdown + #{ strip_tags(message.body) } + %li.list-group-item + = icon 'fas', 'reply' + = render 'messages/form', conversation: @conversation diff --git a/app/views/crops/wrangle.html.haml b/app/views/crops/wrangle.html.haml index 1cb322645..caef0cdf7 100644 --- a/app/views/crops/wrangle.html.haml +++ b/app/views/crops/wrangle.html.haml @@ -1,68 +1,60 @@ - content_for :title, "Crop Wrangling" +%h1 Crop Wrangling -%ul - %li= link_to "Full crop hierarchy", hierarchy_crops_path - %li= link_to "Add Crop", new_crop_path +%nav.nav + = link_to "Full crop hierarchy", hierarchy_crops_path, class: 'nav-link' + = link_to "Add Crop", new_crop_path, class: 'btn' -.crop_wranglers - %h2 Crop Wranglers: - %ul - - @crop_wranglers.each do |crop_wrangler| - %li.crop_wrangler - = link_to crop_wrangler.login_name, crop_wrangler +%section.crop_wranglers + %h2 Crop Wranglers + - @crop_wranglers.each do |crop_wrangler| + = render 'members/tiny', member: crop_wrangler -.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") +%hr/ -%h2 - - if @approval_status == "pending" - Requested Crops - - elsif @approval_status == "rejected" - Rejected Crops - - else - Recently added crops +%section + %h2 Crops + %ul#myTab.nav.nav-tabs{role: "tablist"} + %li.nav-item + %a#home-tab.nav-link{ href: wrangle_crops_path, role: "tab", class: @approval_status.blank? ? 'active' : ''} + Recently added + %li.nav-item + %a#profile-tab.nav-link{ href: wrangle_crops_path(approval_status: "pending"), role: "tab", class: @approval_status == "pending" ? 'active' : ''} + Pending approval + %li.nav-item + %a#contact-tab.nav-link{ href: wrangle_crops_path(approval_status: "rejected"), role: "tab", class: @approval_status == "rejected" ? 'active' : ''} Rejected -.pagination - = page_entries_info @crops - = will_paginate @crops - -%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 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| + %table.table.table-striped.table-bordered.table-sm{id: @approval_status.blank? ? 'recently-added-crops' : "#{@approval_status}-crops" } %tr - %td= link_to c.name, edit_crop_path(c) - %td= link_to c.en_wikipedia_url, c.en_wikipedia_url - %td - - c.scientific_names.each do |s| - = link_to s.name, s - %br/ - %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. + %th System name + %th English Wikipedia URL + %th Scientific names + %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 + %td + = link_to edit_crop_path(c) do + = icon 'fas', 'seedling' + = c.name + %td= link_to c.en_wikipedia_url, c.en_wikipedia_url + %td + - c.scientific_names.each do |s| + = link_to s.name, s + %br/ + %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. -.pagination = page_entries_info @crops = will_paginate @crops - diff --git a/app/views/gardens/index.html.haml b/app/views/gardens/index.html.haml index 444b9c4dc..0515ad7ef 100644 --- a/app/views/gardens/index.html.haml +++ b/app/views/gardens/index.html.haml @@ -13,18 +13,20 @@ - else %li.breadcrumb-item.active= link_to 'Gardens', gardens_path -= link_to gardens_active_tickbox_path(@owner, @show_all) do - = check_box_tag 'active', 'all', @show_all - include in-active - -.pagination - = page_entries_info @gardens - = will_paginate @gardens - +%section.border-top + = link_to gardens_active_tickbox_path(@owner, @show_all), class: 'btn' do + = check_box_tag 'active', 'all', @show_all + include in-active - if @gardens.empty? %p There are no gardens to display. + - if can?(:create, Garden) && @owner == current_member + = link_to 'Add a garden', new_garden_path, class: 'btn btn-primary' + - else + .row + .col-12= page_entries_info @gardens + .col-12= will_paginate @gardens - @gardens.each do |garden| %section.border-top .row @@ -43,6 +45,6 @@ .col-6.col-md-4.col-lg-3= render 'plantings/card', planting: planting -.pagination - = page_entries_info @gardens - = will_paginate @gardens + .row + .col-12= page_entries_info @gardens + .col-12= will_paginate @gardens diff --git a/app/views/harvests/show.html.haml b/app/views/harvests/show.html.haml index 5132e8bd6..116a7ec84 100644 --- a/app/views/harvests/show.html.haml +++ b/app/views/harvests/show.html.haml @@ -33,10 +33,11 @@ %span= display_quantity(@harvest) - - if @harvest.description.present? - %h2 Notes - :growstuff_markdown - #{ @harvest.description != "" ? strip_tags(@harvest.description) : "No description given." } + .col-md-4 + .card + .card-body= render 'harvests/owner', harvest: @harvest + + = render @harvest.crop %hr/ - if @harvest.photos.size.positive? diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 8093caccb..9376d9a02 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -28,11 +28,11 @@ - if member_signed_in? %li.nav-item.dropdown %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"} - - if current_member.notifications.unread_count.positive? + - if current_member.unread_count.positive? = t('.your_stuff') - %span.badge.badge-info= current_member.notifications.unread_count + %span.badge.badge-info= current_member.unread_count - else - = t('.current_memberlogin_name', current_memberlogin_name: current_member.login_name) + = t('.current_memberlogin_name', current_memberlogin_name: current_member) .dropdown-menu{"aria-labelledby" => "navbarDropdown"} = link_to member_path(current_member), class: 'dropdown-item' do = t('.profile') @@ -46,14 +46,14 @@ = t('.seeds') = link_to t('.posts'), member_posts_path(current_member), class: 'dropdown-item' - - if current_member.notifications.unread_count.positive? + - if current_member.unread_count.positive? .dropdown-divider %strong - = link_to(notifications_path, class: 'dropdown-item') do + = link_to(conversations_path, class: 'dropdown-item') do = t('.inbox') - %span.badge.badge-info= current_member.notifications.unread_count + %span.badge.badge-info= current_member.unread_count - else - = link_to t('.inbox'), notifications_path, class: 'dropdown-item' + = link_to t('.inbox'), conversations_path, class: 'dropdown-item' - if current_member.role?(:crop_wrangler) || current_member.role?(:admin) %li.nav-item.dropdown diff --git a/app/views/mailboxer/message_mailer/new_message_email.html.erb b/app/views/mailboxer/message_mailer/new_message_email.html.erb new file mode 100644 index 000000000..c779e8881 --- /dev/null +++ b/app/views/mailboxer/message_mailer/new_message_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

You have a new message: <%= @subject %>

+

+ You have received a new message: +

+
+

+ <%= raw @message.body %> +

+
+

+ Visit <%= link_to root_url, root_url %> and go to your inbox for more info. +

+ + diff --git a/app/views/mailboxer/message_mailer/new_message_email.text.erb b/app/views/mailboxer/message_mailer/new_message_email.text.erb new file mode 100644 index 000000000..228ca58a4 --- /dev/null +++ b/app/views/mailboxer/message_mailer/new_message_email.text.erb @@ -0,0 +1,10 @@ +You have a new message: <%= @subject %> +=============================================== + +You have received a new message: + +----------------------------------------------- +<%= @message.body.html_safe? ? @message.body : strip_tags(@message.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your inbox for more info. diff --git a/app/views/mailboxer/message_mailer/reply_message_email.html.erb b/app/views/mailboxer/message_mailer/reply_message_email.html.erb new file mode 100644 index 000000000..fd1286c5f --- /dev/null +++ b/app/views/mailboxer/message_mailer/reply_message_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

You have a new reply: <%= @subject %>

+

+ You have received a new reply: +

+
+

+ <%= raw @message.body %> +

+
+

+ Visit <%= link_to root_url, root_url %> and go to your inbox for more info. +

+ + diff --git a/app/views/mailboxer/message_mailer/reply_message_email.text.erb b/app/views/mailboxer/message_mailer/reply_message_email.text.erb new file mode 100644 index 000000000..c56bfb5e0 --- /dev/null +++ b/app/views/mailboxer/message_mailer/reply_message_email.text.erb @@ -0,0 +1,10 @@ +You have a new reply: <%= @subject %> +=============================================== + +You have received a new reply: + +----------------------------------------------- +<%= @message.body.html_safe? ? @message.body : strip_tags(@message.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your inbox for more info. diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb new file mode 100644 index 000000000..5502d7fc4 --- /dev/null +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

You have a new notification: <%= @notification.subject.html_safe? ? @notification.subject : strip_tags(@notification.subject) %>

+

+ You have received a new notification: +

+
+

+ <%= raw @notification.body %> +

+
+

+ Visit <%= link_to root_url,root_url %> and go to your notifications for more info. +

+ + \ No newline at end of file diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb new file mode 100644 index 000000000..8ae17d32a --- /dev/null +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -0,0 +1,10 @@ +You have a new notification: <%= @notification.subject.html_safe? ? @notification.subject : strip_tags(@notification.subject) %> +=============================================== + +You have received a new notification: + +----------------------------------------------- +<%= @notification.body.html_safe? ? @notification.body : strip_tags(@notification.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your notifications for more info. diff --git a/app/views/members/_actions.html.haml b/app/views/members/_actions.html.haml deleted file mode 100644 index 9e69566d0..000000000 --- a/app/views/members/_actions.html.haml +++ /dev/null @@ -1,15 +0,0 @@ --# %p.btn-group --# - if can? :update, @member --# = link_to edit_member_registration_path, class: 'btn btn-default text-right' do --# = edit_icon --# = t('members.edit_profile') - --# - if can?(:create, Notification) && current_member != @member --# = link_to 'Send message', new_notification_path(recipient_id: @member.id), class: 'btn btn-default' - --# - if current_member && current_member != @member # must be logged in, can't follow yourself --# - follow = current_member.get_follow(@member) --# - if !follow && can?(:create, Follow) # not already following --# = link_to 'Follow', follows_path(followed: @member), method: :post, class: 'btn btn-default text-right' --# - if follow && can?(:destroy, follow) # already following --# = link_to 'Unfollow', follow_path(follow), method: :delete, class: 'btn btn-default text-right' diff --git a/app/views/members/_member.haml b/app/views/members/_member.haml index 3f0746362..2274a8e5c 100644 --- a/app/views/members/_member.haml +++ b/app/views/members/_member.haml @@ -19,10 +19,10 @@ %ul.nav.nav-justified.small %li.nav-item.border-right = link_to member_plantings_path(member) do - = localize_plural(member.plantings.active, Planting) + = localize_plural(member.plantings, Planting) %li.nav-item.border-right = link_to member_harvests_path(member) do = localize_plural(member.harvests, Harvest) %li.nav-item = link_to member_seeds_path(member) do - = localize_plural(member.seeds.active, Seed) + = localize_plural(member.seeds, Seed) diff --git a/app/views/members/_tiny.haml b/app/views/members/_tiny.haml index f8c534b3d..dcd4e9987 100644 --- a/app/views/members/_tiny.haml +++ b/app/views/members/_tiny.haml @@ -1 +1,10 @@ -= image_tag(avatar_uri(member, 50), alt: '', class: 'img img-fluid avatar', height: 50) \ No newline at end of file +.member-chip + - if member.discarded? + = icon 'fas', 'user-times' + = member + - else + = link_to member do + = image_tag(avatar_uri(member, 100), alt: '', height: 50, width: 50) + = member + + diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index 2560ef9ae..5e1192c31 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -7,9 +7,6 @@ = tag("meta", property: "og:url", content: request.original_url) = tag("meta", property: "og:site_name", content: ENV['GROWSTUFF_SITE_NAME']) - -= render 'members/actions', member: @member - - content_for :member_rss_login_name, @member.login_name - content_for :member_rss_slug, @member.slug @@ -39,7 +36,7 @@ = t('members.edit_profile') - if can?(:create, Notification) && current_member != @member - = link_to new_notification_path(recipient_id: @member.id), class: 'btn btn-block' do + = link_to new_message_path(recipient_id: @member.id), class: 'btn btn-block' do = icon('fas', 'envelope') Send message diff --git a/app/views/messages/_form.haml b/app/views/messages/_form.haml new file mode 100644 index 000000000..c7c6e7a84 --- /dev/null +++ b/app/views/messages/_form.haml @@ -0,0 +1,17 @@ += bootstrap_form_tag url: '/messages' do |f| + - if @conversation.present? + = f.hidden_field :conversation_id, value: @conversation.id + - elsif @recipient.present? + %p + To + = link_to @recipient, @recipient + = render 'members/tiny', member: @recipient + = f.hidden_field :recipient_id, value: @recipient.id + = f.text_field :subject, value: @subject, class: 'form-control', maxlength: 255, required: true + + = f.text_area :body, rows: 12, label: "Type your message here", required: true + %span.help-block= render partial: "shared/markdown_help" + + .card-footer + = link_to 'cancel', conversations_path, class: 'btn' + .float-right= f.submit "Send", class: 'btn btn-primary' diff --git a/app/views/notifications/new.html.haml b/app/views/messages/new.html.haml similarity index 62% rename from app/views/notifications/new.html.haml rename to app/views/messages/new.html.haml index 721dcca7d..0f7477430 100644 --- a/app/views/notifications/new.html.haml +++ b/app/views/messages/new.html.haml @@ -1,6 +1,6 @@ = content_for :title, "Send a message to #{@recipient}" - content_for :breadcrumbs do - %li.breadcrumb-item= link_to 'Messages', notifications_path + %li.breadcrumb-item= link_to 'Messages', conversations_path = render 'form' diff --git a/app/views/notifications/_form.html.haml b/app/views/notifications/_form.html.haml deleted file mode 100644 index 5f5880a93..000000000 --- a/app/views/notifications/_form.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -= bootstrap_form_for @notification do |f| - .card - .card-body - - if @notification.errors.any? - #error_explanation - %h2 - = pluralize(@post.errors.size, "error") - prohibited this message from being sent: - %ul - - @notification.errors.full_messages.each do |msg| - %li= msg - - .field - = f.hidden_field :recipient_id, value: @recipient.id - - %p - To - = link_to @recipient, @recipient - = render 'members/tiny', member: @recipient - = f.text_field :subject, value: @subject, class: 'form-control', maxlength: 255 - = f.text_area :body, rows: 12, label: "Type your message here" - %span.help-block= render partial: "shared/markdown_help" - .card-footer - - = link_to 'cancel', notifications_path, class: 'btn' - .float-right= f.submit "Send", class: 'btn btn-primary' diff --git a/app/views/notifications/_notification.html.haml b/app/views/notifications/_notification.html.haml deleted file mode 100644 index 881b93c50..000000000 --- a/app/views/notifications/_notification.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%p.text-muted - From - = link_to notification.sender, notification.sender - on - = notification.created_at - - - if notification.post_id - in response to: - = link_to notification.post.subject, notification.post - -.well - :growstuff_markdown - #{ strip_tags(notification.body) } diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml deleted file mode 100644 index d6ac4b5b3..000000000 --- a/app/views/notifications/index.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -- content_for :title, "Inbox" -- content_for :breadcrumbs do - %li.breadcrumb-item.active= link_to 'Messages', notifications_path - -- if @notifications.empty? - .alert.alert-success{role: "alert"} You have no messages. -- else - = paginate @notifications - - - @notifications.each do |n| - .card.message - .card-body - .row - .col-6.col-md-9 - %h3= link_to n.subject, notification_path(n) - %p - - if n.read - = icon 'far', 'envelope-open' - - else - = icon 'far', 'envelope' - %strong unread - = n.created_at - .col-6.col-md-3.text-right - - if n.sender.present? - = link_to n.sender do - %h3 - = render 'members/tiny', member: n.sender - = n.sender - = render 'members/follow_buttons', member: n.sender - - else - Member deleted - .card-footer - = link_to 'Read', n, class: 'btn btn-primary' - = link_to 'Reply', reply_link(n), class: 'btn btn-secondary' - = link_to n, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-default btn-danger btn-xs' do - = delete_icon - = t('buttons.delete') - = paginate @notifications \ No newline at end of file diff --git a/app/views/notifications/reply.html.haml b/app/views/notifications/reply.html.haml deleted file mode 100644 index 54230dff4..000000000 --- a/app/views/notifications/reply.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -= content_for :title, "Send a message to #{@recipient}" -- content_for :breadcrumbs do - %li.breadcrumb-item= link_to 'Messages', notifications_path - -.card - .card-body= render @sender_notification -= render 'form' - diff --git a/app/views/notifications/show.html.haml b/app/views/notifications/show.html.haml deleted file mode 100644 index 84a8a29f2..000000000 --- a/app/views/notifications/show.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -= content_for :title, @notification.subject - -- content_for :breadcrumbs do - %li.breadcrumb-item= link_to 'Messages', notifications_path - -.card - .card-header - .card-title= @notification.subject - .float-right - - if @notification.sender.present? - = link_to @notification.sender do - = @notification.sender - = render 'members/tiny', member: @notification.sender - - else - Member deleted - - .card-body - = render @notification - - .card-footer - = link_to 'Reply', @reply_link, class: 'btn btn-primary' - = link_to @notification, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-default btn-danger' do - = delete_icon - = t('buttons.delete') diff --git a/app/views/plantings/_card.html.haml b/app/views/plantings/_card.html.haml index 23cee3cec..c6695f3e0 100644 --- a/app/views/plantings/_card.html.haml +++ b/app/views/plantings/_card.html.haml @@ -21,5 +21,4 @@ .card-body.text-center %h4.card-title= link_to planting.crop, planting .text-center= render 'plantings/badges', planting: planting - .card-footer = render 'plantings/progress', planting: planting \ No newline at end of file diff --git a/app/views/plantings/_progress.html.haml b/app/views/plantings/_progress.html.haml index 6577f2403..16fb5686f 100644 --- a/app/views/plantings/_progress.html.haml +++ b/app/views/plantings/_progress.html.haml @@ -1,10 +1,8 @@ -- if planting.planted_at.present? - planted #{planting.planted_at} -- if planting.percentage_grown.present? +- if planting.annual? && planting.percentage_grown.present? .progress .progress-bar.bg-success{"aria-valuemax" => "100", "aria-valuemin" => "0", "aria-valuenow" => planting.percentage_grown, :role => "progressbar", :style => "width: #{planting.percentage_grown}%; height: 25px"} .float-left #{sprintf '%.0f', planting.percentage_grown}% - unless planting.finish_predicted_at.blank? - .float-right= planting.finish_predicted_at + .float-right= planting.finish_predicted_at.strftime(Date::DATE_FORMATS[:ymd]) diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index f905e12d2..f0c60a53e 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -13,25 +13,28 @@ - else %li.breadcrumb-item.active= link_to 'Plantings', plantings_path -= link_to plantings_active_tickbox_path(@owner, @show_all) do - = check_box_tag 'active', 'all', @show_all - include in-active +%section.border-top + .btn-group + = link_to plantings_active_tickbox_path(@owner, @show_all), class: 'btn' do + = check_box_tag 'active', 'all', @show_all + include in-active -- if @owner - = link_to t('.view_owners_profile', owner: @owner), member_path(@owner) + - if @owner + = link_to t('.view_owners_profile', owner: @owner), member_path(@owner), class: 'btn' -.pagination - = page_entries_info @plantings - = render 'layouts/pagination', collection: @plantings +.row + .col-12= page_entries_info @plantings + .col-12= render 'layouts/pagination', collection: @plantings .index-cards= render @plantings, full: true -.pagination - = page_entries_info @plantings - = render 'layouts/pagination', collection: @plantings +.row + .col-12= page_entries_info @plantings + .col-12= render 'layouts/pagination', collection: @plantings - %ul.list-inline - %li= t('.the_data_on_this_page_is_available_in_the_following_formats') - - ['csv', 'json', 'rss'].each do |format| - %li= link_to format.upcase, - (@owner ? member_plantings_path(@owner, format: format) : plantings_path(format: format)) +%p= t('.the_data_on_this_page_is_available_in_the_following_formats') +%ul.list-group.list-group-horizontal + - ['csv', 'json', 'rss'].each do |format| + %li.list-group-item + = icon 'fas', format + = link_to format.upcase, (@owner ? member_plantings_path(@owner, format: format) : plantings_path(format: format)) diff --git a/app/views/posts/show.html.haml b/app/views/posts/show.html.haml index b91bbb8ad..f88612496 100644 --- a/app/views/posts/show.html.haml +++ b/app/views/posts/show.html.haml @@ -32,25 +32,24 @@ .row .col-md-8.col-12 - %section.blog-post - .card.post{ id: "post-#{@post.id}" } - .card-header - %h2.display-3.float-left - = @post.subject - = render 'posts/actions', post: @post - .card-body - .post-content= render 'posts/single', post: @post - .index-cards - - @post.photos.each do |photo| - = render 'photos/thumbnail', photo: photo + .card.post{ id: "post-#{@post.id}" } + .card-header + %h2.display-3.float-left + = @post.subject + = render 'posts/actions', post: @post + .card-body + .post-content= render 'posts/single', post: @post + .index-cards + - @post.photos.each do |photo| + = render 'photos/thumbnail', photo: photo - .card-footer - = render 'posts/likes', post: @post - .float-right - - if can? :create, Comment - = link_to new_comment_path(post_id: @post.id), class: 'btn' do - = icon 'fas', 'comment' - Comment + .card-footer + = render 'posts/likes', post: @post + .float-right + - if can? :create, Comment + = link_to new_comment_path(post_id: @post.id), class: 'btn' do + = icon 'fas', 'comment' + Comment %secion.comments= render "comments", post: @post diff --git a/app/views/seeds/_card.html.haml b/app/views/seeds/_card.html.haml index e46e5da8b..cd7db9099 100644 --- a/app/views/seeds/_card.html.haml +++ b/app/views/seeds/_card.html.haml @@ -2,11 +2,12 @@ = link_to seed do = image_tag(seed_image_path(seed, full_size: true), alt: seed, class: 'img-card') .card-body - %h5= link_to seed, seed + .card-title= link_to seed.crop, seed + .card-footer + = render 'members/tiny', member: seed.owner - if seed.tradable? - %span.badge.badge-pill.badge-secondary Will trade #{seed.tradable_to} - = link_to seed.owner do - .card-footer - %span.badge.badge-pill.badge-secondary - = truncate(seed.owner.location, length: 20, separator: ' ', omission: '... ') - = render 'members/tiny', member: seed.owner \ No newline at end of file + %span.badge.badge-pill.badge-info + = icon 'fas', 'map' + Will trade #{seed.tradable_to} + %span.badge.badge-pill.badge-secondary + = truncate(seed.owner.location, length: 30, separator: ' ', omission: '... ') \ No newline at end of file diff --git a/app/views/seeds/show.html.haml b/app/views/seeds/show.html.haml index 4e833f02b..07aec7b53 100644 --- a/app/views/seeds/show.html.haml +++ b/app/views/seeds/show.html.haml @@ -68,7 +68,7 @@ - if current_member - if @seed.tradable? && current_member != @seed.owner %p= link_to "Request seeds", - new_notification_path(recipient_id: @seed.owner.id, + new_message_path(recipient_id: @seed.owner.id, subject: "Interested in your #{@seed.crop} seeds"), class: 'btn btn-primary' - else diff --git a/config/initializers/mailboxer.rb b/config/initializers/mailboxer.rb new file mode 100644 index 000000000..6f1b0fa7c --- /dev/null +++ b/config/initializers/mailboxer.rb @@ -0,0 +1,21 @@ +Mailboxer.setup do |config| + # Configures if your application uses or not email sending for Notifications and Messages + config.uses_emails = true + + # Configures the default from for emails sent for Messages and Notifications + config.default_from = "no-reply@growstuff.org" + + # Configures the methods needed by mailboxer + # config.email_method = :email + config.name_method = :login_name + config.notify_method = :notify + + # Configures if you use or not a search engine and which one you are using + # Supported engines: [:solr,:sphinx,:pg_search] + # config.search_enabled = false + # config.search_engine = :solr + + # Configures maximum length of the message subject and body + config.subject_max_length = 255 + config.body_max_length = 32_000 +end diff --git a/config/routes.rb b/config/routes.rb index 008033fc1..e1424d010 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -97,9 +97,8 @@ Rails.application.routes.draw do get 'followers' => 'follows#followers' end - resources :notifications do - get 'reply' - end + resources :messages + resources :conversations resources :places, only: %i(index show), param: :place do get 'search', on: :collection diff --git a/db/migrate/20171105011017_set_prediction_data.rb b/db/migrate/20171105011017_set_prediction_data.rb index c1931b2b6..8dfd91b4f 100644 --- a/db/migrate/20171105011017_set_prediction_data.rb +++ b/db/migrate/20171105011017_set_prediction_data.rb @@ -1,7 +1,7 @@ class SetPredictionData < ActiveRecord::Migration[4.2] def up say "Updating all plantings time to first harvest" - Planting.all.each(&:update_harvest_days!) + Planting.unscoped.all.each(&:update_harvest_days!) say "Updating crop median time to first harvest, and lifespan" Crop.all.each do |crop| crop.update_lifespan_medians diff --git a/db/migrate/20190317023129_finished_boolean.rb b/db/migrate/20190317023129_finished_boolean.rb index 5f8601fca..ac6524316 100644 --- a/db/migrate/20190317023129_finished_boolean.rb +++ b/db/migrate/20190317023129_finished_boolean.rb @@ -1,7 +1,7 @@ class FinishedBoolean < ActiveRecord::Migration[5.2] def change - Planting.where('finished_at < now()').update_all(finished: true) - Planting.where(finished: nil).update_all(finished: false) + Planting.unscoped.where('finished_at < now()').update_all(finished: true) + Planting.unscoped.where(finished: nil).update_all(finished: false) change_column_null :plantings, :finished, false change_column_default :plantings, :finished, from: nil, to: false end diff --git a/db/migrate/20190326224347_add_harvests_count.rb b/db/migrate/20190326224347_add_harvests_count.rb index 8b2396713..9557ffdae 100644 --- a/db/migrate/20190326224347_add_harvests_count.rb +++ b/db/migrate/20190326224347_add_harvests_count.rb @@ -2,7 +2,7 @@ class AddHarvestsCount < ActiveRecord::Migration[5.2] def change add_column :plantings, :harvests_count, :integer, default: 0 Planting.reset_column_information - Planting.pluck(:id).each do |planting_id| + Planting.unscoped.pluck(:id).each do |planting_id| Planting.reset_counters(planting_id, :harvests) end end diff --git a/db/migrate/20190720000555_create_mailboxer.mailboxer_engine.rb b/db/migrate/20190720000555_create_mailboxer.mailboxer_engine.rb new file mode 100644 index 000000000..ff7eaea51 --- /dev/null +++ b/db/migrate/20190720000555_create_mailboxer.mailboxer_engine.rb @@ -0,0 +1,64 @@ +# This migration comes from mailboxer_engine (originally 20110511145103) +class CreateMailboxer < ActiveRecord::Migration[4.2] + def self.up + # Tables + # Conversations + create_table :mailboxer_conversations do |t| + t.column :subject, :string, default: "" + t.timestamps null: false + end + # Receipts + create_table :mailboxer_receipts do |t| + t.references :receiver, polymorphic: true + t.column :notification_id, :integer, null: false + t.column :is_read, :boolean, default: false + t.column :trashed, :boolean, default: false + t.column :deleted, :boolean, default: false + t.column :mailbox_type, :string, limit: 25 + t.timestamps null: false + end + # Notifications and Messages + create_table :mailboxer_notifications do |t| + t.column :type, :string + t.column :body, :text + t.column :subject, :string, default: "" + t.references :sender, polymorphic: true + t.column :conversation_id, :integer + t.column :draft, :boolean, default: false + t.string :notification_code, default: nil + t.references :notified_object, polymorphic: true, index: { name: 'mailboxer_notifications_notified_object' } + t.column :attachment, :string + t.timestamps null: false + t.boolean :global, default: false + t.datetime :expires + end + + # Indexes + # Conversations + # Receipts + add_index "mailboxer_receipts", "notification_id" + + # Messages + add_index "mailboxer_notifications", "conversation_id" + + # Foreign keys + # Conversations + # Receipts + add_foreign_key "mailboxer_receipts", "mailboxer_notifications", + name: "receipts_on_notification_id", column: "notification_id" + # Messages + add_foreign_key "mailboxer_notifications", "mailboxer_conversations", + name: "notifications_on_conversation_id", column: "conversation_id" + end + + def self.down + # Tables + remove_foreign_key "mailboxer_receipts", name: "receipts_on_notification_id" + remove_foreign_key "mailboxer_notifications", name: "notifications_on_conversation_id" + + # Indexes + drop_table :mailboxer_receipts + drop_table :mailboxer_conversations + drop_table :mailboxer_notifications + end +end diff --git a/db/migrate/20190720000556_add_conversation_optout.mailboxer_engine.rb b/db/migrate/20190720000556_add_conversation_optout.mailboxer_engine.rb new file mode 100644 index 000000000..19190febd --- /dev/null +++ b/db/migrate/20190720000556_add_conversation_optout.mailboxer_engine.rb @@ -0,0 +1,17 @@ +# This migration comes from mailboxer_engine (originally 20131206080416) +class AddConversationOptout < ActiveRecord::Migration[4.2] + def self.up + create_table :mailboxer_conversation_opt_outs do |t| + t.references :unsubscriber, polymorphic: true + t.references :conversation + t.timestamps null: false + end + add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", + name: "mb_opt_outs_on_conversations_id", column: "conversation_id" + end + + def self.down + remove_foreign_key "mailboxer_conversation_opt_outs", name: "mb_opt_outs_on_conversations_id" + drop_table :mailboxer_conversation_opt_outs + end +end diff --git a/db/migrate/20190720000557_add_missing_indices.mailboxer_engine.rb b/db/migrate/20190720000557_add_missing_indices.mailboxer_engine.rb new file mode 100644 index 000000000..b079f4a3d --- /dev/null +++ b/db/migrate/20190720000557_add_missing_indices.mailboxer_engine.rb @@ -0,0 +1,20 @@ +# This migration comes from mailboxer_engine (originally 20131206080417) +class AddMissingIndices < ActiveRecord::Migration[4.2] + def change + # We'll explicitly specify its name, as the auto-generated name is too long and exceeds 63 + # characters limitation. + add_index :mailboxer_conversation_opt_outs, %i(unsubscriber_id unsubscriber_type), + name: 'index_mailboxer_conversation_opt_outs_on_unsubscriber_id_type' + add_index :mailboxer_conversation_opt_outs, :conversation_id + + add_index :mailboxer_notifications, :type + add_index :mailboxer_notifications, %i(sender_id sender_type) + + # We'll explicitly specify its name, as the auto-generated name is too long and exceeds 63 + # characters limitation. + add_index :mailboxer_notifications, %i(notified_object_id notified_object_type), + name: 'index_mailboxer_notifications_on_notified_object_id_and_type' + + add_index :mailboxer_receipts, %i(receiver_id receiver_type) + end +end diff --git a/db/migrate/20190720000558_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb b/db/migrate/20190720000558_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb new file mode 100644 index 000000000..498d152d9 --- /dev/null +++ b/db/migrate/20190720000558_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb @@ -0,0 +1,8 @@ +# This migration comes from mailboxer_engine (originally 20151103080417) +class AddDeliveryTrackingInfoToMailboxerReceipts < ActiveRecord::Migration[4.2] + def change + add_column :mailboxer_receipts, :is_delivered, :boolean, default: false + add_column :mailboxer_receipts, :delivery_method, :string + add_column :mailboxer_receipts, :message_id, :string + end +end diff --git a/db/migrate/20190720000625_notifications_to_mailboxer.rb b/db/migrate/20190720000625_notifications_to_mailboxer.rb new file mode 100644 index 000000000..1dabd8925 --- /dev/null +++ b/db/migrate/20190720000625_notifications_to_mailboxer.rb @@ -0,0 +1,20 @@ +class NotificationsToMailboxer < ActiveRecord::Migration[5.2] + def up + Mailboxer.setup do |config| + # turn off emails + config.uses_emails = false + end + Notification.find_in_batches.each do |group| + group.each do |n| + n.body = 'message has no body' if n.body.blank? + receipt = n.sender.send_message(n.recipient, n.body, n.subject) + # Copy over which messages are read + receipt.conversation.receipts.each(&:mark_as_read) if n.read + # copy over timestamps + receipt.conversation.messages.each do |msg| + msg.update!(created_at: n.created_at, updated_at: n.updated_at) + end + end + end + end +end diff --git a/db/migrate/20190721042146_paranoia_to_discard.rb b/db/migrate/20190721042146_paranoia_to_discard.rb new file mode 100644 index 000000000..4b8803360 --- /dev/null +++ b/db/migrate/20190721042146_paranoia_to_discard.rb @@ -0,0 +1,5 @@ +class ParanoiaToDiscard < ActiveRecord::Migration[5.2] + def change + rename_column :members, :deleted_at, :discarded_at + end +end diff --git a/db/schema.rb b/db/schema.rb index e5faf5c12..1ce9e5deb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_12_234859) do +ActiveRecord::Schema.define(version: 2019_07_21_042146) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -268,6 +268,62 @@ ActiveRecord::Schema.define(version: 2019_07_12_234859) do t.index ["member_id"], name: "index_likes_on_member_id" end + create_table "mailboxer_conversation_opt_outs", id: :serial, force: :cascade do |t| + t.string "unsubscriber_type" + t.integer "unsubscriber_id" + t.integer "conversation_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["conversation_id"], name: "index_mailboxer_conversation_opt_outs_on_conversation_id" + t.index ["unsubscriber_id", "unsubscriber_type"], name: "index_mailboxer_conversation_opt_outs_on_unsubscriber_id_type" + end + + create_table "mailboxer_conversations", id: :serial, force: :cascade do |t| + t.string "subject", default: "" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "mailboxer_notifications", id: :serial, force: :cascade do |t| + t.string "type" + t.text "body" + t.string "subject", default: "" + t.string "sender_type" + t.integer "sender_id" + t.integer "conversation_id" + t.boolean "draft", default: false + t.string "notification_code" + t.string "notified_object_type" + t.integer "notified_object_id" + t.string "attachment" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "global", default: false + t.datetime "expires" + t.index ["conversation_id"], name: "index_mailboxer_notifications_on_conversation_id" + t.index ["notified_object_id", "notified_object_type"], name: "index_mailboxer_notifications_on_notified_object_id_and_type" + t.index ["notified_object_type", "notified_object_id"], name: "mailboxer_notifications_notified_object" + t.index ["sender_id", "sender_type"], name: "index_mailboxer_notifications_on_sender_id_and_sender_type" + t.index ["type"], name: "index_mailboxer_notifications_on_type" + end + + create_table "mailboxer_receipts", id: :serial, force: :cascade do |t| + t.string "receiver_type" + t.integer "receiver_id" + t.integer "notification_id", null: false + t.boolean "is_read", default: false + t.boolean "trashed", default: false + t.boolean "deleted", default: false + t.string "mailbox_type", limit: 25 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "is_delivered", default: false + t.string "delivery_method" + t.string "message_id" + t.index ["notification_id"], name: "index_mailboxer_receipts_on_notification_id" + t.index ["receiver_id", "receiver_type"], name: "index_mailboxer_receipts_on_receiver_id_and_receiver_type" + end + create_table "median_functions", id: :serial, force: :cascade do |t| end @@ -307,11 +363,11 @@ ActiveRecord::Schema.define(version: 2019_07_12_234859) do t.integer "gardens_count" t.integer "harvests_count" t.integer "seeds_count" - t.datetime "deleted_at" + t.datetime "discarded_at" t.integer "photos_count" t.integer "forums_count" t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true - t.index ["deleted_at"], name: "index_members_on_deleted_at" + t.index ["discarded_at"], name: "index_members_on_discarded_at" t.index ["email"], name: "index_members_on_email", unique: true t.index ["reset_password_token"], name: "index_members_on_reset_password_token", unique: true t.index ["slug"], name: "index_members_on_slug", unique: true @@ -457,6 +513,9 @@ ActiveRecord::Schema.define(version: 2019_07_12_234859) do end add_foreign_key "harvests", "plantings" + add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", column: "conversation_id", name: "mb_opt_outs_on_conversations_id" + add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id" + add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "photo_associations", "crops" add_foreign_key "photo_associations", "photos" add_foreign_key "plantings", "seeds", column: "parent_seed_id", name: "parent_seed", on_delete: :nullify diff --git a/spec/controllers/api/v1/plantings_controller_spec.rb b/spec/controllers/api/v1/plantings_controller_spec.rb index 57b450d94..c187e1213 100644 --- a/spec/controllers/api/v1/plantings_controller_spec.rb +++ b/spec/controllers/api/v1/plantings_controller_spec.rb @@ -5,12 +5,6 @@ require 'rails_helper' RSpec.describe Api::V1::PlantingsController, type: :controller do subject { JSON.parse response.body } - let(:headers) do - { - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json' - } - end let!(:member) { FactoryBot.create :member } describe '#index' do diff --git a/spec/controllers/charts/gardens_controller_spec.rb b/spec/controllers/charts/gardens_controller_spec.rb index 250b05358..1eb959b29 100644 --- a/spec/controllers/charts/gardens_controller_spec.rb +++ b/spec/controllers/charts/gardens_controller_spec.rb @@ -2,7 +2,6 @@ require 'rails_helper' describe Charts::GardensController do include Devise::Test::ControllerHelpers - let(:valid_params) { { name: 'My second Garden' } } let(:garden) { FactoryBot.create :garden } diff --git a/spec/controllers/garden_types_controller_spec.rb b/spec/controllers/garden_types_controller_spec.rb index 73e59d176..53bc9cae8 100644 --- a/spec/controllers/garden_types_controller_spec.rb +++ b/spec/controllers/garden_types_controller_spec.rb @@ -5,9 +5,6 @@ RSpec.describe GardenTypesController, type: :controller do let(:valid_params) { { name: 'My second GardenType' } } let(:garden_type) { FactoryBot.create :garden_type } - let(:member) { FactoryBot.create(:member) } - let(:admin_member) { FactoryBot.create(:admin) } - context "when not signed in" do describe 'GET new' do before { get :new, params: { id: garden_type.to_param } } diff --git a/spec/controllers/likes_controller_spec.rb b/spec/controllers/likes_controller_spec.rb index 5527a595e..6730b0a8f 100644 --- a/spec/controllers/likes_controller_spec.rb +++ b/spec/controllers/likes_controller_spec.rb @@ -4,7 +4,6 @@ describe LikesController do let(:like) { FactoryBot.create :like, member: member } let(:member) { FactoryBot.create(:member) } let(:blogpost) { FactoryBot.create(:post) } - let(:mypost) { FactoryBot.create(:post, author: member) } before { sign_in member } @@ -22,7 +21,6 @@ describe LikesController do end describe "Liking your own post" do - let(:blogpost) { FactoryBot.create(:post, author: member) } end end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb deleted file mode 100644 index c606d9fe9..000000000 --- a/spec/controllers/notifications_controller_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'rails_helper' - -describe NotificationsController do - login_member - - def valid_attributes - { - "recipient_id" => subject.current_member.id, - "sender_id" => FactoryBot.create(:member).id, - "subject" => 'test' - } - end - - describe "GET index" do - it "assigns all notifications as @notifications" do - notification = FactoryBot.create(:notification, recipient_id: subject.current_member.id) - get :index, params: {} - assigns(:notifications).should eq([notification]) - end - end - - describe "GET show" do - it "assigns the requested notification as @notification" do - notification = FactoryBot.create(:notification, recipient_id: subject.current_member.id) - get :show, params: { id: notification.to_param } - assigns(:notification).should eq(notification) - end - - it "assigns the reply link for a post comment" do - notification = FactoryBot.create(:notification, recipient_id: subject.current_member.id) - - get :show, params: { id: notification.to_param } - assigns(:reply_link).should_not be_nil - assigns(:reply_link).should eq new_comment_url( - post_id: notification.post.id - ) - end - - it "marks notifications as read" do - notification = FactoryBot.create(:notification, recipient_id: subject.current_member.id) - get :show, params: { id: notification.to_param } - # we need to fetch it from the db again, can't test against the old one - n = Notification.find(notification.id) - n.read.should eq true - end - end - - describe "GET reply" do - it "marks notifications as read" do - notification = FactoryBot.create(:notification, recipient_id: subject.current_member.id) - get :reply, params: { notification_id: notification.to_param } - # we need to fetch it from the db again, can't test against the old one - n = Notification.find(notification.id) - n.read.should eq true - end - end - - describe "GET new" do - it "assigns a recipient" do - @recipient = FactoryBot.create(:member) - get :new, params: { recipient_id: @recipient.id } - expect(assigns(:recipient)).to be_an_instance_of(Member) - end - end - - describe "POST create" do - describe "with valid params" do - it "redirects to the recipient's profile" do - @recipient = FactoryBot.create(:member) - post :create, params: { notification: { recipient_id: @recipient.id, subject: 'foo' } } - response.should redirect_to(notifications_path) - end - end - end -end diff --git a/spec/features/admin/admin_spec.rb b/spec/features/admin/admin_spec.rb index 6ee586ee7..cbd83bf53 100644 --- a/spec/features/admin/admin_spec.rb +++ b/spec/features/admin/admin_spec.rb @@ -1,13 +1,8 @@ require 'rails_helper' describe "forums", js: true do - context "as an admin user" do - let(:member) { create :admin_member } - - before do - login_as member - end - + context 'signed in admin' do + include_context 'signed in admin' it "navigating to forum admin with js" do visit admin_path Percy.snapshot(page, name: 'Admin page') diff --git a/spec/features/admin/forums_spec.rb b/spec/features/admin/forums_spec.rb index d13cc13fb..7a41266c0 100644 --- a/spec/features/admin/forums_spec.rb +++ b/spec/features/admin/forums_spec.rb @@ -1,52 +1,54 @@ require 'rails_helper' describe "forums", js: true do - context "as an admin user" do - let(:member) { create :admin_member } - let(:forum) { create :forum } + include_context 'signed in admin' + let(:forum) { create :forum } + describe "navigating to forum admin with js" do before do - login_as member - end - - it "navigating to forum admin with js" do visit admin_path - within 'ul#site_admin' do + within 'nav#site_admin' do click_link "Forums" end - expect(current_path).to eq forums_path - expect(page).to have_link "New forum" end + it { expect(current_path).to eq forums_path } + it { expect(page).to have_link "New forum" } + end - it "adding a forum" do + describe "adding a forum" do + before do visit forums_path click_link "New forum" expect(current_path).to eq new_forum_path fill_in 'Name', with: 'Discussion' fill_in 'Description', with: "this is a new forum" click_button 'Save' - expect(current_path).to eq forum_path(Forum.last) - expect(page).to have_content 'Forum was successfully created' end + it { expect(current_path).to eq forum_path(Forum.last) } + it { expect(page).to have_content 'Forum was successfully created' } + end - it 'editing forum' do + describe 'editing forum' do + before do visit forum_path forum click_link 'Edit' fill_in 'Name', with: 'Something else' click_button 'Save' forum.reload - expect(current_path).to eq forum_path(forum) - expect(page).to have_content 'Forum was successfully updated' - expect(page).to have_content 'Something else' end + it { expect(current_path).to eq forum_path(forum) } + it { expect(page).to have_content 'Forum was successfully updated' } + it { expect(page).to have_content 'Something else' } + end - it 'deleting forum' do + describe 'deleting forum' do + before do visit forum_path forum accept_confirm do click_link 'Delete' end - expect(current_path).to eq forums_path - expect(page).to have_content 'Forum was successfully deleted' end + it { expect(current_path).to eq forums_path } + it { expect(page).to have_content 'Forum was successfully deleted' } end end diff --git a/spec/features/cms_spec.rb b/spec/features/cms_spec.rb index 4abe7d6d4..d13fc1b42 100644 --- a/spec/features/cms_spec.rb +++ b/spec/features/cms_spec.rb @@ -1,26 +1,25 @@ require 'rails_helper' describe "cms admin" do - let(:member) { create :member } - let(:admin_member) { create :admin_member } - it "can't view CMS admin if not signed in" do visit comfy_admin_cms_path expect(current_path).to eq root_path expect(page).to have_content "Please sign in as an admin user" end - it "can't view CMS admin if not an admin member" do - # sign in as an ordinary member - login_as member - visit comfy_admin_cms_path - expect(current_path).to eq root_path - expect(page).to have_content "Please sign in as an admin user" + context 'signed in' do + include_context 'signed in member' + it "can't view CMS admin if not an admin member" do + visit comfy_admin_cms_path + expect(current_path).to eq root_path + expect(page).to have_content "Please sign in as an admin user" + end end - - it "admin members can view CMS admin area" do - login_as admin_member - visit comfy_admin_cms_path - expect(current_path).to match(/#{comfy_admin_cms_path}/) # match any CMS admin page + context 'admin' do + include_context 'signed in admin' + it "admin members can view CMS admin area" do + visit comfy_admin_cms_path + expect(current_path).to match(/#{comfy_admin_cms_path}/) # match any CMS admin page + end end end diff --git a/spec/features/comments/commenting_a_comment_spec.rb b/spec/features/comments/commenting_a_comment_spec.rb index f2136e98e..6debc9d14 100644 --- a/spec/features/comments/commenting_a_comment_spec.rb +++ b/spec/features/comments/commenting_a_comment_spec.rb @@ -1,13 +1,11 @@ require 'rails_helper' describe 'Commenting on a post' do - let(:member) { create :member } - let(:post) { create :post, author: member } + include_context 'signed in member' + let(:member) { create :member } + let(:post) { create :post, author: member } - before do - login_as member - visit new_comment_path post_id: post.id - end + before { visit new_comment_path post_id: post.id } it "creating a comment" do fill_in "comment_body", with: "This is a sample test for comment" diff --git a/spec/features/conversations/index_spec.rb b/spec/features/conversations/index_spec.rb new file mode 100644 index 000000000..850e42236 --- /dev/null +++ b/spec/features/conversations/index_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe "Conversations", :js do + let(:sender) { create :member } + let(:recipient) { create :member, login_name: 'beyonce' } + + before do + sender.send_message(recipient, "this is the body", "something i want to say") + login_as recipient + end + + describe "Read conversations list" do + before do + visit root_path + click_link 'Your Stuff' + click_link 'Inbox' + end + it { expect(page).to have_content 'something i want to say' } + it { Percy.snapshot(page, name: 'conversations#index') } + + describe 'deleting' do + before do + # delete button + click_link class: 'delete' + end + + describe 'view trash' do + before { click_link 'trash' } + it { expect(page).to have_content 'something i want to say' } + describe 'restore conversation' do + before { click_link class: 'restore' } + it { expect(page).not_to have_content 'something i want to say' } + + describe 'conversation was restored' do + before { click_link 'inbox' } + it { expect(page).to have_content 'something i want to say' } + end + end + end + end + end +end diff --git a/spec/features/conversations/show_spec.rb b/spec/features/conversations/show_spec.rb new file mode 100644 index 000000000..ccf92fb8f --- /dev/null +++ b/spec/features/conversations/show_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' + +describe "Conversations", :js do + let(:sender) { create :member } + let(:recipient) { create :member, login_name: 'beyonce' } + + before do + sender.send_message(recipient, "this is the body", "something i want to say") + login_as recipient + end + + describe 'view conversation thread' do + before do + visit root_path + click_link 'Your Stuff' + click_link 'Inbox' + click_link 'something i want to say' + end + + it { expect(page).to have_content 'this is the body' } + it { expect(page).to have_link sender.login_name } + it { Percy.snapshot(page, name: 'conversations#show') } + + describe 'Replying to the conversation' do + before do + fill_in :body, with: 'i like this too' + click_button 'Send' + end + it { expect(page).to have_content "i like this too" } + end + end +end diff --git a/spec/features/crops/alternate_name_spec.rb b/spec/features/crops/alternate_name_spec.rb index 903b6d3a7..adfc3b4e3 100644 --- a/spec/features/crops/alternate_name_spec.rb +++ b/spec/features/crops/alternate_name_spec.rb @@ -4,26 +4,23 @@ describe "Alternate names", js: true do let!(:alternate_eggplant) { create :alternate_eggplant } let(:crop) { alternate_eggplant.crop } - it "Display alternate names on crop page" do - visit crop_path(alternate_eggplant.crop) - # expect(page.status_code).to equal 200 - expect(page).to have_content alternate_eggplant.name - end - - it "Index page for alternate names" do - visit alternate_names_path - expect(page).to have_content alternate_eggplant.name - end - - context "User is a crop wrangler" do - let!(:crop_wranglers) { create_list :crop_wrangling_member, 3 } - let(:member) { crop_wranglers.first } - - before do - login_as member + shared_examples 'show alt names' do + it "can see alternate names on crop page" do + visit crop_path(alternate_eggplant.crop) + # expect(page.status_code).to equal 200 + expect(page).to have_content alternate_eggplant.name end - it "Crop wranglers can edit alternate names" do + it "can see page for alternate names" do + visit alternate_names_path + expect(page).to have_content alternate_eggplant.name + end + end + + shared_examples 'edit alt names' do + let!(:crop_wranglers) { create_list :crop_wrangling_member, 3 } + + it "can edit alternate names" do visit crop_path(crop) # expect(page.status_code).to equal 200 expect(page).to have_content "CROP WRANGLER" @@ -41,7 +38,7 @@ describe "Alternate names", js: true do expect(page).to have_content 'Alternate name was successfully updated' end - it "Crop wranglers can delete alternate names" do + it "can delete alternate names" do visit crop_path(alternate_eggplant.crop) click_link('aubergine', href: '#') accept_confirm do @@ -51,7 +48,7 @@ describe "Alternate names", js: true do expect(page).to have_content 'Alternate name was successfully deleted' end - it "Crop wranglers can add alternate names" do + it "can add alternate names" do visit crop_path(crop) expect(page).to have_link "Add", href: new_alternate_name_path(crop_id: crop.id) @@ -65,7 +62,7 @@ describe "Alternate names", js: true do expect(page).to have_content 'Alternate name was successfully created' end - it "The show-alternate-name page works" do + it "shows alternate-name page" do visit alternate_name_path(alternate_eggplant) # expect(page.status_code).to equal 200 expect(page).to have_content alternate_eggplant.crop.name @@ -82,4 +79,25 @@ describe "Alternate names", js: true do end end end + + context 'Anonymous' do + include_examples 'show alt names' + end + + context 'Signed in member' do + include_context 'signed in member' + include_examples 'show alt names' + end + + context 'Crop wrangler' do + include_context 'signed in crop wrangler' + include_examples 'show alt names' + include_examples 'edit alt names' + end + + context 'Admin' do + include_context 'signed in admin' + include_examples 'show alt names' + include_examples 'edit alt names' + end end diff --git a/spec/features/crops/browse_crops_spec.rb b/spec/features/crops/browse_crops_spec.rb index 821ae01c4..d60727bbc 100644 --- a/spec/features/crops/browse_crops_spec.rb +++ b/spec/features/crops/browse_crops_spec.rb @@ -6,35 +6,51 @@ describe "browse crops" do let!(:pending_crop) { FactoryBot.create :crop_request } let!(:rejected_crop) { FactoryBot.create :rejected_crop } - it "has a form for sorting by" do - visit crops_path - expect(page).to have_css "select#sort" - end - - it "shows a list of crops" do - crop1 = tomato - visit crops_path - expect(page).to have_content crop1.name - end - - it "pending crops are not listed" do - visit crops_path - expect(page).not_to have_content pending_crop.name - end - - it "rejected crops are not listed" do - visit crops_path - expect(page).not_to have_content rejected_crop.name - end - - context "logged in and crop wrangler" do - before do - login_as FactoryBot.create(:crop_wrangling_member) - visit crops_path + shared_examples 'shows crops' do + before { visit crops_path } + it "has a form for sorting by" do + expect(page).to have_css "select#sort" end + it "shows a list of crops" do + expect(page).to have_content tomato.name + end + + it "pending crops are not listed" do + expect(page).not_to have_content pending_crop.name + end + + it "rejected crops are not listed" do + expect(page).not_to have_content rejected_crop.name + end + end + + shared_examples 'add new crop' do it "shows a new crop link" do expect(page).to have_link "Add New Crop" end end + + context 'anon' do + include_examples 'shows crops' + it { expect(page).not_to have_link "Add New Crop" } + end + + context 'member' do + include_context 'signed in member' + include_examples 'shows crops' + include_examples 'add new crop' + end + + context 'wrangler' do + include_context 'signed in crop wrangler' + include_examples 'shows crops' + include_examples 'add new crop' + end + + context 'admin' do + include_context 'signed in admin' + include_examples 'shows crops' + include_examples 'add new crop' + end end diff --git a/spec/features/crops/creating_a_crop_spec.rb b/spec/features/crops/creating_a_crop_spec.rb index 14dd983d2..21178ac92 100644 --- a/spec/features/crops/creating_a_crop_spec.rb +++ b/spec/features/crops/creating_a_crop_spec.rb @@ -1,35 +1,66 @@ require 'rails_helper' -describe "Crop - " do - let!(:crop_wrangler) { FactoryBot.create :crop_wrangling_member } - let!(:member) { FactoryBot.create :member } - - before do - login_as member - visit new_crop_path +describe "Crop", js: true do + shared_examples 'fill in form' do + before do + visit new_crop_path + within "form#new_crop" do + fill_in "crop_name", with: "Philippine flower" + fill_in "en_wikipedia_url", with: "https://en.wikipedia.org/wiki/Jasminum_sambac" + click_button "add-sci_name-row" + fill_in "sci_name[1]", with: "Jasminum sambac 1" + fill_in "sci_name[2]", with: "Jasminum sambac 2" + fill_in "alt_name[1]", with: "Sampaguita" + click_button "add-alt_name-row" + click_button "add-alt_name-row" + fill_in "alt_name[2]", with: "Manol" + click_button "add-alt_name-row" + fill_in "alt_name[3]", with: "Jazmin" + fill_in "alt_name[4]", with: "Matsurika" + end + end + end + shared_examples 'request crop' do + describe "requesting a crop with multiple scientific and alternate name" do + include_examples 'fill in form' + before do + within "form#new_crop" do + fill_in "request_notes", with: "This is the Philippine national flower." + click_button "Save" + end + end + it { expect(page).to have_content 'crop was successfully created.' } + it { expect(page).to have_content "This crop is currently pending approval." } + it { expect(page).to have_content "Jasminum sambac 2" } + it { expect(page).to have_content "Matsurika" } + end + end + shared_examples 'create crop' do + describe "creating a crop with multiple scientific and alternate name" do + include_examples 'fill in form' + before do + click_button "Save" + end + it { expect(page).to have_content 'crop was successfully created.' } + it { expect(page).to have_content "Jasminum sambac 2" } + it { expect(page).to have_content "Matsurika" } + end end - it "creating a crop with multiple scientific and alternate name", :js do - within "form#new_crop" do - fill_in "crop_name", with: "Philippine flower" - fill_in "en_wikipedia_url", with: "https://en.wikipedia.org/wiki/Jasminum_sambac" - click_button "add-sci_name-row" - fill_in "sci_name[1]", with: "Jasminum sambac 1" - fill_in "sci_name[2]", with: "Jasminum sambac 2" - fill_in "alt_name[1]", with: "Sampaguita" - click_button "add-alt_name-row" - click_button "add-alt_name-row" - fill_in "alt_name[2]", with: "Manol" - click_button "add-alt_name-row" - fill_in "alt_name[3]", with: "Jazmin" - fill_in "alt_name[4]", with: "Matsurika" - fill_in "request_notes", with: "This is the Philippine national flower." - click_button "Save" - end - - expect(page).to have_content 'crop was successfully created.' - expect(page).to have_content "This crop is currently pending approval." - expect(page).to have_content "Jasminum sambac 2" - expect(page).to have_content "Matsurika" + context 'anon' do + before { visit new_crop_path } + it { expect(page).to have_content 'You need to sign in' } + end + context 'member' do + include_context 'signed in member' + include_examples 'request crop' + end + context 'crop wrangler' do + include_context 'signed in crop wrangler' + include_examples 'create crop' + end + context 'admin' do + include_context 'signed in admin' + include_examples 'create crop' end end diff --git a/spec/features/crops/crop_detail_page_spec.rb b/spec/features/crops/crop_detail_page_spec.rb index 2e1fec723..3d1449a1d 100644 --- a/spec/features/crops/crop_detail_page_spec.rb +++ b/spec/features/crops/crop_detail_page_spec.rb @@ -20,13 +20,8 @@ describe "crop detail page", js: true do end end - context "signed in member" do - let(:member) { create :member } - - before do - login_as(member) - end - + context 'signed in' do + include_context "signed in member" context "action buttons" do before { subject } @@ -78,25 +73,25 @@ describe "crop detail page", js: true do end context "seed quantity for a crop" do - let(:member) { create :member } - let(:seed) { create :seed, crop: crop, quantity: 20, owner: member } + let(:seed) { create :seed, crop: crop, quantity: 20 } it "User not signed in" do visit crop_path(seed.crop) expect(page).not_to have_content "You have 20 seeds" end - it "User signed in" do - login_as(member) - visit crop_path(seed.crop) - expect(page).to have_link "You have 20 seeds of this crop." - end - - it "click link to your owned seeds" do - login_as(member) - visit crop_path(seed.crop) - click_link "You have 20 seeds of this crop." - expect(current_path).to eq member_seeds_path(member_slug: member.slug) + context 'signed in' do + include_context 'signed in member' + before { seed.update! owner: member } + it "User signed in" do + visit crop_path(seed.crop) + expect(page).to have_link "You have 20 seeds of this crop." + end + it "click link to your owned seeds" do + visit crop_path(seed.crop) + click_link "You have 20 seeds of this crop." + expect(current_path).to eq member_seeds_path(member_slug: member.slug) + end end end diff --git a/spec/features/crops/crop_photos_spec.rb b/spec/features/crops/crop_photos_spec.rb index e66d645d3..13ecc14ac 100644 --- a/spec/features/crops/crop_photos_spec.rb +++ b/spec/features/crops/crop_photos_spec.rb @@ -3,20 +3,20 @@ require 'rails_helper' describe "crop detail page", js: true do subject { page } - let!(:member) { FactoryBot.create :member } + let!(:owner_member) { FactoryBot.create :member } let!(:crop) { FactoryBot.create :crop } - let!(:planting) { FactoryBot.create :planting, crop: crop, owner: member } - let!(:harvest) { FactoryBot.create :harvest, crop: crop, owner: member } - let!(:seed) { FactoryBot.create :seed, crop: crop, owner: member } + let!(:planting) { FactoryBot.create :planting, crop: crop, owner: owner_member } + let!(:harvest) { FactoryBot.create :harvest, crop: crop, owner: owner_member } + let!(:seed) { FactoryBot.create :seed, crop: crop, owner: owner_member } - let!(:photo1) { FactoryBot.create(:photo, owner: member) } - let!(:photo2) { FactoryBot.create(:photo, owner: member) } - let!(:photo3) { FactoryBot.create(:photo, owner: member) } - let!(:photo4) { FactoryBot.create(:photo, owner: member) } - let!(:photo5) { FactoryBot.create(:photo, owner: member) } - let!(:photo6) { FactoryBot.create(:photo, owner: member) } + let!(:photo1) { FactoryBot.create(:photo, owner: owner_member) } + let!(:photo2) { FactoryBot.create(:photo, owner: owner_member) } + let!(:photo3) { FactoryBot.create(:photo, owner: owner_member) } + let!(:photo4) { FactoryBot.create(:photo, owner: owner_member) } + let!(:photo5) { FactoryBot.create(:photo, owner: owner_member) } + let!(:photo6) { FactoryBot.create(:photo, owner: owner_member) } before do planting.photos << photo1 @@ -50,14 +50,13 @@ describe "crop detail page", js: true do end context "when signed in" do - before { login_as(FactoryBot.create(:member)) } - + include_context 'signed in member' include_examples "shows photos" end context "when signed in as photos owner" do - before { login_as(member) } - + include_context 'signed in member' + let(:member) { owner_member } include_examples "shows photos" end diff --git a/spec/features/crops/crop_wranglers_spec.rb b/spec/features/crops/crop_wranglers_spec.rb index 77571c0d4..5dd8af71f 100644 --- a/spec/features/crops/crop_wranglers_spec.rb +++ b/spec/features/crops/crop_wranglers_spec.rb @@ -2,21 +2,19 @@ require 'rails_helper' describe "crop wranglers", js: true do context "signed in wrangler" do + include_context 'signed in crop wrangler' let!(:crop_wranglers) { create_list :crop_wrangling_member, 3 } - let(:wrangler) { crop_wranglers.first } let!(:crops) { create_list :crop, 2 } let!(:requested_crop) { create :crop_request } let!(:rejected_crop) { create :rejected_crop } - before { login_as wrangler } - it "sees crop wranglers listed on the crop wrangler page" do visit root_path click_link 'Admin' click_link 'Crop Wrangling' within '.crop_wranglers' do - expect(page).to have_content 'Crop Wranglers:' + expect(page).to have_content 'Crop Wranglers' crop_wranglers.each do |crop_wrangler| expect(page).to have_link crop_wrangler.login_name, href: member_path(crop_wrangler) end @@ -67,22 +65,20 @@ describe "crop wranglers", js: true do expect(page).to have_content "This crop was rejected for the following reason: Totally fake" end end +end - context "signed in non-wrangler" do - let!(:crop_wranglers) { create_list :crop_wrangling_member, 3 } - let(:member) { create :member } +context "signed in non-wrangler" do + include_context 'signed in member' + let!(:crop_wranglers) { create_list :crop_wrangling_member, 3 } - before { login_as member } + it "can't see wrangling page without js", js: false do + visit root_path + expect(page).not_to have_link "Crop Wrangling" + end - it "can't see wrangling page without js", js: false do - visit root_path - expect(page).not_to have_link "Crop Wrangling" - end - - it "can't see wrangling page with js" do - visit root_path - click_link member.login_name - expect(page).not_to have_link "Crop Wrangling" - end + it "can't see wrangling page with js" do + visit root_path + click_link member.login_name + expect(page).not_to have_link "Crop Wrangling" end end diff --git a/spec/features/crops/crop_wrangling_button_spec.rb b/spec/features/crops/crop_wrangling_button_spec.rb index 7e9c3d8d1..3545db9ff 100644 --- a/spec/features/crops/crop_wrangling_button_spec.rb +++ b/spec/features/crops/crop_wrangling_button_spec.rb @@ -1,28 +1,20 @@ require 'rails_helper' describe "crop wrangling button" do - let(:crop_wrangler) { create :crop_wrangling_member } - let(:member) { create :member } - - context "crop wrangling button" do - before do - login_as crop_wrangler - visit crops_path - end - - it "has a link to crop wrangling page" do - expect(page).to have_link "Wrangle Crops", href: wrangle_crops_path - end + context 'not signed in' do + before { visit crops_path } + it { expect(page).to have_no_link "Wrangle Crops", href: wrangle_crops_path } end - context "crop wrangling button" do - before do - login_as member - visit crops_path - end + context "signed in, but not a crop wrangler" do + include_context 'signed in member' + before { visit crops_path } + it { expect(page).to have_no_link "Wrangle Crops", href: wrangle_crops_path } + end - it "has no link to crop wrangling page" do - expect(page).to have_no_link "Wrangle Crops", href: wrangle_crops_path - end + context "signed in crop wrangler" do + include_context 'signed in crop wrangler' + before { visit crops_path } + it { expect(page).to have_link "Wrangle Crops", href: wrangle_crops_path } end end diff --git a/spec/features/crops/delete_crop_spec.rb b/spec/features/crops/delete_crop_spec.rb index b3c108ef9..41092c895 100644 --- a/spec/features/crops/delete_crop_spec.rb +++ b/spec/features/crops/delete_crop_spec.rb @@ -1,14 +1,10 @@ require 'rails_helper' describe "Delete crop spec" do - context "As a crop wrangler" do - let(:wrangler) { FactoryBot.create :crop_wrangling_member } + shared_examples 'delete crop' do let!(:pending_crop) { FactoryBot.create :crop_request } let!(:approved_crop) { FactoryBot.create :crop } - - before { login_as wrangler } - - it "Delete approved crop" do + it "deletes approved crop" do visit crop_path(approved_crop) click_link 'Actions' accept_confirm do @@ -17,7 +13,7 @@ describe "Delete crop spec" do expect(page).to have_content "crop was successfully destroyed" end - it "Delete pending crop" do + it "deletes pending crop" do visit crop_path(pending_crop) click_link 'Actions' accept_confirm do @@ -26,4 +22,14 @@ describe "Delete crop spec" do expect(page).to have_content "crop was successfully destroyed" end end + + context "As a crop wrangler" do + include_context 'signed in crop wrangler' + include_examples 'delete crop' + end + + context 'admin' do + include_context 'signed in admin' + include_examples 'delete crop' + end end diff --git a/spec/features/crops/request_new_crop_spec.rb b/spec/features/crops/request_new_crop_spec.rb index cd5cfb984..4257e3a69 100644 --- a/spec/features/crops/request_new_crop_spec.rb +++ b/spec/features/crops/request_new_crop_spec.rb @@ -2,13 +2,7 @@ require 'rails_helper' describe "Requesting a new crop" do context "As a regular member" do - let(:member) { create :member } - let!(:wrangler) { create :crop_wrangling_member } - - before do - login_as member - end - + include_context 'signed in member' it "Submit request" do visit new_crop_path fill_in "Name", with: "Couch potato" @@ -20,12 +14,10 @@ describe "Requesting a new crop" do end context "As a crop wrangler" do - let(:wrangler) { create :crop_wrangling_member } + include_context 'signed in crop wrangler' let!(:crop) { create :crop_request } let!(:already_approved) { create :crop } - before { login_as wrangler } - it "Approve a request" do visit edit_crop_path(crop) select "approved", from: "Approval status" diff --git a/spec/features/crops/requested_crops_spec.rb b/spec/features/crops/requested_crops_spec.rb index a4e406b63..e73292c1e 100644 --- a/spec/features/crops/requested_crops_spec.rb +++ b/spec/features/crops/requested_crops_spec.rb @@ -1,15 +1,14 @@ require 'rails_helper' -describe "Crop - " do - let(:member) { create :member } +describe "Requesting Crops" do let!(:requested_crop) { create :crop, requester: member, approval_status: 'pending', name: 'puha for dinner' } - before do - login_as member - visit requested_crops_path - end + context 'signed in' do + include_context 'signed in member' + before { visit requested_crops_path } - it "creating a crop with multiple scientific and alternate name", :js do - expect(page).to have_content "puha for dinner" + it "creating a crop with multiple scientific and alternate name", :js do + expect(page).to have_content "puha for dinner" + end end end diff --git a/spec/features/crops/scientific_name_spec.rb b/spec/features/crops/scientific_name_spec.rb index be428f7b0..56616b81b 100644 --- a/spec/features/crops/scientific_name_spec.rb +++ b/spec/features/crops/scientific_name_spec.rb @@ -18,12 +18,8 @@ describe "Scientific names", js: true do context "User is a crop wrangler" do let!(:crop_wranglers) { create_list :crop_wrangling_member, 3 } - let(:member) { crop_wranglers.first } - - before do - login_as(member) - end + include_context 'signed in crop wrangler' it "Crop wranglers can edit scientific names" do visit crop_path(crop) # expect(page.status_code).to equal 200 diff --git a/spec/features/crops/show_spec.rb b/spec/features/crops/show_spec.rb index bfab353a4..f6fb15410 100644 --- a/spec/features/crops/show_spec.rb +++ b/spec/features/crops/show_spec.rb @@ -1,10 +1,7 @@ require 'rails_helper' describe "browse crops" do - let(:tomato) { create :tomato } - let(:maize) { create :maize } - let(:pending_crop) { create :crop_request } - let(:rejected_crop) { create :rejected_crop } + let(:tomato) { create :tomato } it "Show crop info" do visit crop_path(tomato) diff --git a/spec/features/gardens/actions_spec.rb b/spec/features/gardens/actions_spec.rb index 0e53e178b..0c9336a74 100644 --- a/spec/features/gardens/actions_spec.rb +++ b/spec/features/gardens/actions_spec.rb @@ -5,9 +5,7 @@ describe "Gardens" do context 'logged in' do subject { page } - let(:member) { FactoryBot.create :member } - before { login_as member } - + include_context 'signed in member' let(:garden) { member.gardens.first } let(:other_member_garden) { FactoryBot.create :garden } diff --git a/spec/features/gardens/adding_gardens_spec.rb b/spec/features/gardens/adding_gardens_spec.rb index bb161e4fd..7320a1244 100644 --- a/spec/features/gardens/adding_gardens_spec.rb +++ b/spec/features/gardens/adding_gardens_spec.rb @@ -2,37 +2,35 @@ require 'rails_helper' require 'custom_matchers' describe "Gardens", :js do - let(:member) { FactoryBot.create :member } + context 'signed in' do + include_context 'signed in member' + before { visit new_garden_path } - before do - login_as member - visit new_garden_path - end + it "has the required fields help text" do + expect(page).to have_content "* denotes a required field" + end - it "has the required fields help text" do - expect(page).to have_content "* denotes a required field" - end + it "displays required and optional fields properly" do + expect(page).to have_selector ".required", text: "Name" + expect(page).to have_selector 'textarea#garden_description' + expect(page).to have_selector 'input#garden_location' + expect(page).to have_selector 'input#garden_area' + end - it "displays required and optional fields properly" do - expect(page).to have_selector ".required", text: "Name" - expect(page).to have_selector 'textarea#garden_description' - expect(page).to have_selector 'input#garden_location' - expect(page).to have_selector 'input#garden_area' - end + it "Create new garden" do + fill_in "Name", with: "New garden" + click_button "Save" + expect(page).to have_content "Garden was successfully created" + expect(page).to have_content "New garden" + end - it "Create new garden" do - fill_in "Name", with: "New garden" - click_button "Save" - expect(page).to have_content "Garden was successfully created" - expect(page).to have_content "New garden" - end - - it "Refuse to create new garden with negative area" do - visit new_garden_path - fill_in "Name", with: "Negative Garden" - fill_in "Area", with: -5 - click_button "Save" - expect(page).not_to have_content "Garden was successfully created" - expect(page).to have_content "Area must be greater than or equal to 0" + it "Refuse to create new garden with negative area" do + visit new_garden_path + fill_in "Name", with: "Negative Garden" + fill_in "Area", with: -5 + click_button "Save" + expect(page).not_to have_content "Garden was successfully created" + expect(page).to have_content "Area must be greater than or equal to 0" + end end end diff --git a/spec/features/gardens/gardens_spec.rb b/spec/features/gardens/gardens_spec.rb index f39c210f6..132997584 100644 --- a/spec/features/gardens/gardens_spec.rb +++ b/spec/features/gardens/gardens_spec.rb @@ -1,121 +1,120 @@ require 'rails_helper' describe "Planting a crop", js: true do - # name is aaa to ensure it is ordered first - let!(:garden) { create :garden, name: 'aaa' } - let!(:planting) { create :planting, garden: garden, owner: garden.owner, planted_at: Date.parse("2013-3-10") } - let!(:tomato) { create :tomato } - let!(:finished_planting) { create :finished_planting, owner: garden.owner, garden: garden, crop: tomato } + context 'signed in' do + include_context 'signed in member' + # name is aaa to ensure it is ordered first + let!(:garden) { create :garden, name: 'aaa', owner: member } + let!(:planting) { create :planting, garden: garden, owner: garden.owner, planted_at: Date.parse("2013-3-10") } + let!(:tomato) { create :tomato } + let!(:finished_planting) { create :finished_planting, owner: garden.owner, garden: garden, crop: tomato } - before do - login_as garden.owner - end - - it "View gardens" do - visit gardens_path - expect(page).to have_content "Everyone's gardens" - within '.layout-actions' do - click_link "My gardens" - end - expect(page).to have_content "#{garden.owner.login_name}'s gardens" - within '.layout-actions' do - click_link "Everyone's gardens" - end - expect(page).to have_content "Everyone's gardens" - end - - it "Marking a garden as inactive" do - visit garden_path(garden) - click_link 'Actions' - accept_confirm do - click_link "Mark as inactive" - end - expect(page).to have_content "Garden was successfully updated" - expect(page).to have_content "This garden is inactive" - - click_link 'Actions' - expect(page).to have_content "Mark as active" - expect(page).not_to have_content "Mark as inactive" - end - - it "List only active gardens" do - visit garden_path(garden) - click_link 'Actions' - accept_confirm do - click_link "Mark as inactive" - end - visit gardens_path - expect(page).not_to have_link garden_path(garden) - end - - it "Create new garden" do - visit new_garden_path - fill_in "Name", with: "New garden" - click_button "Save" - expect(page).to have_content "Garden was successfully created" - expect(page).to have_content "New garden" - end - - it "Refuse to create new garden with negative area" do - visit new_garden_path - fill_in "Name", with: "Negative Garden" - fill_in "Area", with: -5 - click_button "Save" - expect(page).not_to have_content "Garden was successfully created" - expect(page).to have_content "Area must be greater than or equal to 0" - end - - context "Clicking edit from the index page" do - before do + it "View gardens" do visit gardens_path + expect(page).to have_content "Everyone's gardens" + within '.layout-actions' do + click_link "My gardens" + end + expect(page).to have_content "#{garden.owner.login_name}'s gardens" + within '.layout-actions' do + click_link "Everyone's gardens" + end + expect(page).to have_content "Everyone's gardens" end - it "button on index to edit garden" do - first('a#garden-actions-button').click - click_link href: edit_garden_path(garden) - expect(page).to have_content 'Edit garden' - end - end - - it "Edit garden" do - visit new_garden_path - fill_in "Name", with: "New garden" - click_button "Save" - click_link 'Actions' - within '.garden-actions' do - click_link 'Edit' - end - fill_in "Name", with: "Different name" - click_button "Save" - expect(page).to have_content "Garden was successfully updated" - expect(page).to have_content "Different name" - end - - it "Delete garden" do - visit new_garden_path - fill_in "Name", with: "New garden" - click_button "Save" - visit garden_path(Garden.last) - click_link 'Actions' - accept_confirm do - click_link 'Delete' - end - expect(page).to have_content "Garden was successfully deleted" - expect(page).to have_content "#{garden.owner}'s gardens" - end - - describe "Making a planting inactive from garden show" do - it do + it "Marking a garden as inactive" do visit garden_path(garden) - click_link(class: 'planting-menu') - click_link "Mark as finished" - find(".datepicker-days td.day", text: "21").click - expect(page).to have_content 'Finished' - end - end + click_link 'Actions' + accept_confirm do + click_link "Mark as inactive" + end + expect(page).to have_content "Garden was successfully updated" + expect(page).to have_content "This garden is inactive" - it "List only active plantings on a garden" do - visit gardens_path - expect(page).not_to have_content finished_planting.crop_name + click_link 'Actions' + expect(page).to have_content "Mark as active" + expect(page).not_to have_content "Mark as inactive" + end + + it "List only active gardens" do + visit garden_path(garden) + click_link 'Actions' + accept_confirm do + click_link "Mark as inactive" + end + visit gardens_path + expect(page).not_to have_link garden_path(garden) + end + + it "Create new garden" do + visit new_garden_path + fill_in "Name", with: "New garden" + click_button "Save" + expect(page).to have_content "Garden was successfully created" + expect(page).to have_content "New garden" + end + + it "Refuse to create new garden with negative area" do + visit new_garden_path + fill_in "Name", with: "Negative Garden" + fill_in "Area", with: -5 + click_button "Save" + expect(page).not_to have_content "Garden was successfully created" + expect(page).to have_content "Area must be greater than or equal to 0" + end + + context "Clicking edit from the index page" do + before do + visit gardens_path + end + + it "button on index to edit garden" do + first('a#garden-actions-button').click + click_link href: edit_garden_path(garden) + expect(page).to have_content 'Edit garden' + end + end + + it "Edit garden" do + visit new_garden_path + fill_in "Name", with: "New garden" + click_button "Save" + click_link 'Actions' + within '.garden-actions' do + click_link 'Edit' + end + fill_in "Name", with: "Different name" + click_button "Save" + expect(page).to have_content "Garden was successfully updated" + expect(page).to have_content "Different name" + end + + it "Delete garden" do + visit new_garden_path + fill_in "Name", with: "New garden" + click_button "Save" + visit garden_path(Garden.last) + click_link 'Actions' + accept_confirm do + click_link 'Delete' + end + expect(page).to have_content "Garden was successfully deleted" + expect(page).to have_content "#{garden.owner}'s gardens" + end + + describe "Making a planting inactive from garden show" do + it do + visit garden_path(garden) + click_link(class: 'planting-menu') + click_link "Mark as finished" + find(".datepicker-days td.day", text: "21").click + expect(page).to have_content 'Finished' + end + end + + it "List only active plantings on a garden" do + visit gardens_path + expect(page).not_to have_content finished_planting.crop_name + end end end diff --git a/spec/features/gardens/index_spec.rb b/spec/features/gardens/index_spec.rb index e14bbbc5d..526bba7c9 100644 --- a/spec/features/gardens/index_spec.rb +++ b/spec/features/gardens/index_spec.rb @@ -3,10 +3,9 @@ require 'custom_matchers' describe "Gardens#index", :js do context "Logged in as member" do + include_context 'signed in member' let(:member) { FactoryBot.create :member, login_name: 'shadow' } - before { login_as member } - context "with 10 gardens" do before do FactoryBot.create_list :garden, 10, owner: member diff --git a/spec/features/harvests/browse_harvests_spec.rb b/spec/features/harvests/browse_harvests_spec.rb index 67dc75e55..6ead7e231 100644 --- a/spec/features/harvests/browse_harvests_spec.rb +++ b/spec/features/harvests/browse_harvests_spec.rb @@ -3,30 +3,30 @@ require 'rails_helper' describe "browse harvests" do subject { page } - let!(:member) { create :member } let!(:harvest) { create :harvest, owner: member } - before { login_as member } + context 'signed in' do + include_context 'signed in member' + describe 'blank optional fields' do + let!(:harvest) { create :harvest, :no_description } - describe 'blank optional fields' do - let!(:harvest) { create :harvest, :no_description } + before { visit harvests_path } - before { visit harvests_path } - - it 'read more' do - expect(subject).not_to have_link "Read more" - end - end - - describe "filled in optional fields" do - let!(:harvest) { create :harvest, :long_description } - - before do - visit harvests_path + it 'read more' do + expect(subject).not_to have_link "Read more" + end end - it 'links to #show' do - expect(subject).to have_link harvest.crop.name, href: harvest_path(harvest) + describe "filled in optional fields" do + let!(:harvest) { create :harvest, :long_description } + + before do + visit harvests_path + end + + it 'links to #show' do + expect(subject).to have_link harvest.crop.name, href: harvest_path(harvest) + end end end end diff --git a/spec/features/harvests/harvesting_a_crop_spec.rb b/spec/features/harvests/harvesting_a_crop_spec.rb index c425dfa4f..e7aeea3d6 100644 --- a/spec/features/harvests/harvesting_a_crop_spec.rb +++ b/spec/features/harvests/harvesting_a_crop_spec.rb @@ -2,128 +2,126 @@ require 'rails_helper' require 'custom_matchers' describe "Harvesting a crop", :js, :elasticsearch do - let(:member) { create :member } - let!(:maize) { create :maize } - let!(:plant_part) { create :plant_part } - let(:planting) { create :planting, crop: maize, owner: member } + context 'signed in' do + include_context 'signed in member' + let!(:maize) { create :maize } + let!(:plant_part) { create :plant_part } - before do - login_as member - visit new_harvest_path - end + before { visit new_harvest_path } - it_behaves_like "crop suggest", "harvest", "crop" + it_behaves_like "crop suggest", "harvest", "crop" - it "has the required fields help text" do - expect(page).to have_content "* denotes a required field" - end - - describe "displays required and optional fields properly" do - it { expect(page).to have_selector ".required", text: "What did you harvest?" } - it { expect(page).to have_selector 'input#harvest_quantity' } - it { expect(page).to have_selector 'input#harvest_weight_quantity' } - it { expect(page).to have_selector 'textarea#harvest_description' } - end - - it "Creating a new harvest", :js do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - - within "form#new_harvest" do - choose plant_part.name - fill_in "When?", with: "2014-06-15" - fill_in "How many?", with: 42 - fill_in "Weighing (in total)", with: 42 - fill_in "Notes", with: "It's killer." - click_button "Save" + it "has the required fields help text" do + expect(page).to have_content "* denotes a required field" end - expect(page).to have_content "harvest was successfully created." - end + describe "displays required and optional fields properly" do + it { expect(page).to have_selector ".required", text: "What did you harvest?" } + it { expect(page).to have_selector 'input#harvest_quantity' } + it { expect(page).to have_selector 'input#harvest_weight_quantity' } + it { expect(page).to have_selector 'textarea#harvest_description' } + end - it "Clicking link to owner's profile" do - visit member_harvests_path(member) - click_link "View #{member}'s profile >>" - expect(current_path).to eq member_path member - end + it "Creating a new harvest", :js do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" - describe "Harvesting from crop page" do - before do - visit crop_path(maize) - within '.crop-actions' do - click_link "Harvest #{maize.name}" - end within "form#new_harvest" do choose plant_part.name - expect(page).to have_selector "input[value='maize']" + fill_in "When?", with: "2014-06-15" + fill_in "How many?", with: 42 + fill_in "Weighing (in total)", with: 42 + fill_in "Notes", with: "It's killer." click_button "Save" end + + expect(page).to have_content "harvest was successfully created." + end + + it "Clicking link to owner's profile" do + visit member_harvests_path(member) + click_link "View #{member}'s profile >>" + expect(current_path).to eq member_path member + end + + describe "Harvesting from crop page" do + before do + visit crop_path(maize) + within '.crop-actions' do + click_link "Harvest #{maize.name}" + end + within "form#new_harvest" do + choose plant_part.name + expect(page).to have_selector "input[value='maize']" + click_button "Save" + end + end + + it { expect(page).to have_content "harvest was successfully created." } + it { expect(page).to have_content "maize" } + end + + describe "Harvesting from planting page" do + let!(:planting) { create :planting, crop: maize, owner: member, garden: member.gardens.first } + before do + visit planting_path(planting) + click_link "Record Harvest" + + choose plant_part.name + click_button "Save" + end + + it { expect(page).to have_content "harvest was successfully created." } + it { expect(page).to have_content planting.garden.name } + it { expect(page).to have_content "maize" } + end + + context "Editing a harvest" do + let(:existing_harvest) { create :harvest, crop: maize, owner: member } + let!(:other_plant_part) { create :plant_part, name: 'chocolate' } + + before do + visit harvest_path(existing_harvest) + click_link 'Actions' + click_link "Edit" + end + + it "Saving without edits" do + # Check that the autosuggest helper properly fills inputs with + # existing resource's data + click_button "Save" + expect(page).to have_content "harvest was successfully updated." + expect(page).to have_content "maize" + end + + it "change plant part" do + choose other_plant_part.name + click_button "Save" + expect(page).to have_content "harvest was successfully updated." + expect(page).to have_content other_plant_part.name + end end - it { expect(page).to have_content "harvest was successfully created." } - it { expect(page).to have_content "maize" } - end + context "Viewing a harvest" do + let(:existing_harvest) do + create :harvest, crop: maize, owner: member, + harvested_at: Time.zone.today + end + let!(:existing_planting) do + create :planting, crop: maize, owner: member, + planted_at: Time.zone.yesterday + end - describe "Harvesting from planting page" do - let!(:planting) { create :planting, crop: maize, owner: member, garden: member.gardens.first } - before do - visit planting_path(planting) - click_link "Record Harvest" + before do + visit harvest_path(existing_harvest) + end - choose plant_part.name - click_button "Save" - end - - it { expect(page).to have_content "harvest was successfully created." } - it { expect(page).to have_content planting.garden.name } - it { expect(page).to have_content "maize" } - end - - context "Editing a harvest" do - let(:existing_harvest) { create :harvest, crop: maize, owner: member } - let!(:other_plant_part) { create :plant_part, name: 'chocolate' } - - before do - visit harvest_path(existing_harvest) - click_link 'Actions' - click_link "Edit" - end - - it "Saving without edits" do - # Check that the autosuggest helper properly fills inputs with - # existing resource's data - click_button "Save" - expect(page).to have_content "harvest was successfully updated." - expect(page).to have_content "maize" - end - - it "change plant part" do - choose other_plant_part.name - click_button "Save" - expect(page).to have_content "harvest was successfully updated." - expect(page).to have_content other_plant_part.name - end - end - - context "Viewing a harvest" do - let(:existing_harvest) do - create :harvest, crop: maize, owner: member, - harvested_at: Time.zone.today - end - let!(:existing_planting) do - create :planting, crop: maize, owner: member, - planted_at: Time.zone.yesterday - end - - before do - visit harvest_path(existing_harvest) - end - - it "linking to a planting" do - expect(page).to have_content existing_planting.to_s - choose("harvest_planting_id_#{existing_planting.id}") - click_button "save" - expect(page).to have_link(href: planting_path(existing_planting)) + it "linking to a planting" do + expect(page).to have_content existing_planting.to_s + choose("harvest_planting_id_#{existing_planting.id}") + click_button "save" + expect(page).to have_link(href: planting_path(existing_planting)) + end end end end diff --git a/spec/features/home/home_spec.rb b/spec/features/home/home_spec.rb index 89dbacdc5..c3ad7d8c9 100644 --- a/spec/features/home/home_spec.rb +++ b/spec/features/home/home_spec.rb @@ -79,8 +79,7 @@ describe "home page" do end context "when signed in" do - before { login_as member } - + include_context 'signed in member' include_examples 'show crops' include_examples 'show plantings' include_examples 'show harvests' diff --git a/spec/features/likeable_spec.rb b/spec/features/likeable_spec.rb index 42aa03c6b..95503efaf 100644 --- a/spec/features/likeable_spec.rb +++ b/spec/features/likeable_spec.rb @@ -1,97 +1,95 @@ require 'rails_helper' describe 'Likeable', js: true do - let(:member) { FactoryBot.create(:member) } - let(:another_member) { FactoryBot.create(:london_member) } + let(:another_member) { FactoryBot.create(:london_member) } let!(:post) { FactoryBot.create(:post, author: member) } let!(:photo) { FactoryBot.create(:photo, owner: member) } - context 'logged in member' do - before { login_as member } + include_context 'signed in member' + describe 'photos' do + let(:like_count_class) { "#photo-#{photo.id} .like-count" } - describe 'photos' do - let(:like_count_class) { "#photo-#{photo.id} .like-count" } - - shared_examples 'photo can be liked' do - it 'can be liked' do - visit path - expect(page).to have_css(like_count_class, text: "0") - click_link '0', class: 'like-btn' - expect(page).to have_css(like_count_class, text: "1") - - # Reload page - visit path - expect(page).to have_css(like_count_class, text: "1") - expect(page).to have_link '1' - - click_link '1', class: 'like-btn' - expect(page).to have_css(like_count_class, text: "0") - end - - it 'displays correct number of likes' do - visit path - expect(page).to have_css(like_count_class, text: "0") - expect(page).to have_link '0' - click_link '0', class: 'like-btn' - expect(page).to have_css(like_count_class, text: "1") - - logout(member) - login_as(another_member) - visit path - - expect(page).to have_css(like_count_class, text: "1") - click_link '1', class: 'like-btn' - expect(page).to have_css(like_count_class, text: "2") - end - end - describe 'photos#index' do - let(:path) { photos_path } - include_examples 'photo can be liked' - end - describe 'photos#show' do - let(:path) { photo_path(photo) } - include_examples 'photo can be liked' - end - describe 'crops#show' do - let(:crop) { FactoryBot.create :crop } - let(:planting) { FactoryBot.create :planting, owner: member, crop: crop } - let(:path) { crop_path(crop) } - before { planting.photos << photo } - include_examples 'photo can be liked' - end - end - - describe 'posts' do - let(:like_count_class) { "#post-#{post.id} .like-count" } - before { visit post_path(post) } + shared_examples 'photo can be liked' do it 'can be liked' do + visit path expect(page).to have_css(like_count_class, text: "0") - expect(page).to have_link 'Like' - click_link 'Like', class: 'like-btn' + click_link '0', class: 'like-btn' expect(page).to have_css(like_count_class, text: "1") # Reload page - visit post_path(post) + visit path expect(page).to have_css(like_count_class, text: "1") - expect(page).to have_link 'Unlike' + expect(page).to have_link '1' - click_link 'Unlike', class: 'like-btn' + click_link '1', class: 'like-btn' expect(page).to have_css(like_count_class, text: "0") end it 'displays correct number of likes' do - expect(page).to have_link 'Like' - click_link 'Like', class: 'like-btn' + visit path + expect(page).to have_css(like_count_class, text: "0") + expect(page).to have_link '0' + click_link '0', class: 'like-btn' expect(page).to have_css(like_count_class, text: "1") logout(member) login_as(another_member) - visit post_path(post) + visit path - expect(page).to have_link 'Like' - click_link 'Like', class: 'like-btn' + expect(page).to have_css(like_count_class, text: "1") + click_link '1', class: 'like-btn' expect(page).to have_css(like_count_class, text: "2") + logout(another_member) end end + describe 'photos#index' do + let(:path) { photos_path } + include_examples 'photo can be liked' + end + describe 'photos#show' do + let(:path) { photo_path(photo) } + include_examples 'photo can be liked' + end + describe 'crops#show' do + let(:crop) { FactoryBot.create :crop } + let(:planting) { FactoryBot.create :planting, owner: member, crop: crop } + let(:path) { crop_path(crop) } + before { planting.photos << photo } + include_examples 'photo can be liked' + end + end + + describe 'posts' do + let(:like_count_class) { "#post-#{post.id} .like-count" } + before { visit post_path(post) } + it 'can be liked' do + expect(page).to have_css(like_count_class, text: "0") + expect(page).to have_link 'Like' + click_link 'Like', class: 'like-btn' + expect(page).to have_css(like_count_class, text: "1") + + # Reload page + visit post_path(post) + expect(page).to have_css(like_count_class, text: "1") + expect(page).to have_link 'Unlike' + + click_link 'Unlike', class: 'like-btn' + expect(page).to have_css(like_count_class, text: "0") + end + + it 'displays correct number of likes' do + expect(page).to have_link 'Like' + click_link 'Like', class: 'like-btn' + expect(page).to have_css(like_count_class, text: "1") + + logout(member) + login_as(another_member) + visit post_path(post) + + expect(page).to have_link 'Like' + click_link 'Like', class: 'like-btn' + expect(page).to have_css(like_count_class, text: "2") + logout(another_member) + end end end diff --git a/spec/features/members/ban_spec.rb b/spec/features/members/ban_spec.rb index 8eb2888e7..f6f99e698 100644 --- a/spec/features/members/ban_spec.rb +++ b/spec/features/members/ban_spec.rb @@ -4,11 +4,9 @@ describe "members list" do let!(:spammer) { FactoryBot.create :member } let!(:admin) { FactoryBot.create :admin_member } - context 'logged in as admin' do - before do - login_as admin - visit member_path(spammer) - end + context 'logged in as admin' do + include_context 'signed in admin' + before { visit member_path(spammer) } it { expect(page).to have_link "Ban member" } describe 'bans the user' do before do diff --git a/spec/features/members/deletion_spec.rb b/spec/features/members/deletion_spec.rb index 7a5ccf273..ce3ff7769 100644 --- a/spec/features/members/deletion_spec.rb +++ b/spec/features/members/deletion_spec.rb @@ -10,7 +10,6 @@ describe "member deletion" do let!(:harvest) { FactoryBot.create(:harvest, owner: member) } let!(:seed) { FactoryBot.create(:seed, owner: member) } let!(:secondgarden) { FactoryBot.create(:garden, owner: member) } - let(:admin) { FactoryBot.create(:admin_member) } before do login_as(member) @@ -41,7 +40,7 @@ describe "member deletion" do click_link 'Edit profile' click_link 'Delete Account' click_button "Delete" - expect(page).to have_content "Current password can't be blank" + expect(page).to have_content "Incorrect password" end it "password must be correct" do @@ -50,7 +49,7 @@ describe "member deletion" do click_link 'Delete Account' fill_in "current_pw_for_delete", with: "wrongpassword" click_button "Delete" - expect(page).to have_content "Current password is invalid" + expect(page).to have_content "Incorrect password" end it "deletes and removes bio" do @@ -87,7 +86,10 @@ describe "member deletion" do end describe 'member exists but is marked deleted' do - it { expect(Member.with_deleted.find(member.id)).to eq member } + subject { Member.all.find(member.id) } + it { expect(subject).to eq member } + it { expect(subject.discarded?).to eq true } + it { expect(Member.kept).not_to include(member) } end it "removes plantings" do @@ -144,7 +146,7 @@ describe "member deletion" do fill_in 'Login', with: member.login_name fill_in 'Password', with: member.password click_button 'Sign in' - expect(page).to have_content 'Invalid Login or password' + expect(page).to have_content 'Your account is not activated' end end end diff --git a/spec/features/members/following_spec.rb b/spec/features/members/following_spec.rb index 762320671..42a20df34 100644 --- a/spec/features/members/following_spec.rb +++ b/spec/features/members/following_spec.rb @@ -12,13 +12,9 @@ describe "follows", :js do end context "when signed in" do - let(:member) { create :member } + include_context 'signed in member' let(:other_member) { create :member } - before do - login_as(member) - end - it "your profile doesn't have a follow button" do visit member_path(member) expect(page).not_to have_link "Follow" diff --git a/spec/features/members/profile_spec.rb b/spec/features/members/profile_spec.rb index 6b6ab0f94..4b51b5868 100644 --- a/spec/features/members/profile_spec.rb +++ b/spec/features/members/profile_spec.rb @@ -162,8 +162,7 @@ describe "member profile", js: true do end context "signed in member" do - before { login_as(member) } - + include_context 'signed in member' include_examples 'member details' include_examples 'member activity' @@ -179,7 +178,7 @@ describe "member profile", js: true do before { visit member_path(other_member) } it "has a private message button" do - expect(page).to have_link "Send message", href: new_notification_path(recipient_id: other_member.id) + expect(page).to have_link "Send message", href: new_message_path(recipient_id: other_member.id) end it { expect(page).not_to have_link "Edit profile", href: edit_member_registration_path } end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb deleted file mode 100644 index dc149c7d9..000000000 --- a/spec/features/notifications_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'rails_helper' - -describe "Notifications", :js do - let(:sender) { create :member } - let(:recipient) { create :member, login_name: 'beyonce' } - - context "On existing notification" do - let!(:notification) do - create :notification, - sender: sender, - recipient: recipient, - body: "Notification body", - post_id: nil - end - - before do - login_as recipient - visit root_path - click_link 'Your Stuff' - Percy.snapshot(page, name: "notification menu") - visit notification_path(notification) - Percy.snapshot(page, name: "notifications#show") - end - - it "Replying to the notification" do - click_link "Reply" - expect(page).to have_content "Notification body" - Percy.snapshot(page, name: 'Replying to notification') - - fill_in 'notification_body', with: "Response body" - Percy.snapshot(page, name: "notifications#new") - click_button "Send" - - expect(page).to have_content "Message was successfully sent" - end - end - - describe 'pagination' do - before do - FactoryBot.create_list :notification, 34, recipient: recipient - login_as recipient - visit notifications_path - end - - it do - Percy.snapshot(page, name: "notifications#index") - end - - it 'has page navigation' do - expect(page).to have_selector 'a[rel="next"]' - end - - it 'paginates at 30 notifications per page' do - expect(page).to have_selector '.message', count: 30 - end - - it 'navigates pages' do - first('a[rel="next"]').click - expect(page).to have_selector '.message', count: 4 - end - end -end diff --git a/spec/features/percy/percy_spec.rb b/spec/features/percy/percy_spec.rb index 7b614d169..70ce97092 100644 --- a/spec/features/percy/percy_spec.rb +++ b/spec/features/percy/percy_spec.rb @@ -235,7 +235,7 @@ rest of the garden. context 'when signed in' do let(:prefix) { 'signed-in' } - before { login_as member } + include_context 'signed in member' include_examples 'visit pages' it 'load my plantings#show' do @@ -325,20 +325,20 @@ rest of the garden. describe 'expand menus' do it 'expands crop menu' do + member.update! login_name: 'percy' visit root_path click_on 'Crops' Percy.snapshot(page, name: "#{prefix}/crops-menu") click_on 'Community' Percy.snapshot(page, name: "#{prefix}/community-menu") - click_on 'percy' + click_on 'percy', class: 'nav-link' Percy.snapshot(page, name: "#{prefix}/member-menu") end end end context 'wrangling crops' do - let(:prefix) { 'crop-wrangler' } - before { login_as crop_wrangler } + include_context 'signed in crop wrangler' let!(:candy) { FactoryBot.create :crop_request, name: 'candy' } it 'crop wrangling page' do @@ -350,11 +350,10 @@ rest of the garden. Percy.snapshot(page, name: 'editing pending crop') end end + context 'admin' do - before do - login_as admin_user - visit admin_path - end + include_context 'signed in admin' + before { visit admin_path } it 'admin page' do Percy.snapshot(page, name: 'Admin') end diff --git a/spec/features/photos/new_photo_spec.rb b/spec/features/photos/new_photo_spec.rb index de0ae5664..d52a1ef43 100644 --- a/spec/features/photos/new_photo_spec.rb +++ b/spec/features/photos/new_photo_spec.rb @@ -1,13 +1,8 @@ require 'rails_helper' describe "new photo page" do - let(:photo) { FactoryBot.create :photo } - context "signed in member" do - let(:member) { FactoryBot.create :member } - - before { login_as member } - + include_context 'signed in member' context "viewing a planting" do let(:planting) { FactoryBot.create :planting, owner: member } diff --git a/spec/features/photos/show_photo_spec.rb b/spec/features/photos/show_photo_spec.rb index 2c027d57d..f7b101c48 100644 --- a/spec/features/photos/show_photo_spec.rb +++ b/spec/features/photos/show_photo_spec.rb @@ -2,10 +2,7 @@ require 'rails_helper' describe "show photo page" do context "signed in member" do - let(:member) { create :member } - - before { login_as member } - + include_context 'signed in member' context "linked to planting" do let(:planting) { create :planting } let(:photo) { create :photo, owner: planting.owner } diff --git a/spec/features/places/searching_a_place_spec.rb b/spec/features/places/searching_a_place_spec.rb index fa64ea568..5b6985322 100644 --- a/spec/features/places/searching_a_place_spec.rb +++ b/spec/features/places/searching_a_place_spec.rb @@ -1,11 +1,11 @@ require "rails_helper" describe "User searches" do - let(:member) { create :member, location: "Philippines" } - let!(:maize) { create :maize } - let(:garden) { create :garden, owner: member } - let!(:seed1) { create :seed, owner: member } - let!(:planting) { create :planting, garden: garden, owner: member, planted_at: Date.parse("2013-3-10") } + let!(:located_member) { create :member, location: "Philippines" } + let!(:maize) { create :maize } + let(:garden) { create :garden, owner: located_member } + let!(:seed1) { create :seed, owner: located_member } + let!(:planting) { create :planting, garden: garden, owner: located_member, planted_at: Date.parse("2013-3-10") } describe "with a valid place" do before do @@ -26,8 +26,9 @@ describe "User searches" do end describe "Nearby plantings, seed, and members" do + include_context 'signed in member' + let(:member) { located_member } before do - login_as member visit places_path search_with "Philippines" end diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index 24925b073..5a262ecbb 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -2,279 +2,277 @@ require "rails_helper" require 'custom_matchers' describe "Planting a crop", :js, :elasticsearch do - let(:member) { FactoryBot.create :member } let!(:maize) { FactoryBot.create :maize } let(:garden) { FactoryBot.create :garden, owner: member } let!(:planting) do FactoryBot.create :planting, garden: garden, owner: member, planted_at: Date.parse("2013-03-10") end - before do - login_as member - visit new_planting_path - end + context 'signed in' do + include_context 'signed in member' + before { visit new_planting_path } - it_behaves_like "crop suggest", "planting" + it_behaves_like "crop suggest", "planting" - it "has the required fields help text" do - expect(page).to have_content "* denotes a required field" - end + it "has the required fields help text" do + expect(page).to have_content "* denotes a required field" + end - describe "displays required and optional fields properly" do - it { expect(page).to have_selector ".required", text: "What did you plant?" } - it { expect(page).to have_selector ".required", text: "Where did you plant it?" } - it { expect(page).to have_selector 'input#planting_planted_at' } - it { expect(page).to have_selector 'input#planting_quantity' } - it { expect(page).to have_selector 'select#planting_planted_from' } - it { expect(page).to have_selector 'select#planting_sunniness' } - it { expect(page).to have_selector 'textarea#planting_description' } - it { expect(page).to have_selector 'input#planting_finished_at' } - end + describe "displays required and optional fields properly" do + it { expect(page).to have_selector ".required", text: "What did you plant?" } + it { expect(page).to have_selector ".required", text: "Where did you plant it?" } + it { expect(page).to have_selector 'input#planting_planted_at' } + it { expect(page).to have_selector 'input#planting_quantity' } + it { expect(page).to have_selector 'select#planting_planted_from' } + it { expect(page).to have_selector 'select#planting_sunniness' } + it { expect(page).to have_selector 'textarea#planting_description' } + it { expect(page).to have_selector 'input#planting_finished_at' } + end - describe "Creating a new planting" do - before do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_planting" do - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "semi-shade", from: "Sun or shade?" - fill_in "Tell us more about it", with: "It's rad." - choose 'Garden' - fill_in "When", with: "2014-06-15" - click_button "Save" + describe "Creating a new planting" do + before do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "semi-shade", from: "Sun or shade?" + fill_in "Tell us more about it", with: "It's rad." + choose 'Garden' + fill_in "When", with: "2014-06-15" + click_button "Save" + end + end + + it { expect(page).to have_content "planting was successfully created" } + end + + describe "Clicking link to owner's profile" do + before do + visit member_plantings_path(member) + click_link "View #{member}'s profile >>" + end + it { expect(current_path).to eq member_path(member) } + end + + describe "Progress bar status on planting creation" do + before do + visit new_planting_path + + @a_past_date = 15.days.ago.strftime("%Y-%m-%d") + @right_now = Time.zone.today.strftime("%Y-%m-%d") + @a_future_date = 1.year.from_now.strftime("%Y-%m-%d") + end + + it "shows that it is not planted yet" do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + choose 'Garden' + fill_in "When", with: @a_future_date + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "semi-shade", from: "Sun or shade?" + fill_in "Tell us more about it", with: "It's rad." + click_button "Save" + end + + expect(page).to have_content "planting was successfully created" + expect(page).to have_content "0%" + end + + it "shows that days before maturity is unknown" do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + choose 'Garden' + fill_in "When", with: @a_past_date + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "semi-shade", from: "Sun or shade?" + fill_in "Tell us more about it", with: "It's rad." + click_button "Save" + end + + expect(page).to have_content "planting was successfully created" + expect(page).not_to have_content "Finished" + expect(page).not_to have_content "Finishes" + end + + it "shows that planting is in progress" do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + choose 'Garden' + fill_in "When", with: @right_now + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "semi-shade", from: "Sun or shade?" + fill_in "When?", with: '2013-03-10' + fill_in "Tell us more about it", with: "It's rad." + fill_in "Finished date", with: @a_future_date + click_button "Save" + end + + expect(page).to have_content "planting was successfully created" + expect(page).not_to have_content "0%" + expect(page).not_to have_content "Finished" + expect(page).not_to have_content "Finishes" + end + + it "shows that planting is 100% complete (no date specified)" do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + choose 'Garden' + fill_in "When", with: @right_now + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "semi-shade", from: "Sun or shade?" + fill_in "Tell us more about it", with: "It's rad." + check "Mark as finished" + click_button "Save" + end + + expect(page).to have_content "planting was successfully created" + expect(page).to have_content "Finished" + end + + it "shows that planting is 100% complete (date specified)" do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + choose 'Garden' + fill_in "When", with: @a_past_date + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "semi-shade", from: "Sun or shade?" + choose 'Garden' + fill_in "Tell us more about it", with: "It's rad." + fill_in "Finished date", with: @right_now + click_button "Save" + end + + expect(page).to have_content "planting was successfully created" + expect(page).to have_content "Finished" end end - it { expect(page).to have_content "planting was successfully created" } - end - - describe "Clicking link to owner's profile" do - before do - visit member_plantings_path(member) - click_link "View #{member}'s profile >>" - end - it { expect(current_path).to eq member_path(member) } - end - - describe "Progress bar status on planting creation" do - before do - login_as member - visit new_planting_path - - @a_past_date = 15.days.ago.strftime("%Y-%m-%d") - @right_now = Time.zone.today.strftime("%Y-%m-%d") - @a_future_date = 1.year.from_now.strftime("%Y-%m-%d") - end - - it "shows that it is not planted yet" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" + it "Planting from crop page" do + visit crop_path(maize) + within '.crop-actions' do + click_link "Plant maize" + end within "form#new_planting" do - choose 'Garden' - fill_in "When", with: @a_future_date - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "semi-shade", from: "Sun or shade?" - fill_in "Tell us more about it", with: "It's rad." - click_button "Save" + expect(page).to have_selector "input[value='maize']" end - expect(page).to have_content "planting was successfully created" - expect(page).to have_content "0%" - end - - it "shows that days before maturity is unknown" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_planting" do - choose 'Garden' - fill_in "When", with: @a_past_date - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "semi-shade", from: "Sun or shade?" - fill_in "Tell us more about it", with: "It's rad." - click_button "Save" - end - - expect(page).to have_content "planting was successfully created" - expect(page).not_to have_content "Finished" - expect(page).not_to have_content "Finishes" - end - - it "shows that planting is in progress" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_planting" do - choose 'Garden' - fill_in "When", with: @right_now - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "semi-shade", from: "Sun or shade?" - fill_in "When?", with: '2013-03-10' - fill_in "Tell us more about it", with: "It's rad." - fill_in "Finished date", with: @a_future_date - click_button "Save" - end - - expect(page).to have_content "planting was successfully created" - expect(page).not_to have_content "0%" - expect(page).not_to have_content "Finished" - expect(page).not_to have_content "Finishes" - end - - it "shows that planting is 100% complete (no date specified)" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_planting" do - choose 'Garden' - fill_in "When", with: @right_now - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "semi-shade", from: "Sun or shade?" - fill_in "Tell us more about it", with: "It's rad." - check "Mark as finished" - click_button "Save" - end - - expect(page).to have_content "planting was successfully created" - expect(page).to have_content "Finished" - end - - it "shows that planting is 100% complete (date specified)" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_planting" do - choose 'Garden' - fill_in "When", with: @a_past_date - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "semi-shade", from: "Sun or shade?" - choose 'Garden' - fill_in "Tell us more about it", with: "It's rad." - fill_in "Finished date", with: @right_now - click_button "Save" - end - - expect(page).to have_content "planting was successfully created" - expect(page).to have_content "Finished" - end - end - - it "Planting from crop page" do - visit crop_path(maize) - within '.crop-actions' do - click_link "Plant maize" - end - within "form#new_planting" do - expect(page).to have_selector "input[value='maize']" - end - - choose(member.gardens.first.name) - click_button "Save" - - expect(page).to have_content "planting was successfully created" - expect(page).to have_content "maize" - end - - it "Editing a planting to add details" do - visit planting_path(planting) - click_link 'Actions' - click_link "Edit" - fill_in "Tell us more about it", with: "Some extra notes" - click_button "Save" - expect(page).to have_content "planting was successfully updated" - end - - it "Editing a planting to fill in the finished date" do - visit planting_path(planting) - expect(page).not_to have_content "Finishes" - # click_link(id: 'planting-actions-button') - click_link 'Actions' - click_link "Edit" - check "finished" - fill_in "Finished date", with: "2015-06-25" - click_button "Save" - expect(page).to have_content "planting was successfully updated" - expect(page).to have_content "Finished" - end - - it "Marking a planting as finished" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - choose(member.gardens.first.name) - within "form#new_planting" do - fill_in "When?", with: "2014-07-01" - check "Mark as finished" - fill_in "Finished date", with: "2014-08-30" - uncheck 'Mark as finished' - end - - # Javascript removes the finished at date when the - # planting is marked unfinished. - expect(find("#planting_finished_at").value).to eq("") - - within "form#new_planting" do - check 'Mark as finished' - end - - # The finished at date was cached in Javascript in - # case the user clicks unfinished accidentally. - expect(find("#planting_finished_at").value).to eq("2014-08-30") - - within "form#new_planting" do + choose(member.gardens.first.name) click_button "Save" + + expect(page).to have_content "planting was successfully created" + expect(page).to have_content "maize" end - expect(page).to have_content "planting was successfully created" - expect(page).to have_content "Finished" - expect(page).to have_content "30 Aug" - # shouldn't be on the page - visit plantings_path - expect(page).not_to have_content "maize" + it "Editing a planting to add details" do + visit planting_path(planting) + click_link 'Actions' + click_link "Edit" + fill_in "Tell us more about it", with: "Some extra notes" + click_button "Save" + expect(page).to have_content "planting was successfully updated" + end - # show all plantings to see this finished planting - visit plantings_path(all: 1) - expect(page).to have_content "maize" - end + it "Editing a planting to fill in the finished date" do + visit planting_path(planting) + expect(page).not_to have_content "Finishes" + # click_link(id: 'planting-actions-button') + click_link 'Actions' + click_link "Edit" + check "finished" + fill_in "Finished date", with: "2015-06-25" + click_button "Save" + expect(page).to have_content "planting was successfully updated" + expect(page).to have_content "Finished" + end - describe "Marking a planting as finished without a date" do - before do + it "Marking a planting as finished" do fill_autocomplete "crop", with: "mai" select_from_autocomplete "maize" + choose(member.gardens.first.name) within "form#new_planting" do - choose member.gardens.first.name + fill_in "When?", with: "2014-07-01" check "Mark as finished" + fill_in "Finished date", with: "2014-08-30" + uncheck 'Mark as finished' + end + + # Javascript removes the finished at date when the + # planting is marked unfinished. + expect(find("#planting_finished_at").value).to eq("") + + within "form#new_planting" do + check 'Mark as finished' + end + + # The finished at date was cached in Javascript in + # case the user clicks unfinished accidentally. + expect(find("#planting_finished_at").value).to eq("2014-08-30") + + within "form#new_planting" do click_button "Save" end + expect(page).to have_content "planting was successfully created" + expect(page).to have_content "Finished" + expect(page).to have_content "30 Aug" + + # shouldn't be on the page + visit plantings_path + expect(page).not_to have_content "maize" + + # show all plantings to see this finished planting + visit plantings_path(all: 1) + expect(page).to have_content "maize" + end + + describe "Marking a planting as finished without a date" do + before do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + choose member.gardens.first.name + check "Mark as finished" + click_button "Save" + end + end + + it { expect(page).to have_content "planting was successfully created" } + it { expect(page).to have_content "Finished" } + end + + describe "Planting sunniness" do + before "shows the a sunny image" do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_planting" do + fill_in "When", with: "2015-10-15" + fill_in "How many?", with: 42 + select "cutting", from: "Planted from" + select "sun", from: "Sun or shade?" + fill_in "Tell us more about it", with: "It's rad." + check "Mark as finished" + click_button "Save" + end + + it { expect(page).to have_css("img[alt='sun']") } + end end - it { expect(page).to have_content "planting was successfully created" } - it { expect(page).to have_content "Finished" } - end + describe "Marking a planting as finished from the show page" do + let(:path) { planting_path(planting) } + let(:link_text) { "Mark as finished" } - describe "Planting sunniness" do - before "shows the a sunny image" do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_planting" do - fill_in "When", with: "2015-10-15" - fill_in "How many?", with: 42 - select "cutting", from: "Planted from" - select "sun", from: "Sun or shade?" - fill_in "Tell us more about it", with: "It's rad." - check "Mark as finished" - click_button "Save" - end - - it { expect(page).to have_css("img[alt='sun']") } + it_behaves_like "append date" end end - - describe "Marking a planting as finished from the show page" do - let(:path) { planting_path(planting) } - let(:link_text) { "Mark as finished" } - - it_behaves_like "append date" - end end diff --git a/spec/features/posts/posting_a_post_spec.rb b/spec/features/posts/posting_a_post_spec.rb index 7467517c5..f1b61bc83 100644 --- a/spec/features/posts/posting_a_post_spec.rb +++ b/spec/features/posts/posting_a_post_spec.rb @@ -1,33 +1,31 @@ require 'rails_helper' describe 'Post a post' do - let(:member) { create :member } + context 'signed in' do + include_context 'signed in member' + before { visit new_post_path } - before do - login_as member - visit new_post_path - end - - it "creating a post" do - fill_in "post_subject", with: "Testing" - fill_in "post_body", with: "This is a sample test" - click_button "Post" - expect(page).to have_content "Post was successfully created" - expect(page).to have_content "Posted by" - end - - context "editing a post" do - let(:existing_post) { create :post, author: member } - - before do - visit edit_post_path(existing_post) + it "creating a post" do + fill_in "post_subject", with: "Testing" + fill_in "post_body", with: "This is a sample test" + click_button "Post" + expect(page).to have_content "Post was successfully created" + expect(page).to have_content "Posted by" end - it "saving edit" do - fill_in "post_subject", with: "Testing Edit" - click_button "Post" - expect(page).to have_content "Post was successfully updated" - expect(page).to have_content "edited at" + context "editing a post" do + let(:existing_post) { create :post, author: member } + + before do + visit edit_post_path(existing_post) + end + + it "saving edit" do + fill_in "post_subject", with: "Testing Edit" + click_button "Post" + expect(page).to have_content "Post was successfully updated" + expect(page).to have_content "edited at" + end end end end diff --git a/spec/features/seeds/adding_seeds_spec.rb b/spec/features/seeds/adding_seeds_spec.rb index ce5ab31b4..dd667bdb7 100644 --- a/spec/features/seeds/adding_seeds_spec.rb +++ b/spec/features/seeds/adding_seeds_spec.rb @@ -2,71 +2,70 @@ require 'rails_helper' require 'custom_matchers' describe "Seeds", :js, :elasticsearch do - let(:member) { create :member } - let!(:maize) { create :maize } + context 'signed in' do + include_context 'signed in member' + let!(:maize) { create :maize } - before do - login_as member - visit new_seed_path - end + before { visit new_seed_path } - it_behaves_like "crop suggest", "seed", "crop" + it_behaves_like "crop suggest", "seed", "crop" - it "has the required fields help text" do - expect(page).to have_content "* denotes a required field" - end - - describe "displays required and optional fields properly" do - it { expect(page).to have_selector ".form-group.required", text: "Crop" } - it { expect(page).to have_selector 'input#seed_quantity' } - it { expect(page).to have_selector 'input#seed_plant_before' } - it { expect(page).to have_selector 'input#seed_days_until_maturity_min' } - it { expect(page).to have_selector 'input#seed_days_until_maturity_max' } - it { expect(page).to have_selector '.form-group.required', text: 'Organic?' } - it { expect(page).to have_selector '.form-group.required', text: 'GMO?' } - it { expect(page).to have_selector '.form-group.required', text: 'Heirloom?' } - it { expect(page).to have_selector 'textarea#seed_description' } - it { expect(page).to have_selector '.form-group.required', text: 'Will trade' } - end - - describe "Adding a new seed", js: true do - before do - fill_autocomplete "crop", with: "mai" - select_from_autocomplete "maize" - within "form#new_seed" do - fill_in "Quantity", with: 42 - fill_in "Plant before", with: "2014-06-15" - fill_in "min", with: 999 - fill_in "max", 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 + it "has the required fields help text" do + expect(page).to have_content "* denotes a required field" end - it { expect(page).to have_content "Successfully added maize seed to your stash" } - it { expect(page).to have_content "Quantity: 42" } - it { expect(page).to have_content "Days until maturity: 999–1999" } - it { expect(page).to have_content "certified organic" } - it { expect(page).to have_content "non-certified GMO-free" } - it { expect(page).to have_content "Heirloom? heirloom" } - it { expect(page).to have_content "It's killer." } - end - - describe "Adding a seed from crop page" do - before do - visit crop_path(maize) - click_link "Add maize seeds to stash" - within "form#new_seed" do - expect(page).to have_selector "input[value='maize']" - click_button "Save" - end + describe "displays required and optional fields properly" do + it { expect(page).to have_selector ".form-group.required", text: "Crop" } + it { expect(page).to have_selector 'input#seed_quantity' } + it { expect(page).to have_selector 'input#seed_plant_before' } + it { expect(page).to have_selector 'input#seed_days_until_maturity_min' } + it { expect(page).to have_selector 'input#seed_days_until_maturity_max' } + it { expect(page).to have_selector '.form-group.required', text: 'Organic?' } + it { expect(page).to have_selector '.form-group.required', text: 'GMO?' } + it { expect(page).to have_selector '.form-group.required', text: 'Heirloom?' } + it { expect(page).to have_selector 'textarea#seed_description' } + it { expect(page).to have_selector '.form-group.required', text: 'Will trade' } end - it { expect(page).to have_content "Successfully added maize seed to your stash" } - it { expect(page).to have_content "maize" } + describe "Adding a new seed", js: true do + before do + fill_autocomplete "crop", with: "mai" + select_from_autocomplete "maize" + within "form#new_seed" do + fill_in "Quantity", with: 42 + fill_in "Plant before", with: "2014-06-15" + fill_in "min", with: 999 + fill_in "max", 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 + end + + it { expect(page).to have_content "Successfully added maize seed to your stash" } + it { expect(page).to have_content "Quantity: 42" } + it { expect(page).to have_content "Days until maturity: 999–1999" } + it { expect(page).to have_content "certified organic" } + it { expect(page).to have_content "non-certified GMO-free" } + it { expect(page).to have_content "Heirloom? heirloom" } + it { expect(page).to have_content "It's killer." } + end + + describe "Adding a seed from crop page" do + before do + visit crop_path(maize) + click_link "Add maize seeds to stash" + within "form#new_seed" do + expect(page).to have_selector "input[value='maize']" + click_button "Save" + end + end + + it { expect(page).to have_content "Successfully added maize seed to your stash" } + it { expect(page).to have_content "maize" } + end end end diff --git a/spec/features/seeds/misc_seeds_spec.rb b/spec/features/seeds/misc_seeds_spec.rb index 620a07823..b2205ee8a 100644 --- a/spec/features/seeds/misc_seeds_spec.rb +++ b/spec/features/seeds/misc_seeds_spec.rb @@ -1,13 +1,8 @@ require 'rails_helper' describe "seeds", js: true do - let(:member) { create :member } - context "signed in user" do - let(:crop) { create :crop } - - before { login_as member } - + include_context 'signed in member' xit "button on index to edit seed" do let!(:seed) { create :seed, owner: member } diff --git a/spec/features/seeds/seed_photos.rb b/spec/features/seeds/seed_photos.rb index d1eacdd7f..6f4d9828c 100644 --- a/spec/features/seeds/seed_photos.rb +++ b/spec/features/seeds/seed_photos.rb @@ -2,45 +2,45 @@ require 'rails_helper' require 'custom_matchers' describe "Seeds", :js do - subject do - login_as member - visit seed_path(seed) - page - end + context 'signed in' do + include_context 'signed in member' + before { visit seed_path(seed) } + subject { page } - let(:member) { FactoryBot.create :member } - let!(:seed) { FactoryBot.create :seed, owner: member } + let(:member) { FactoryBot.create :member } + let!(:seed) { FactoryBot.create :seed, owner: member } - it { is_expected.to have_content 'Add photo' } + it { is_expected.to have_content 'Add photo' } - # context 'no photos' do - # it { is_expected.to have_content 'no photos' } - # end - context 'has one photo' do - before { seed.photos = [photo] } + # context 'no photos' do + # it { is_expected.to have_content 'no photos' } + # end + context 'has one photo' do + before { seed.photos = [photo] } - let!(:photo) { FactoryBot.create :photo, title: 'hello photo' } + let!(:photo) { FactoryBot.create :photo, title: 'hello photo' } - it { is_expected.to have_xpath("//img[contains(@src,'#{photo.thumbnail_url}')]") } - it { is_expected.to have_xpath("//a[contains(@href,'#{photo_path(photo)}')]") } - end - - context 'has 50 photos' do - before { seed.photos = photos } - - let!(:photos) { FactoryBot.create_list :photo, 50 } - - it "shows newest photo" do - expect(subject).to have_xpath("//img[contains(@src,'#{photos.last.thumbnail_url}')]") + it { is_expected.to have_xpath("//img[contains(@src,'#{photo.thumbnail_url}')]") } + it { is_expected.to have_xpath("//a[contains(@href,'#{photo_path(photo)}')]") } end - it "links to newest photo" do - expect(subject).to have_xpath("//a[contains(@href,'#{photo_path(photos.last)}')]") - end - it "does not show oldest photo" do - expect(subject).not_to have_xpath("//img[contains(@src,'#{photos.first.thumbnail_url}')]") - end - it "does not link to oldest photo" do - expect(subject).not_to have_xpath("//a[contains(@href,'#{photo_path(photos.first)}')]") + + context 'has 50 photos' do + before { seed.photos = photos } + + let!(:photos) { FactoryBot.create_list :photo, 50 } + + it "shows newest photo" do + expect(subject).to have_xpath("//img[contains(@src,'#{photos.last.thumbnail_url}')]") + end + it "links to newest photo" do + expect(subject).to have_xpath("//a[contains(@href,'#{photo_path(photos.last)}')]") + end + it "does not show oldest photo" do + expect(subject).not_to have_xpath("//img[contains(@src,'#{photos.first.thumbnail_url}')]") + end + it "does not link to oldest photo" do + expect(subject).not_to have_xpath("//a[contains(@href,'#{photo_path(photos.first)}')]") + end end end end diff --git a/spec/features/shared_examples/append_date.rb b/spec/features/shared_examples/append_date.rb index 6f6247736..8dc283c22 100644 --- a/spec/features/shared_examples/append_date.rb +++ b/spec/features/shared_examples/append_date.rb @@ -1,6 +1,5 @@ shared_examples "append date" do let(:this_month) { Time.zone.today.strftime("%b") } - let(:this_year) { Time.zone.today.strftime("%Y") } before { visit path } diff --git a/spec/features/signin_spec.rb b/spec/features/signin_spec.rb index 4be647588..55e31d4b9 100644 --- a/spec/features/signin_spec.rb +++ b/spec/features/signin_spec.rb @@ -4,7 +4,6 @@ describe "signin", js: true do let(:member) { FactoryBot.create :member } let(:recipient) { FactoryBot.create :member } let(:wrangler) { FactoryBot.create :crop_wrangling_member } - let(:notification) { FactoryBot.create :notification, recipient: recipient } def login fill_in 'Login', with: member.login_name @@ -33,9 +32,12 @@ describe "signin", js: true do expect(current_path).to eq root_path end - it "redirect to signin page for if not authenticated to view notification" do - visit notification_path(notification) - expect(current_path).to eq new_member_session_path + describe "redirect to signin page for if not authenticated to view conversations" do + before do + conversation = member.send_message(recipient, 'hey there', 'kiaora') + visit conversation_path(conversation) + end + it { expect(current_path).to eq new_member_session_path } end shared_examples "redirects to what you were trying to do" do @@ -55,11 +57,11 @@ describe "signin", js: true do end end - it "after signin, redirect to new notifications page" do - visit new_notification_path(recipient_id: recipient.id) + it "after signin, redirect to new message page" do + visit new_message_path(recipient_id: recipient.id) expect(current_path).to eq new_member_session_path login - expect(current_path).to eq new_notification_path + expect(current_path).to eq new_message_path end it "after crop wrangler signs in and crops await wrangling, show alert" do diff --git a/spec/features/signout_spec.rb b/spec/features/signout_spec.rb index c94221b1e..d65370ff6 100644 --- a/spec/features/signout_spec.rb +++ b/spec/features/signout_spec.rb @@ -3,8 +3,6 @@ require 'rails_helper' describe "signout" do let(:member) { create :member } - let(:path) {} - it "redirect to previous page after signout" do visit crops_path # some random page click_link 'Sign in' diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb deleted file mode 100644 index 68262fb9e..000000000 --- a/spec/helpers/notifications_helper_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rails_helper' - -describe NotificationsHelper do - describe "reply_link" do - let(:member) { FactoryBot.create(:member) } - - it "replies to PMs with PMs" do - notification = FactoryBot.create(:notification, recipient_id: member.id, post_id: nil) - link = helper.reply_link(notification) - link.should_not be_nil - link.should eq notification_reply_url(notification) - end - - it "replies to post comments with post comments" do - notification = FactoryBot.create(:notification, recipient_id: member.id) - - link = helper.reply_link(notification) - link.should_not be_nil - link.should eq new_comment_url( - post_id: notification.post.id - ) - end - end -end diff --git a/spec/mailers/notifier_spec.rb b/spec/mailers/notifier_spec.rb deleted file mode 100644 index 791137c59..000000000 --- a/spec/mailers/notifier_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -require "rails_helper" - -describe Notifier do - describe "notifications" do - let(:notification) { FactoryBot.create(:notification) } - let(:mail) { Notifier.notify(notification) } - - it 'sets the subject correctly' do - mail.subject.should == notification.subject - end - - it 'comes from noreply@test.growstuff.org' do - mail.from.should == ['noreply@test.growstuff.org'] - end - - it 'sends the mail to the recipient of the notification' do - mail.to.should == [notification.recipient.email] - end - end - - describe "planting reminders" do - let(:member) { FactoryBot.create(:member) } - let(:mail) { Notifier.planting_reminder(member) } - - it 'sets the subject correctly' do - mail.subject.should == "What have you planted lately?" - end - - it 'comes from noreply@test.growstuff.org' do - mail.from.should == ['noreply@test.growstuff.org'] - end - - it 'sends the mail to the recipient of the notification' do - mail.to.should == [member.email] - end - - it 'includes the new planting URL' do - mail.body.encoded.should match new_planting_path - end - - it 'includes the new harvest URL' do - mail.body.encoded.should match new_harvest_path - end - end - - describe "new crop request" do - let(:member) { FactoryBot.create(:crop_wrangling_member) } - let(:crop) { FactoryBot.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@test.growstuff.org' do - mail.from.should == ['noreply@test.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) { FactoryBot.create(:member) } - let(:crop) { FactoryBot.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@test.growstuff.org' do - expect(mail.from).to eq ['noreply@test.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) { FactoryBot.create(:member) } - let(:crop) { FactoryBot.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@test.growstuff.org' do - expect(mail.from).to eq ['noreply@test.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/comment_spec.rb b/spec/models/comment_spec.rb index 687219386..75166df9f 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -14,8 +14,6 @@ describe Comment do end context "notifications" do - let(:comment) { FactoryBot.create(:comment) } - it "sends a notification when a comment is posted" do expect do FactoryBot.create(:comment) diff --git a/spec/models/crop_spec.rb b/spec/models/crop_spec.rb index ed17069c9..4b775b4ef 100644 --- a/spec/models/crop_spec.rb +++ b/spec/models/crop_spec.rb @@ -1,9 +1,6 @@ require 'rails_helper' describe Crop do - let(:pp2) { FactoryBot.create(:plant_part) } - let(:pp1) { FactoryBot.create(:plant_part) } - let(:maize) { FactoryBot.create(:maize) } context 'all fields present' do let(:crop) { FactoryBot.create(:tomato) } @@ -50,7 +47,6 @@ describe Crop do context 'popularity' do let(:tomato) { FactoryBot.create(:tomato) } let(:maize) { FactoryBot.create(:maize) } - let(:cucumber) { FactoryBot.create(:crop, name: 'cucumber') } before do FactoryBot.create_list(:planting, 10, crop: maize) @@ -145,8 +141,7 @@ describe Crop do shared_examples 'has default photo' do it { expect(Crop.has_photos).to include(crop) } end - let!(:crop) { FactoryBot.create :tomato } - let(:member) { FactoryBot.create :member } + let!(:crop) { FactoryBot.create :tomato } context 'with a planting photo' do let!(:photo) { FactoryBot.create(:photo, owner: planting.owner) } @@ -296,7 +291,6 @@ describe Crop do context 'interesting' do subject { Crop.interesting } - let(:photo) { FactoryBot.create :photo } # first, a couple of candidate crops let(:crop1) { FactoryBot.create(:crop) } let(:crop2) { FactoryBot.create(:crop) } @@ -349,13 +343,7 @@ describe Crop do end end - let(:maize) { FactoryBot.create(:maize) } - let(:pp1) { FactoryBot.create(:plant_part) } - let(:pp2) { FactoryBot.create(:plant_part) } - context "harvests" do - let(:h1) { FactoryBot.create(:harvest, crop: maize, plant_part: pp1) } - let(:h2) { FactoryBot.create(:harvest, crop: maize, plant_part: pp2) } let!(:crop) { FactoryBot.create(:crop) } let!(:harvest) { FactoryBot.create(:harvest, crop: crop) } diff --git a/spec/models/garden_spec.rb b/spec/models/garden_spec.rb index c709794a3..a6e0b12fb 100644 --- a/spec/models/garden_spec.rb +++ b/spec/models/garden_spec.rb @@ -3,7 +3,6 @@ require 'rails_helper' describe Garden do let(:owner) { FactoryBot.create(:member, login_name: 'hatupatu') } let(:garden) { FactoryBot.create(:garden, owner: owner, name: 'Springfield Community Garden') } - let(:garden_type) { FactoryBot.create(:garden_type, name: "aquaponic") } it "has a slug" do garden.slug.should match(/hatupatu-springfield-community-garden/) @@ -56,44 +55,6 @@ describe Garden do garden.to_s.should == garden.name end - context "featured plantings" do - let(:tomato) { FactoryBot.create(:tomato) } - let(:maize) { FactoryBot.create(:maize) } - let(:chard) { FactoryBot.create(:chard) } - let(:apple) { FactoryBot.create(:apple) } - let(:pear) { FactoryBot.create(:pear) } - let(:walnut) { FactoryBot.create(:walnut) } - - it "fetches < 4 featured plantings if insufficient exist" do - @p1 = FactoryBot.create(:planting, crop: tomato, garden: garden, owner: garden.owner) - @p2 = FactoryBot.create(:planting, crop: maize, garden: garden, owner: garden.owner) - - expect(garden.featured_plantings).to eq [@p2, @p1] - end - - it "fetches most recent 4 featured plantings" do - @p1 = FactoryBot.create(:planting, crop: tomato, garden: garden, owner: garden.owner) - @p2 = FactoryBot.create(:planting, crop: maize, garden: garden, owner: garden.owner) - @p3 = FactoryBot.create(:planting, crop: chard, garden: garden, owner: garden.owner) - @p4 = FactoryBot.create(:planting, crop: apple, garden: garden, owner: garden.owner) - @p5 = FactoryBot.create(:planting, crop: walnut, garden: garden, owner: garden.owner) - - expect(garden.featured_plantings).to eq [@p5, @p4, @p3, @p2] - end - - it "skips repeated plantings" do - @p1 = FactoryBot.create(:planting, crop: tomato, garden: garden, owner: garden.owner) - @p2 = FactoryBot.create(:planting, crop: maize, garden: garden, owner: garden.owner) - @p3 = FactoryBot.create(:planting, crop: chard, garden: garden, owner: garden.owner) - @p4 = FactoryBot.create(:planting, crop: apple, garden: garden, owner: garden.owner) - @p5 = FactoryBot.create(:planting, crop: walnut, garden: garden, owner: garden.owner) - @p6 = FactoryBot.create(:planting, crop: apple, garden: garden, owner: garden.owner) - @p7 = FactoryBot.create(:planting, crop: pear, garden: garden, owner: garden.owner) - - expect(garden.featured_plantings).to eq [@p7, @p6, @p5, @p3] - end - end - it "destroys plantings when deleted" do garden = FactoryBot.create(:garden, owner: owner) @planting1 = FactoryBot.create(:planting, garden: garden, owner: garden.owner) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 16ad228fb..10ca3f5e6 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -359,7 +359,7 @@ describe 'member' do context "deleted admin member" do let(:member) { FactoryBot.create(:admin_member) } - before { member.destroy } + before { member.discard } context 'crop creator' do let!(:crop) { FactoryBot.create(:crop, creator: member) } diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 330a97003..2a4c7e86a 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -24,20 +24,14 @@ describe Notification do expect(Notification.unread).to include notification end - it "counts unread" do - @who = notification.recipient - @n2 = FactoryBot.create(:notification, recipient: @who, read: false) - expect(@who.notifications.unread_count).to eq 2 - end - it "sends email if asked" do @notification2 = FactoryBot.create(:notification) - @notification2.send_email + @notification2.send_message expect(ActionMailer::Base.deliveries.last&.to).to eq [@notification2.recipient.email] end it "doesn't send email to people who don't want it" do - FactoryBot.create(:no_email_notification).send_email + FactoryBot.create(:no_email_notification).send_message expect(ActionMailer::Base.deliveries.last&.to).not_to eq [notification.recipient.email] end diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index 32d3da008..56e477f4f 100644 --- a/spec/models/planting_spec.rb +++ b/spec/models/planting_spec.rb @@ -5,9 +5,6 @@ describe Planting do let(:garden_owner) { FactoryBot.create(:member, login_name: 'hatupatu') } let(:garden) { FactoryBot.create(:garden, owner: garden_owner, name: 'Springfield Community Garden') } let(:planting) { FactoryBot.create(:planting, crop: crop, garden: garden, owner: garden.owner) } - let(:finished_planting) do - FactoryBot.create :planting, planted_at: 4.days.ago, finished_at: 2.days.ago, finished: true - end describe 'planting lifespan predictions' do context 'no predications data yet' do diff --git a/spec/requests/notifications_spec.rb b/spec/requests/notifications_spec.rb deleted file mode 100644 index 85afbdd9f..000000000 --- a/spec/requests/notifications_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'rails_helper' - -describe "Notifications" do - describe "GET /notifications" do - it "works! (now write some real specs)" do - get notifications_path - # can't see notifications because not logged in - # therefore redirect to homepage - response.status.should be(302) - end - end -end diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb deleted file mode 100644 index bb7335e1b..000000000 --- a/spec/routing/notifications_routing_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require "rails_helper" - -describe NotificationsController do - describe "routing" do - it "routes to #index" do - get("/notifications").should route_to("notifications#index") - end - - it "routes to #new" do - get("/notifications/new").should route_to("notifications#new") - end - - it "routes to #show" do - get("/notifications/1").should route_to("notifications#show", id: "1") - end - - it "routes to #edit" do - get("/notifications/1/edit").should route_to("notifications#edit", id: "1") - end - - it "routes to #create" do - post("/notifications").should route_to("notifications#create") - end - - it "routes to #update" do - put("/notifications/1").should route_to("notifications#update", id: "1") - end - - it "routes to #destroy" do - delete("/notifications/1").should route_to("notifications#destroy", id: "1") - end - end -end diff --git a/spec/support/devise.rb b/spec/support/devise.rb index 0cf56ea83..090ffc6a8 100644 --- a/spec/support/devise.rb +++ b/spec/support/devise.rb @@ -1,4 +1,5 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view + config.include Devise::Test::IntegrationHelpers, type: :feature end diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 857e6b250..0b85fe72a 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -9,6 +9,24 @@ module FeatureHelpers selector = %{ul.ui-autocomplete li.ui-menu-item a:contains("#{select}")} page.execute_script " $('#{selector}').mouseenter().click() " end + + shared_context 'signed in member' do + let(:member) { FactoryBot.create :member } + include_examples 'sign in' + end + shared_context 'signed in crop wrangler' do + let(:member) { FactoryBot.create :crop_wrangling_member } + include_examples 'sign in' + end + shared_context 'signed in admin' do + let(:member) { FactoryBot.create :admin_member } + include_examples 'sign in' + end + + shared_examples 'sign in' do + before { sign_in member } + after { sign_out member } + end end RSpec.configure do |config| diff --git a/spec/views/notifications/index.html.haml_spec.rb b/spec/views/notifications/index.html.haml_spec.rb deleted file mode 100644 index eece9f23e..000000000 --- a/spec/views/notifications/index.html.haml_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' - -describe "notifications/index" do - before do - @member = FactoryBot.create(:member) - controller.stub(:current_user) { @member } - end - - context "ordinary notifications" do - before do - @notification = FactoryBot.create(:notification, sender: @member, - recipient: @member) - assign(:notifications, Kaminari.paginate_array([@notification, @notification]).page(1)) - render - end - - it "renders a list of notifications" do - expect(rendered).to have_content @notification.sender.to_s, count: 2 - expect(rendered).to have_content @notification.subject, count: 2 - end - - it "links to sender's profile" do - assert_select "a", href: member_path(@notification.sender) - end - end - - context "no subject" do - it "shows (no subject)" do - @notification = FactoryBot.create(:notification, - sender: @member, recipient: @member, subject: nil) - assign(:notifications, Kaminari.paginate_array([@notification]).page(1)) - render - rendered.should have_content "(no subject)" - end - end - - context "whitespace-only subject" do - it "shows (no subject)" do - @notification = FactoryBot.create(:notification, - sender: @member, recipient: @member, subject: " ") - assign(:notifications, Kaminari.paginate_array([@notification]).page(1)) - render - rendered.should have_content "(no subject)" - end - end -end diff --git a/spec/views/notifications/new.html.haml_spec.rb b/spec/views/notifications/new.html.haml_spec.rb deleted file mode 100644 index 242f433e3..000000000 --- a/spec/views/notifications/new.html.haml_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'rails_helper' - -describe "notifications/new" do - before do - @recipient = FactoryBot.create(:member) - @sender = FactoryBot.create(:member) - assign(:notification, FactoryBot.create(:notification, recipient_id: @recipient.id, sender_id: @sender.id)) - sign_in @sender - controller.stub(:current_user) { @sender } - end - - it "renders new message form" do - render - assert_select "form", action: notifications_path, method: "notification" do - assert_select "input#notification_subject", name: "notification[subject]" - assert_select "textarea#notification_body", name: "notification[body]" - end - end - - it "tells you who the recipient is" do - render - rendered.should have_content @recipient.login_name - end - - it "puts the recipient in a hidden field" do - render - assert_select "input#notification_recipient_id[type=hidden]", name: "notification[recipient_id]" - end - - it "fills in the subject if provided" do - assign(:subject, 'Foo') - render - assert_select "input#notification_subject", value: "Foo" - end - - it "leaves the subject empty if not provided" do - render - assert_select "input#notification_subject", value: "" - end - - it "Tells you to write your message here" do - render - rendered.should have_content "Type your message here" - end - - it 'shows markdown help' do - render - rendered.should have_content 'Markdown' - end -end diff --git a/spec/views/notifications/show.html.haml_spec.rb b/spec/views/notifications/show.html.haml_spec.rb deleted file mode 100644 index 9f5087f41..000000000 --- a/spec/views/notifications/show.html.haml_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'rails_helper' - -describe "notifications/show" do - before do - @member = FactoryBot.create(:member) - @notification = FactoryBot.create(:notification, recipient: @member) - assign(:notification, @notification) - @reply_link = assign(:reply_link, new_notification_path) - controller.stub(:current_user) { @member } - render - end - - it "renders attributes" do - rendered.should have_content @notification.sender.to_s - rendered.should have_content @notification.body.to_s - end - - it "includes a delete button" do - assert_select "a", "Delete" - end - - it "includes a reply button" do - assert_select "a", { href: @reply_link }, "Reply" - end -end diff --git a/spec/views/notifier/notify.html.haml_spec.rb b/spec/views/notifier/notify.html.haml_spec.rb deleted file mode 100644 index 39b52e603..000000000 --- a/spec/views/notifier/notify.html.haml_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'rails_helper' - -describe 'notifier/notify.html.haml', type: "view" do - before do - @notification = FactoryBot.create(:notification) - @reply_link = "http://example.com" - @signed_message = "EncryptedMessage" - assign(:reply_link, @reply_link) - render - end - - it 'says that you have a message' do - rendered.should have_content 'You have received a message' - end - - it 'includes notification metadata' do - rendered.should have_content @notification.sender.login_name - rendered.should have_content @notification.post.subject - end - - it 'includes a reply link' do - assert_select "a[href='#{@reply_link}']", text: /Reply/ - end - - it 'contains a link to your inbox' do - assert_select "a[href*='notifications']" - end - - it 'has fully qualified URLs' do - # lots of lovely fully qualified URLs - assert_select "a[href^='http']", minimum: 4 - # no relative URLs starting with / - assert_select "a[href^='/']", count: 0 - end -end