diff --git a/app/admin/fund.rb b/app/admin/fund.rb index 6e9ff791..3ec4db95 100644 --- a/app/admin/fund.rb +++ b/app/admin/fund.rb @@ -43,6 +43,9 @@ def find_resource column :open_data column "Geo", :geo_area column "Grants", :grant_count + column "Requests" do |fund| + fund.requests.size + end column :last_updated do |fund| fund.updated_at.strftime("%F") end diff --git a/app/assets/stylesheets/active_admin.sass b/app/assets/stylesheets/active_admin.sass index 59d742dc..29b02172 100644 --- a/app/assets/stylesheets/active_admin.sass +++ b/app/assets/stylesheets/active_admin.sass @@ -16,3 +16,6 @@ // For example, to change the default status-tag color: // // .status_tag { background: #6090DB; } +.col-geo + max-width: 120px + word-wrap: break-word \ No newline at end of file diff --git a/app/assets/stylesheets/v2/modules/form.sass b/app/assets/stylesheets/v2/modules/form.sass index e0bd789f..a14b31d6 100644 --- a/app/assets/stylesheets/v2/modules/form.sass +++ b/app/assets/stylesheets/v2/modules/form.sass @@ -28,6 +28,7 @@ text-transform: capitalize .field_with_errors > input +.field_with_errors > select border-color: map-get($colors, 'red') .label diff --git a/app/cells/progress/eligibility.rb b/app/cells/progress/eligibility.rb index 31b40816..4f0af8a0 100644 --- a/app/cells/progress/eligibility.rb +++ b/app/cells/progress/eligibility.rb @@ -2,6 +2,7 @@ module Progress class Eligibility < Base def initialize(*args) super + return if @fund.stub? @status = @proposal.eligible_status(@fund.slug) end @@ -10,6 +11,7 @@ def label end def indicator + return " #{@position}" if @status.nil? { -1 => 'bg-blue', 0 => 'bg-red', 1 => 'bg-green' }[@status] << " #{@position}" @@ -17,6 +19,8 @@ def indicator def message case @status + when nil + "Missing" when -1 link_to( 'Complete this check', @@ -31,6 +35,7 @@ def message end def highlight + return '' if @status.nil? 'bg-light-blue' unless @status == 1 end end diff --git a/app/cells/progress/request.rb b/app/cells/progress/request.rb new file mode 100644 index 00000000..b307b757 --- /dev/null +++ b/app/cells/progress/request.rb @@ -0,0 +1,27 @@ +module Progress + class Request < Base + def initialize(*args) + super + end + + def label + 'Update fund details' + end + + def indicator + "bg-blue #{@position}" + end + + def message + if @proposal.recipient.requests.find_by(fund_id: @fund.id) + tag.a('Requested', class: 'btn fs15 slate border-silver disabled') + else + link_to('Request', url_helpers.requests_path(fund: @fund), method: :post, class: 'fs15 btn white bg-blue shadow') + end + end + + def highlight + 'bg-light-blue' + end + end +end diff --git a/app/cells/progress/suitability.rb b/app/cells/progress/suitability.rb index 929bb875..dd10c565 100644 --- a/app/cells/progress/suitability.rb +++ b/app/cells/progress/suitability.rb @@ -2,6 +2,7 @@ module Progress class Suitability < Base def initialize(*args) super + return if @fund.stub? @status = @proposal.suitability[@fund.slug] .all_values_for('score') .count { |s| s > 0.2 } @@ -12,7 +13,9 @@ def label end def indicator - "#{@position} " << if @status == 5 + "#{@position} " << if @status.nil? + 'grey' + elsif @status == 5 'bg-green' elsif @status >= 2 'bg-yellow' @@ -22,7 +25,9 @@ def indicator end def message - if @status == 5 + if @status.nil? + "Missing" + elsif @status == 5 link_to('Good', '#suitability', link_opts.merge(class: 'green')) elsif @status >= 2 link_to('Review', '#suitability', link_opts.merge(class: 'yellow')) diff --git a/app/cells/progress_cell.rb b/app/cells/progress_cell.rb index bc4e5856..0898e060 100644 --- a/app/cells/progress_cell.rb +++ b/app/cells/progress_cell.rb @@ -21,10 +21,17 @@ def proposal_summary def steps opts = { proposal: model, fund: options[:fund] } - [ + return [ ::Progress::Eligibility.new(opts.merge(position: 'bot')), ::Progress::Suitability.new(opts.merge(position: 'top bot')), ::Progress::Apply.new(opts.merge(position: 'top')) + ] unless options[:fund].stub? + + [ + ::Progress::Request.new(opts.merge(position: 'bot')), + ::Progress::Eligibility.new(opts.merge(position: 'top bot')), + ::Progress::Suitability.new(opts.merge(position: 'top bot')), + ::Progress::Apply.new(opts.merge(position: 'top')) ] end end diff --git a/app/contexts/fund_context.rb b/app/contexts/fund_context.rb index dd0e3ba7..e44049db 100644 --- a/app/contexts/fund_context.rb +++ b/app/contexts/fund_context.rb @@ -6,4 +6,8 @@ def self.policy_class def featured fund.featured end + + def stub? + fund.stub? + end end diff --git a/app/controllers/funds_controller.rb b/app/controllers/funds_controller.rb index c7dfdb81..50b4880a 100644 --- a/app/controllers/funds_controller.rb +++ b/app/controllers/funds_controller.rb @@ -1,16 +1,20 @@ class FundsController < ApplicationController before_action :ensure_logged_in, :update_legacy_suitability, except: :sources - before_action :query, only: %i[index themed] + before_action :query, :stub_query, only: %i[index themed] def show @fund = Fund.includes(:funder).find_by_hashid(params[:id]) authorize FundContext.new(@fund, @proposal) + render :stub if @fund.stub? end def index query = @query.order_by(@proposal, params[:sort]) @fund_count = query.size @funds = Kaminari.paginate_array(query).page(params[:page]) + + @fund_stubs = @stub_query.order("RANDOM()").limit(5) + end def themed @@ -46,4 +50,10 @@ def query .eligibility(@proposal, params[:eligibility]) .duration(@proposal, params[:duration]) end + + def stub_query + @stub_query = Fund.stubs + .includes(:funder) + .eligibility(@proposal, 'eligible_noquiz') + end end diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb new file mode 100644 index 00000000..53845cb4 --- /dev/null +++ b/app/controllers/requests_controller.rb @@ -0,0 +1,16 @@ +class RequestsController < ApplicationController + before_action :ensure_logged_in + + def create + authorize :request + fund = Fund.find_by_hashid(params[:fund]) + Request.create(fund: fund, recipient: @recipient, message: params[:message]) + redirect_to proposal_fund_path(@proposal, fund) + end + + private + + def user_not_authorised + redirect_to account_upgrade_path(@recipient) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 795ed071..f609e790 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,14 +15,14 @@ def v2_layout? # TODO: remove @ v2 eligibilities: %i[new], enquiries: %i[new], errors: %i[not_found gone internal_server_error], - # feedback: %i[edit new], + feedback: %i[edit new create], funds: %i[index themed show hidden], pages: %i[about faq forfunders privacy terms], # preview? password_resets: %i[new create edit update], proposals: %i[index], # edit new update public_funds: %i[index show themed], recipients: %i[edit update], - # sessions: %i[new], + sessions: %i[new], signup: %i[user create_user], # funder, granted_access, unauthorised # signup_proposals: %i[edit new], # signup_recipients: %i[edit new], diff --git a/app/models/fund.rb b/app/models/fund.rb index b5f73585..01e9af67 100644 --- a/app/models/fund.rb +++ b/app/models/fund.rb @@ -2,6 +2,7 @@ class Fund < ApplicationRecord include ActionView::Helpers::NumberHelper scope :active, -> { where(state: 'active') } + scope :stubs, -> { where(state: 'stub') } scope :newer_than, ->(date) { where('updated_at > ?', date) } scope :recent, -> { order updated_at: :desc } @@ -10,6 +11,7 @@ class Fund < ApplicationRecord belongs_to :funder has_many :enquiries, dependent: :destroy + has_many :requests, dependent: :destroy has_many :fund_themes, dependent: :destroy has_many :themes, through: :fund_themes @@ -93,6 +95,8 @@ def self.order_by(proposal, col) def self.eligibility(proposal, state) case state + when 'eligible_noquiz' + where slug: proposal.eligible_noquiz.keys when 'eligible' where slug: proposal.eligible_funds.keys when 'ineligible' @@ -144,6 +148,10 @@ def tags? tags.count.positive? end + def stub? + %w[stub draft].include? state + end + def key_criteria_html markdown(key_criteria) end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index c72ddd12..26be54ce 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -147,8 +147,14 @@ def initial_recommendation # Check::Suitability::Quiz.new(self, Fund.active), ] ) + check_stub_eligibility = Check::Each.new( + [ + Check::Eligibility::Location.new, + Check::Eligibility::Theme.new, + ] + ) update_columns( - eligibility: check_eligibility.call_each(self, Fund.active), + eligibility: check_stub_eligibility.call_each(self, Fund.stubs).merge(check_eligibility.call_each(self, Fund.active)), suitability: check_suitability.call_each_with_total(self, Fund.active) ) end @@ -162,6 +168,11 @@ def suitable_funds suitability.sort_by { |fund| fund[1]['total'] }.reverse end + def eligible_noquiz + # Same as eligible_funds except doesn't check for the quiz + eligibility.select { |f, fund| fund.all_values_for('eligible').exclude?(false) } + end + def eligible_funds eligibility.select { |f, _| eligible_status(f) == 1 } end diff --git a/app/models/recipient.rb b/app/models/recipient.rb index 61cb527f..f01349ab 100644 --- a/app/models/recipient.rb +++ b/app/models/recipient.rb @@ -26,6 +26,7 @@ class Recipient < ApplicationRecord has_one :subscription, dependent: :destroy has_many :users, as: :organisation, dependent: :destroy has_many :proposals + has_many :requests has_many :countries, -> { distinct }, through: :proposals has_many :districts, -> { distinct }, through: :proposals has_many :answers, as: :category, dependent: :destroy diff --git a/app/models/request.rb b/app/models/request.rb new file mode 100644 index 00000000..e8589c99 --- /dev/null +++ b/app/models/request.rb @@ -0,0 +1,8 @@ +class Request < ApplicationRecord + belongs_to :recipient + belongs_to :fund + + validates :recipient, :fund, presence: true + validates :fund, uniqueness: { scope: :recipient, + message: 'only one request per fund / recipient' } +end diff --git a/app/policies/fund_policy.rb b/app/policies/fund_policy.rb index 2d26f01e..cb09d774 100644 --- a/app/policies/fund_policy.rb +++ b/app/policies/fund_policy.rb @@ -7,7 +7,7 @@ def show? def v1_show? return false unless record.fund && record.proposal - return true if record.fund.featured + return true if record.fund.featured || record.fund.stub? return true if user.subscription_active? record.proposal .suitable_funds @@ -18,7 +18,7 @@ def v1_show? def v2_show? return false unless record - return true if record.featured + return true if record.featured || record.stub? return true if user.subscription_active? user.reveals.include?(record.slug) end diff --git a/app/policies/request_policy.rb b/app/policies/request_policy.rb new file mode 100644 index 00000000..dd927164 --- /dev/null +++ b/app/policies/request_policy.rb @@ -0,0 +1,5 @@ +class RequestPolicy < ApplicationPolicy + def create? + true + end +end diff --git a/app/services/check/eligibility/theme.rb b/app/services/check/eligibility/theme.rb new file mode 100644 index 00000000..ebd11b17 --- /dev/null +++ b/app/services/check/eligibility/theme.rb @@ -0,0 +1,35 @@ +module Check + module Eligibility + class Theme + include Check::Base + + def call(proposal, fund) + validate_call proposal, fund + proposal_themes = get_proposal_themes(proposal) + match_score = match_themes(proposal_themes, fund.themes) + return eligible true if match_score.size > 0 + eligible false + end + + private + + def get_proposal_themes(proposal) + # get all the themes from a proposal, including the related themes attached to each theme + proposal_themes = {} + proposal.themes.each do |theme| + proposal_themes[theme.name] = 1 + theme.related.each do |theme_name, theme_score| + proposal_themes[theme_name] = [theme_score, proposal_themes[theme_name] || 0].max + end + end + proposal_themes + end + + def match_themes(proposal_themes, fund_themes) + # return the themes from the proposal that match those from the fund + fund_theme_names = fund_themes.pluck(:name) + proposal_themes.select { |theme, score| fund_theme_names.include? theme } + end + end + end +end diff --git a/app/views/feedback/edit.html.haml b/app/views/feedback/edit.html.haml index f4de6764..bc4ae4b1 100644 --- a/app/views/feedback/edit.html.haml +++ b/app/views/feedback/edit.html.haml @@ -1,36 +1,38 @@ = content_for :title, 'Coming soon' -.uk-container.uk-container-center - .uk-grid.uk-margin-large.uk-margin-large-top - .uk-width-medium-1-1 - .uk-width-large-1-2.uk-container-center - %h2 Coming soon - %p - Dear - = succeed ',' do - = @current_user.first_name - %p Thank you for using the Beehive Beta! We're working hard to add new features and funders every week. - %p You are seeing this page because we've currently restricted access to some parts of the site. We'll be in touch when we're ready to give access to all areas, but in the meantime please help to shape our strategy by providing some feedback below. - %p Thank you - %p - Suraj - %br - Creator of Beehive - %p P.S. You'll be returned to where you left off once you save your feedback. +%header + = cell :v2_navbar, @current_user + = cell :breadcrumb, 'Feedback' => '' - %hr.thick +%main + .maxw1080.mx-auto.px20 - .uk-width-large-1-2.uk-container-center - .cta.tight - %h4.uk-text-bold Give us some feedback - = simple_form_for @feedback, url: feedback_path, method: :patch do |f| - .uk-form.uk-form-stacked.form - .uk-width-1-1.uk-margin-bottom - %p{style: 'font-size: 14px;'} - How much would you pay - %strong a year - to have unlimited access to all of the funders on Beehive? - = f.input :price, as: :currency, input_html: { class: 'large numeric integer uk-width-1-1', min: 0 }, placeholder: 'e.g. 50' - = f.error :price, class: 'error-message' + .maxw1080.mx-auto.px20.flex.flex-wrap + %aside.perc25.md.px20.mb40 + %h2.light.mb20 Coming soon - = f.submit 'Save feedback', class: 'uk-button uk-button-primary uk-button-large uk-width-1-1' + %section.perc75.md.mb80.px20.fs16.lh18 + %p.mb15 + Dear + = succeed ',' do + = @current_user.first_name + %p.mb15 Thank you for using the Beehive Beta! We're working hard to add new features and funders every week. + %p.mb15 You are seeing this page because we've currently restricted access to some parts of the site. We'll be in touch when we're ready to give access to all areas, but in the meantime please help to shape our strategy by providing some feedback below. + %p.mb15 Thank you + %p.mb15 + Suraj + %br + Creator of Beehive + %p.mb15 P.S. You'll be returned to where you left off once you save your feedback. + + %hr.thick + + .mt40 + = simple_form_for @feedback, url: feedback_path, method: :patch do |f| + %label.label.mb10.bold.lh16 + How much would you pay + %strong a year + to have unlimited access to all of the funders on Beehive? + = f.input :price, as: :currency, input_html: { class: 'input', min: 0 }, placeholder: 'e.g. 50' + + = f.submit 'Save feedback', class: 'button bg-green white caps mt30' diff --git a/app/views/feedback/new.html.haml b/app/views/feedback/new.html.haml index 713ecd27..7a41f286 100644 --- a/app/views/feedback/new.html.haml +++ b/app/views/feedback/new.html.haml @@ -1,58 +1,56 @@ = content_for :title, 'Before you continue' -.uk-container.uk-container-center.uk-margin-large-top.uk-margin-large-bottom - .uk-width-medium-2-3.uk-width-large-1-2.uk-container-center - %h2 Before you continue - %h4 - %strong Take a minute and let us know what you think. - - = simple_form_for @feedback, url: feedback_index_path do |f| - .form.uk-form.uk-form-stacked.uk-panel.uk-panel-box.shadow - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom How suitable were your recommended funders? - = f.input :suitable, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::SUITABLE[n]}" }, input_html: {class: 'large uk-width-1-1'} - = f.error :suitable, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom What was most useful about Beehive? - = f.input :most_useful, as: :select, collection: Feedback::MOST_USEFUL, input_html: {class: 'large uk-width-1-1'} - = f.error :most_useful, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom How likely is it that you would recommend Beehive to a friend or colleague? - = f.input :nps, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::NPS[n]}" }, input_html: {class: 'large uk-width-1-1'} - = f.error :nps, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom How would you feel if Beehive was taken away? - = f.input :taken_away, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::TAKEN_AWAY[n]}" }, input_html: {class: 'large uk-width-1-1'} - = f.error :taken_away, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom Beehive informed my fundraising - = f.input :informs_decision, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::INFORMS_DECISION[n]}" }, input_html: {class: 'large uk-width-1-1'} - = f.error :informs_decision, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom How many funding applications did you submit in the past 12 months? - = f.input :application_frequency, as: :select, collection: Feedback::APP_AND_GRANT_FREQUENCY, input_html: {class: 'large uk-width-1-1'} - = f.error :application_frequency, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom How many grants did you recieve in the past 12 months? - = f.input :grant_frequency, as: :select, collection: Feedback::APP_AND_GRANT_FREQUENCY, input_html: {class: 'large uk-width-1-1'} - = f.error :grant_frequency, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom How often would you like to be informed about new funders and funding rounds? - = f.input :marketing_frequency, as: :select, collection: Feedback::MARKETING_FREQUENCY, input_html: {class: 'large uk-width-1-1'} - = f.error :marketing_frequency, class: 'error-message' - - .uk-width-1-1.uk-margin-bottom - .uk-margin-small-bottom Any other feedback? - = f.input :other, as: :text, input_html: {class: 'uk-width-1-1'} - = f.error :other, class: 'error-message' +%header + = cell :v2_navbar, @current_user + = cell :breadcrumb, 'Feedback' => '' + +%main + .maxw1080.mx-auto.px20 + + .maxw1080.mx-auto.px20.flex.flex-wrap + %aside.perc25.md.px20.mb40 + %h2.light.mb20 Before you continue + %h4.light Take a minute and let us know what you think. + + + %section.perc75.md.mb80.px20.fs14 + = simple_form_for @feedback, url: feedback_index_path do |f| + + .mb40 + %label.label.mb10.bold.lh16 How suitable were your recommended funders? + = f.input :suitable, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::SUITABLE[n]}" }, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 What was most useful about Beehive? + = f.input :most_useful, as: :select, collection: Feedback::MOST_USEFUL, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 How likely is it that you would recommend Beehive to a friend or colleague? + = f.input :nps, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::NPS[n]}" }, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 How would you feel if Beehive was taken away? + = f.input :taken_away, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::TAKEN_AWAY[n]}" }, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 Beehive informed my fundraising + = f.input :informs_decision, as: :select, collection: (0..10).to_a.reverse, label_method: lambda { |n| "#{n} #{Feedback::INFORMS_DECISION[n]}" }, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 How many funding applications did you submit in the past 12 months? + = f.input :application_frequency, as: :select, collection: Feedback::APP_AND_GRANT_FREQUENCY, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 How many grants did you recieve in the past 12 months? + = f.input :grant_frequency, as: :select, collection: Feedback::APP_AND_GRANT_FREQUENCY, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 How often would you like to be informed about new funders and funding rounds? + = f.input :marketing_frequency, as: :select, collection: Feedback::MARKETING_FREQUENCY, input_html: {class: 'chosen-select'} + + .mb40 + %label.label.mb10.bold.lh16 Any other feedback? + = f.input :other, as: :text, input_html: {class: 'mb10 p5', rows: 5} .uk-text-right - = f.button :button, 'Submit feedback', class: 'uk-button uk-button-primary uk-button-large uk-width-1-1' + = f.button :button, 'Submit feedback', class: 'button bg-green white caps' diff --git a/app/views/funds/index.haml b/app/views/funds/index.haml index c00aa22f..a2d7a0f2 100644 --- a/app/views/funds/index.haml +++ b/app/views/funds/index.haml @@ -38,6 +38,24 @@ .fs15.lh20.caps.bold.white Current Proposal = render partial: 'proposals/card', locals: { proposal: @proposal, context: 'fund' } + + - if @fund_stubs.present? + .mt40 + %h2.mb20.fs18 + Other potential leads + .fs12.lh16.mb20.grey + %p.mb10 + These are funds without the full set of data that + Beehive uses to check eligibility and suitability. + %p.mb10 + They have been selected based on the theme and location + of your proposal.] + %p + You can request more information about these funds. + %ul + - @fund_stubs.each do |f| + %li.mb20.fs14.lh16 + = link_to f.funder.name, proposal_fund_path(@proposal, f), class: "blue" %section.perc75.md.mb80.px20 %h1.mb20 Funds diff --git a/app/views/funds/stub.haml b/app/views/funds/stub.haml new file mode 100644 index 00000000..719cc2a9 --- /dev/null +++ b/app/views/funds/stub.haml @@ -0,0 +1,52 @@ += content_for :title, @fund.title + +:javascript + mixpanel.track('View Fund Page', { "Fund": "#{@fund.name}" }); + mixpanel.identify("#{@current_user.id}"); + +%header + = cell :v2_navbar, @current_user + = cell :breadcrumb, (@proposal.title? ? @proposal.title : 'Current proposal').upcase_first => nil, 'Funds' => proposal_funds_path(@proposal), @fund.funder.name => proposal_fund_path(@proposal, @fund) + +%main + .maxw1080.mx-auto.px40.mt40 + + .flex.flex-wrap.mb40 + %section.perc50.md.mb40.px20 + %h1.mb10= @fund.funder.name + + .mb30.fs18.night.lh25= raw @fund.description_html + + %hr + %h6.my20.slate= cell(:fund_insight, @fund, proposal: @proposal).call(:summary) + %hr.mb30 + + = cell(:fund_insight, @fund, proposal: @proposal).call(:themes) + + %section.perc50.md + = cell(:progress, @proposal, fund: @fund) + + %section.px20.mb80.md + .p20.bg-light-blue.border.border-blue.rounded + %h2.mb20 About this fund + + %p.mb10 + This fund is a "stub". This means we don't have all the information we would usually + need to check your eligibility and suitability. We have selected it based on the theme + and location of your proposal. + + %p.mb10 + You can request that we update this fund with the full details. + + = link_to 'Request fund update', requests_path(fund: @fund), method: :post, class: 'button white bg-blue shadow caps mt10' + + %h4.mb20.mt20 Data source + + %p.mb10 + The information we have here is generated from data from the Charity Commission for England + and Wales, used under the + = succeed '.' do + = link_to 'Open Government License', 'https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', class: 'blue' + More details are available on our + = link_to 'Beehive Data', 'http://data.beehivegiving.org/sources', class: 'blue' + site. diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 171ce044..6cb1cd47 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -78,58 +78,62 @@ = yield - %footer.bg-silver.border-top.border-lightest-gray - - .maxw1080.mx-auto - .flex.flex-wrap.items-center.slate - .f1.minw250.mx40.py40 - .flex.items-center - %a{ href: root_path, alt: 'Beehive logo' } - #logo-hex - .fs12.ml5 Intelligently connecting non-profits with funders - .f1.minw250.mx40.pt40 - .flex.fs14.flex-wrap - .f1.minw150.pb40 - .bold.mb14 BEEHIVE - %ul - %li.mb10 - = link_to 'About', about_path, class: 'slate' - %li.mb10 - = link_to 'FAQ', faq_path, class: 'slate' - -# %li.mb10 Pricing - %li.mb10 - = link_to 'Research', articles_path, class: 'slate' - %li.mb10 - = link_to 'Beehive for Funders', for_funders_path, class: 'slate' - - .f1.minw150.pb40 - .bold.mb14 FUNDING - = cell(:footer_themes, @current_user, proposal: @proposal) - - .f1.minw150.pb40 - .bold.mb14 RESOURCES - %ul - %li.mb10 - = link_to 'Privacy', privacy_path, class: 'slate' - %li.mb10 - = link_to 'Terms', terms_path, class: 'slate' - -# %li.mb10 Developers - %li.mb10 - = mail_to 'support@beehivegiving.org', 'Contact', class: 'slate' - - .mx40.pb40.fs12.mid-gray.lh16 - © - = Time.zone.today.year - = succeed ';' do - = link_to 'BeehiveGiving.org', root_path, class: 'mid-gray' - Incubated by - = succeed ',' do - = link_to 'CAST', 'http://www.wearecast.org.uk/', target: '_blank', class: 'mid-gray' - Registered Charity No: - = succeed ',' do - = link_to '1161998', 'http://beta.charitycommission.gov.uk/charity-details/?regid=1161998&subid=0', target: '_blank', class: 'mid-gray' - Company No: - = link_to '09544506', 'https://beta.companieshouse.gov.uk/company/09544506', target: '_blank', class: 'mid-gray' + - if content_for?(:footer) + = yield(:footer) + - else + + %footer.bg-ice.border-top.border-silver + + .maxw1080.mx-auto + .flex.flex-wrap.items-center.slate + .f1.minw250.mx40.py40 + .flex.items-center + %a{ href: root_path, alt: 'Beehive logo' } + #logo-hex + .fs12.ml5 Intelligently connecting non-profits with funders + .f1.minw250.mx40.pt40 + .flex.fs14.flex-wrap + .f1.minw150.pb40 + .bold.mb14 BEEHIVE + %ul + %li.mb10 + = link_to 'About', about_path, class: 'slate' + %li.mb10 + = link_to 'FAQ', faq_path, class: 'slate' + -# %li.mb10 Pricing + %li.mb10 + = link_to 'Research', articles_path, class: 'slate' + %li.mb10 + = link_to 'Beehive for Funders', for_funders_path, class: 'slate' + + .f1.minw150.pb40 + .bold.mb14 FUNDING + = cell(:footer_themes, @current_user, proposal: @proposal) + + .f1.minw150.pb40 + .bold.mb14 RESOURCES + %ul + %li.mb10 + = link_to 'Privacy', privacy_path, class: 'slate' + %li.mb10 + = link_to 'Terms', terms_path, class: 'slate' + -# %li.mb10 Developers + %li.mb10 + = mail_to 'support@beehivegiving.org', 'Contact', class: 'slate' + + .mx40.pb40.fs12.grey.lh16 + © + = Time.zone.today.year + = succeed ';' do + = link_to 'BeehiveGiving.org', root_path, class: 'grey' + Incubated by + = succeed ',' do + = link_to 'CAST', 'http://www.wearecast.org.uk/', target: '_blank', class: 'grey' + Registered Charity No: + = succeed ',' do + = link_to '1161998', 'http://beta.charitycommission.gov.uk/charity-details/?regid=1161998&subid=0', target: '_blank', class: 'grey' + Company No: + = link_to '09544506', 'https://beta.companieshouse.gov.uk/company/09544506', target: '_blank', class: 'grey' - else %main diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index e903419d..4584e97d 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -1,44 +1,30 @@ = content_for :title, 'Sign in' -- content_for :sign_in do - :css - html, body { - min-height: 100% !important; - height: 100%; - } - - .full-height.max.background-image - .full-height-content - .uk-container.uk-container-center.uk-margin-large-top.uk-margin-large - - .uk-text-center.uk-margin-bottom - %a.logo-large{ href: root_path, alt: 'Beehive' } - %h3.white.uk-margin-top-remove Intelligently connecting non-profits with funders - .sign-in-container.uk-container-center - - - if flash[:error] - .uk-alert.uk-alert-danger{'data-uk-alert' => ''} - %a.uk-alert-close.uk-close{:href => ''} - %h3.uk-text-bold Oops! - = flash[:error] - - .uk-panel.uk-panel-box#panel.shadow{style: 'border-radius: 3px'} - %h4.uk-text-bold.uk-text-center Sign in to continue - - = form_tag sign_in_path, method: 'post', class: 'uk-panel uk-form form', id: 'sign-in' do - .uk-form-row - = text_field_tag :email, nil, placeholder: 'Email', type: 'email', class: 'large uk-width-1-1' - .uk-form-row{style: 'position: relative;'} - = password_field_tag :password, nil, placeholder: 'Password', type: 'password', class: 'large uk-form-password uk-width-1-1' - %a.uk-form-password-toggle{"data-uk-form-password" => "", :href => ""} Show - .uk-form-row - = button_tag 'Sign in', class: 'uk-width-1-1 uk-button uk-button-large uk-button-primary' - - .uk-form-row.uk-text-small - %label.uk-float-left - = check_box_tag :remember_me, 1, params[:remember_me] - Remember Me - = link_to 'Forgot Password?', new_password_reset_path, class: 'uk-float-right uk-link' - - .uk-text-center.uk-margin-top - = link_to 'Create account', root_path, style: 'font-size: 16px;' +%header#header-offset + #hex + + .maxw1080.mx-auto + .perc33.md.mt120.mx-auto + .center.my40 + = link_to 'Beehive', root_path, id: 'navbar-logo', alt: 'Beehive logo', class: 'white block mx-auto' + + .bg-white.p15.rounded.shadow + + - if flash[:error] + .mb20.p15.bg-red.white.center + = flash[:error] + + = form_tag sign_in_path, method: 'post', class: '', id: 'sign-in' do + = text_field_tag :email, nil, placeholder: 'Email', type: 'email', class: 'input mb10' + = password_field_tag :password, nil, placeholder: 'Password', type: 'password', class: 'input mb10' + = button_tag 'Sign in', class: 'button-wide white bg-olive caps truncate mb10' + %label.gray.mb20 + = check_box_tag :remember_me, 1, params[:remember_me] + Remember me + + .fit.lh20.mb10 + = link_to 'Forgot Password?', new_password_reset_path, class: 'blue fs12 right' + +- content_for :footer do + %div + \ diff --git a/config/routes.rb b/config/routes.rb index 15143c5e..3bc3ca1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ resources :articles, only: [:index, :show] resources :password_resets, except: [:show, :index, :destroy] resources :reveals, only: :create + resources :requests, only: :create get '/funds', to: 'public_funds#index', as: 'public_funds' get '/funds/:id', to: 'public_funds#show', as: 'public_fund' diff --git a/db/migrate/20171121183536_add_requests.rb b/db/migrate/20171121183536_add_requests.rb new file mode 100644 index 00000000..496c9bf9 --- /dev/null +++ b/db/migrate/20171121183536_add_requests.rb @@ -0,0 +1,10 @@ +class AddRequests < ActiveRecord::Migration[5.1] + def change + create_table :requests do |t| + t.references :fund + t.references :recipient + t.column :message, :string + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index be31371c..3fdb9958 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: 20171114103802) do +ActiveRecord::Schema.define(version: 20171121183536) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -419,6 +419,16 @@ t.index ["slug"], name: "index_recipients_on_slug", unique: true end + create_table "requests", force: :cascade do |t| + t.bigint "fund_id" + t.bigint "recipient_id" + t.string "message" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["fund_id"], name: "index_requests_on_fund_id" + t.index ["recipient_id"], name: "index_requests_on_recipient_id" + end + create_table "subscriptions", id: :serial, force: :cascade do |t| t.integer "recipient_id" t.datetime "created_at", null: false diff --git a/spec/cells/progress_cell_spec.rb b/spec/cells/progress_cell_spec.rb index 0ebab7e3..91710d5b 100644 --- a/spec/cells/progress_cell_spec.rb +++ b/spec/cells/progress_cell_spec.rb @@ -47,4 +47,19 @@ expect(subject).to have_text 'Suitability' expect(subject).to have_text 'Apply' end + + context 'fund stub' do + before { fund.state = 'stub' } + + it '#steps' do + expect(subject).to have_text 'Request' + expect(subject).to have_text 'Eligibility' + expect(subject).to have_text 'Suitability' + expect(subject).to have_text 'Apply' + end + + it 'eligibility and suitability missing' do + expect(subject).to have_text 'Missing', count: 2 + end + end end diff --git a/spec/factories/funds.rb b/spec/factories/funds.rb index 5f532ccf..f52087fb 100644 --- a/spec/factories/funds.rb +++ b/spec/factories/funds.rb @@ -102,4 +102,12 @@ end end end + + + factory :fundstub, class: Fund do + funder + sequence(:name) { |n| "Foundation Main Fund Stub #{n}" } + description 'Some description of the fund stub.' + state 'stub' + end end diff --git a/spec/factories/requests.rb b/spec/factories/requests.rb new file mode 100644 index 00000000..6c8a304b --- /dev/null +++ b/spec/factories/requests.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :request do + recipient + fund + end +end diff --git a/spec/features/browse_spec.rb b/spec/features/browse_spec.rb index 4a1c0e8a..629cb63c 100644 --- a/spec/features/browse_spec.rb +++ b/spec/features/browse_spec.rb @@ -4,10 +4,12 @@ before(:each) do @app.seed_test_db .setup_funds(num: 7, open_data: true) + .setup_fund_stubs(num: 5) .create_recipient_with_subscription_v1! .with_user .create_registered_proposal @db = @app.instances + @fund_stubs = @app.instances[:fund_stubs] @proposal = @db[:registered_proposal] @theme = @db[:themes].first @themes = @db[:themes] @@ -25,8 +27,8 @@ scenario 'When I browse the site, there is a list of fund themes in the footer' do - Fund.first.update themes: [@themes.first, @themes.second] - Fund.second.update themes: [@themes.first] + Fund.active.first.update themes: [@themes.first, @themes.second] + Fund.active.second.update themes: [@themes.first] visit root_path expect(page).to have_text 'FUNDING' expect(page).to have_text @themes.first.name @@ -36,14 +38,20 @@ context 'signed in' do before(:each) do - @unsuitable_fund = Fund.first - @low_fund = Fund.find_by(name: 'Awards for All 2') - @top_fund = Fund.last + @unsuitable_fund = Fund.active.first + @low_fund = Fund.active.find_by(name: 'Awards for All 2') + @top_fund = Fund.active.last @recipient = @db[:recipient] @app.sign_in visit root_path end + scenario "Fund stub selection shown on proposal fund page" do + @proposal.update_column(:eligibility, @proposal.eligibility.merge( @fund_stubs.first.slug => {'location': true}) ) + visit proposal_funds_path(@proposal) + expect(page).to have_text @fund_stubs.first.funder.name + end + scenario "When I find a recommended fund I'm interested in, I want to view more details, so I can decide if I want to apply" do @@ -62,7 +70,7 @@ scenario "When I find a funding theme I'm interested in, I want to see similar funds, so I can discover new funding opportunties" do - @proposal.update_column(:suitability, Fund.last.slug => { 'total': 0 }) + @proposal.update_column(:suitability, Fund.active.last.slug => { 'total': 0 }) click_link @theme.name, match: :first expect(current_path) .to eq theme_proposal_funds_path(@proposal, @theme.slug) @@ -105,7 +113,7 @@ def subscribe_and_visit(path) click_link 'Hidden fund' expect(current_path).to eq account_upgrade_path(@recipient) - subscribe_and_visit proposal_fund_path(@proposal, Fund.first) + subscribe_and_visit proposal_fund_path(@proposal, Fund.active.first) end context 'When I view fund a with open data' do diff --git a/spec/models/request_spec.rb b/spec/models/request_spec.rb new file mode 100644 index 00000000..1039d74c --- /dev/null +++ b/spec/models/request_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +describe 'Request' do + before(:each) do + @app.seed_test_db + .setup_funds(num: 2, open_data: true) + .create_recipient + .create_complete_proposal + @db = @app.instances + @proposal = @db[:complete_proposal] + @recipient = @db[:recipient] + @fund = @db[:funds].first + @request = create(:request, recipient: @recipient, fund: @fund) + end + + it 'belongs to recipient' do + expect(@request.recipient).to eq @recipient + end + + it 'belongs to fund' do + expect(@request.fund).to eq @fund + end + + it 'is valid' do + expect(@request).to be_valid + end + + it 'is invalid' do + @request.fund = nil + expect(@request).not_to be_valid + end + + it 'is unique to recipient and fund' do + duplicate = build(:request, recipient: @recipient, fund: @fund) + expect(duplicate).not_to be_valid + + unique = build(:request, recipient: @recipient, fund: @db[:funds].last) + expect(unique).to be_valid + end +end \ No newline at end of file diff --git a/spec/policies/enquiry_policy_spec.rb b/spec/policies/enquiry_policy_spec.rb index 13f8c945..199d2181 100644 --- a/spec/policies/enquiry_policy_spec.rb +++ b/spec/policies/enquiry_policy_spec.rb @@ -10,7 +10,7 @@ let(:eligibility) { {} } let(:suitability) { {} } - let(:fund) { Fund.new(slug: 'fund') } + let(:fund) { Fund.new(slug: 'fund', state: 'active') } let(:proposal) do Proposal.new(eligibility: eligibility, suitability: suitability) end diff --git a/spec/policies/fund_policy_spec.rb b/spec/policies/fund_policy_spec.rb index 49e236f1..84f828e7 100644 --- a/spec/policies/fund_policy_spec.rb +++ b/spec/policies/fund_policy_spec.rb @@ -9,7 +9,7 @@ let(:subscribed) { false } let(:suitability) { {} } - let(:fund) { Fund.new(slug: 'fund') } + let(:fund) { Fund.new(slug: 'fund', state: 'active') } let(:proposal) { Proposal.new(suitability: suitability) } let(:reveals) { [] } let(:user) do diff --git a/spec/policies/request_policy_spec.rb b/spec/policies/request_policy_spec.rb new file mode 100644 index 00000000..57061722 --- /dev/null +++ b/spec/policies/request_policy_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' +require 'pundit/rspec' + +describe RequestPolicy do + subject { described_class } + + let(:subscribed) { false } + let(:fund) { Fund.new(slug: 'fund') } + let(:requests) { [] } + let(:user) do + instance_double( + User, + subscription_active?: subscribed + ) + end + let(:recipient) { Recipient.new() } + + permissions :create? do + it 'not subscribed' do + is_expected.to permit(user, :requests) + end + + context 'subscribed' do + let(:subscribed){ true } + + it 'permit' do + is_expected.to permit(user, :requests) + end + end + end +end diff --git a/spec/services/check/eligibility/theme_spec.rb b/spec/services/check/eligibility/theme_spec.rb new file mode 100644 index 00000000..a680f85f --- /dev/null +++ b/spec/services/check/eligibility/theme_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe Check::Eligibility::Theme do + before(:each) do + @app.seed_test_db.setup_funds.create_recipient.create_registered_proposal + @fund = Fund.last + @proposal = Proposal.last + @t1 = Theme.first + @t2 = Theme.second + @t3 = Theme.third + end + + it '#call suitable' do + @fund.themes = [@t1] + @proposal.themes = [@t1, @t2] + expect(subject.call(@proposal, @fund)).to eq 'eligible' => true + end + + it '#call unsuitable' do + @fund.themes = [] + @proposal.themes = [@t1, @t2] + expect(subject.call(@proposal, @fund)).to eq 'eligible' => false + end + + it 'suitability based on related themes' do + @t2.related = { @t1.name => 0.75 } + @fund.themes = [@t1] + @proposal.themes = [@t2] + expect(subject.call(@proposal, @fund)).to eq 'eligible' => true + end +end diff --git a/spec/support/test_helper.rb b/spec/support/test_helper.rb index ecc6c625..f354eb8b 100644 --- a/spec/support/test_helper.rb +++ b/spec/support/test_helper.rb @@ -55,6 +55,19 @@ def setup_funds(num: 1, save: true, open_data: false, opts: {}) # TODO: refactor self end + def setup_fund_stubs(num: 1, save: true, opts: {}) + FactoryGirl.reload + @funder = create(:funder, name: 'Fund Stub Funder') + @fund_stubs = build_list(:fundstub, num, opts.merge(funder: @funder)) + @fund_stubs.each_with_index do |fund, i| + stub_fund_summary_endpoint(fund.instance_eval { set_slug }) + fund.themes = @themes + fund.geo_area = GeoArea.first + fund.save if save + end + self + end + def create_simple_fund(num: 1) opts = { themes: create_list(:theme, 3), @@ -232,6 +245,7 @@ def instances if @funds instances[:funds] = @funds instances[:funder] = @funder + instances[:fund_stubs] = @fund_stubs end instances end