Merge pull request #107 from houdiniproject/federated_p2p_campaigns

Federated P2P campaigns
This commit is contained in:
Eric Schultz 2018-11-21 14:19:21 -06:00 committed by GitHub
commit 4f27593311
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1010 additions and 318 deletions

3
.gitignore vendored
View file

@ -7,6 +7,7 @@ client/js/nonprofits/donate/plugins-enabled
.DS_Store
/postgres-data
# Ignore bundler config
/.bundle
@ -62,4 +63,4 @@ javascripts/api
!public/css/donate-button.css
!public/css/donate-button.v2.css
!public/svgs
!public/svgs/*
!public/svgs/*

View file

@ -1,6 +1,6 @@
source 'https://rubygems.org'
ruby '2.3.6'
ruby '~> 2.3.6'
gem 'rake'
gem 'rails', '3.2.22.5'
gem 'rails_12factor'
@ -27,7 +27,7 @@ gem 'hamster'
gem 'aws-ses'
gem 'aws-sdk'
# for blocking ip addressses
gem 'rack-attack'

View file

@ -0,0 +1,9 @@
/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */
body .campaign-banner {
margin: auto;
width: 980px;
max-height: 120px;
text-align: center;
overflow: hidden;
}

View file

@ -6,11 +6,14 @@
@import 'components/simple_tabs';
@import 'pikaday';
@import 'common/image_uploader';
@import '../common';
body {
padding: 0;
background: $fog;
}
main {
display: block;
padding: 60px 0 50px 0;

View file

@ -13,6 +13,7 @@
@import 'components/draggable';
@import 'gift_levels';
@import '../../nonprofits/donation_form/form';
@import '../common';
.button--gift {
font-size: 17px;
@ -103,6 +104,43 @@
margin-right: 1px;
}
.campaigner-profile {
display: flex;
flex-wrap: nowrap;
align-items: baseline;
}
.campaigner-profile > figure {
display: flex;
flex-direction: column;
align-items: center;
margin: auto 10px;
align-self: flex-start;
}
.campaigner-profile > figure > .avatar {
flex-basis: 20%;
min-width: 100px;
height: auto;
}
.avatar > img {
clip-path: circle(50% at center);
}
.campaigner-profile > figure > figcaption {
flex-basis: 40%;
padding: 15px;
text-align: left;
}
.campaigner-profile .reason {
flex-basis: 60%;
}
.campaigner-profile .pastelBox-body {
min-height: 100px;
}
@media screen and (max-width: 1000px) {
.box,

View file

@ -19,6 +19,11 @@
width: 192px;
}
.image-upload--banner {
width: 100%;
height: auto;
}
.image-upload span {
margin-top: 30px;
padding: 5px;
@ -28,6 +33,11 @@
@include basicShadow;
}
.image-upload--banner span {
margin-top: 30px;
margin-bottom: 30px;
}
.image-upload--large span {
margin-top: 60px;
}

View file

@ -5,6 +5,7 @@ class CampaignsController < ApplicationController
helper_method :current_campaign_editor?
before_filter :authenticate_confirmed_user!, only: [:create, :name_and_id, :duplicate]
before_filter :authenticate_campaign_editor!, only: [:update, :soft_delete]
before_filter :authenticate_nonprofit_user!, only: [:create_via_template]
before_filter :check_nonprofit_status, only: [:index, :show]
def index
@ -36,23 +37,34 @@ class CampaignsController < ApplicationController
@nonprofit = current_nonprofit
@url = Format::Url.concat(root_url, @campaign.url)
@campaign_background_image = FetchBackgroundImage.with_model(@campaign)
respond_to do |format|
format.html
if @campaign.child_campaign?
@parent_campaign = @campaign.parent_campaign
@peer_to_peer_campaign_param = @parent_campaign.id
else
@peer_to_peer_campaign_param = @campaign.id
end
@campaign_background_image = FetchBackgroundImage.with_model(@campaign)
end
def activities
render json: QueryDonations.for_campaign_activities(params[:id])
@campaign = current_campaign
render json: QueryDonations.for_campaign_activities(@campaign.id)
end
def create
Time.use_zone(current_nonprofit.timezone || 'UTC') do
params[:campaign][:end_datetime] = Chronic.parse(params[:campaign][:end_datetime]) if params[:campaign][:end_datetime].present?
end
campaign = current_nonprofit.campaigns.create params[:campaign]
json_saved campaign, 'Campaign created! Well done.'
if !params[:campaign][:parent_campaign_id]
campaign = current_nonprofit.campaigns.create params[:campaign]
json_saved campaign, 'Campaign created! Well done.'
else
profile_id = params[:campaign][:profile_id]
Profile.find(profile_id).update_attributes params[:profile]
render json: CreatePeerToPeerCampaign.create(params[:campaign], profile_id)
end
end
def update
@ -63,7 +75,6 @@ class CampaignsController < ApplicationController
json_saved current_campaign, 'Successfully updated!'
end
# post 'nonprofits/:np_id/campaigns/:campaign_id/duplicate'
def duplicate
@ -98,7 +109,22 @@ class CampaignsController < ApplicationController
def peer_to_peer
session[:donor_signup_url] = request.env["REQUEST_URI"]
@npo = Nonprofit.find_by_id(params[:npo_id])
@nonprofit = Nonprofit.find_by_id(params[:npo_id])
@parent_campaign = Campaign.find_by_id(params[:campaign_id])
if params[:campaign_id].present? && !@parent_campaign
raise ActionController::RoutingError.new('Not Found')
end
if current_user
@profile = current_user.profile
if (@parent_campaign)
@child_campaign = Campaign.where(
profile_id: @profile.id,
parent_campaign_id: @parent_campaign.id
).first
end
end
end
private
@ -108,5 +134,4 @@ class CampaignsController < ApplicationController
raise ActionController::RoutingError.new('Not Found')
end
end
end

View file

@ -13,6 +13,8 @@ class Campaign < ActiveRecord::Base
:remove_main_image, # for carrierwave
:background_image,
:remove_background_image, #bool carrierwave
:banner_image,
:remove_banner_image,
:published,
:video_url, #str
:vimeo_video_id,
@ -30,7 +32,10 @@ class Campaign < ActiveRecord::Base
:hide_thermometer, #bool
:hide_title, # bool
:receipt_message, # text
:hide_custom_amounts # boolean
:hide_custom_amounts, # boolean
:parent_campaign_id,
:reason_for_supporting,
:default_reason_for_supporting
validate :end_datetime_cannot_be_in_past, :on => :create
validates :profile, :presence => true
@ -47,6 +52,7 @@ class Campaign < ActiveRecord::Base
mount_uploader :main_image, CampaignMainImageUploader
mount_uploader :background_image, CampaignBackgroundImageUploader
mount_uploader :banner_image, CampaignBannerImageUploader
has_many :donations
has_many :charges, through: :donations
@ -61,6 +67,9 @@ class Campaign < ActiveRecord::Base
belongs_to :profile
belongs_to :nonprofit
belongs_to :parent_campaign, class_name: 'Campaign'
has_many :children_campaigns, class_name: 'Campaign', foreign_key: 'parent_campaign_id'
scope :published, -> {where(:published => true)}
scope :active, -> {where(:published => true).where("end_datetime IS NULL OR end_datetime >= ?", Date.today)}
scope :past, -> {where(:published => true).where("end_datetime < ?", Date.today)}
@ -155,4 +164,21 @@ class Campaign < ActiveRecord::Base
(self.end_datetime.to_date - Date.today).to_i
end
def child_params
excluded_for_peer_to_peer = %w(
id created_at updated_at slug profile_id url
total_raised show_recurring_amount external_identifier parent_campaign_id
reason_for_supporting metadata
)
attributes.except(*excluded_for_peer_to_peer)
end
def child_campaign?
parent_campaign.present?
end
def parent_campaign?
!child_campaign?
end
end

View file

@ -0,0 +1,16 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
class CampaignBannerImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
def store_dir
"uploads/campaigns/#{mounted_as}/#{model.id}"
end
def extension_white_list
%w(jpg jpeg png)
end
def cache_dir
"#{Rails.root}/tmp/uploads"
end
end

View file

@ -5,39 +5,42 @@
<p><strong>Manage Campaign</strong></p>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'campaignSettings') -->
<i class='fa fa-gear'></i> Settings
</a>
<a class='button--tiny edit' target='_blank' href="<%= @campaign.url + '/supporters' %>">
<i class='fa fa-th-list'></i> Dashboard
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'offlineCampaignDonation') -->
<i class='fa fa-plus'></i> Offsite Donation
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'manageGiftOptionsModal') -->
<i class='fa fa-gift'></i> Gift Options
<!--= on 'click' (open_modal 'campaignSettings') -->
<i class='fa fa-gear'></i> Settings
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'customReceiptModal') -->
<i class='fa fa-pencil'></i> Custom Receipt
</a>
<% if @campaign.parent_campaign? %>
<a class='button--tiny edit' target='_blank' href="<%= @campaign.url + '/supporters' %>">
<i class='fa fa-th-list'></i> Dashboard
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'offlineCampaignDonation') -->
<i class='fa fa-plus'></i> Offsite Donation
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'manageGiftOptionsModal') -->
<i class='fa fa-gift'></i> Gift Options
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'customReceiptModal') -->
<i class='fa fa-pencil'></i> Custom Receipt
</a>
<% end %>
<a class='button--tiny edit' href='?preview=t'>
<i class='fa fa-eye'></i> Preview Mode
</a>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'duplicateFundraiserModal') -->
<i class='fa fa-copy'></i> Copy Campaign
</a>
<% if @campaign.parent_campaign? %>
<a class='button--tiny edit'>
<!--= on 'click' (open_modal 'duplicateFundraiserModal') -->
<i class='fa fa-copy'></i> Copy Campaign
</a>
<% end %>
<%= render 'components/messages/deleted_campaign_or_event', type: 'campaign' %>

View file

@ -1,6 +1,8 @@
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
<table class='table--striped u-marginBottom--0'>
<% campaigns.each do |campaign |%>
<% campaigns.each do |campaign|%>
<% metric = QueryCampaignMetrics.on_donations(campaign.id) %>
<tr>
<td class='u-padding--0 u-width--200 u-hideIf--400'>
<img src='<%= campaign.main_image_url(:normal) %>'>
@ -18,11 +20,11 @@
<small><%= campaign.summary ? strip_tags(campaign.summary) : strip_tags(campaign.tagline) %></small>
</p>
<% if campaign.show_total_count %>
<span class='pastelBox--white u-padding--3'><%= campaign.donations.count %> <small>supporters</small></span>
<span class='pastelBox--white u-padding--3'><%= metric['supporters_count'] %> <small>supporters</small></span>
<% end %>
<% if campaign.show_total_raised %>
<span class='pastelBox--white u-padding--3'>
<%= print_currency campaign.total_raised, campaign.nonprofit.currency_symbol %> <small>raised</small>
<%= print_currency metric['total_raised'], campaign.nonprofit.currency_symbol %> <small>raised</small>
</span>
<% end %>
</td>

View file

@ -0,0 +1,77 @@
<!-- partial campaigns/new-modal -->
<!-- include 'campaigns/new/index' -->
<div class='modal' id='newPeerToPeerCampaign'>
<%= render 'common/modal_header', title: @parent_campaign.name %>
<div class='wizard-steps' style='display:none;'>
<!--= wizard.init 'new_p2p_campaign_wiz' -->
<%= render 'components/wizard/step_index', wizard_name: 'new_p2p_campaign_wiz' %>
<div class='modal-body' style='display: table;'>
<div class='wizard-step reason-step'>
<!--= wizard.set_step 'new_p2p_campaign_wiz' 'About you' -->
<form parsley-validate>
<!--= on 'submit' (def 'new_p2p_campaign' form_object) (wizard.advance 'new_p2p_campaign_wiz') -->
<input type='hidden' name='campaign[profile_id]' value='<%= current_user.profile.id %>'>
<input type='hidden' name='campaign[parent_campaign_id]' value='<%= @parent_campaign.id %>'>
<p>Personalize your campaign page with your name and photo to greatly increase the success of your campaign.</p>
<div class='layout--three'>
<fieldset>
<label>Name</label>
<input type='text' name='profile[name]' placeholder='Your Name' value='<%= @profile.name %>' required>
</fieldset>
<fieldset>
<label>City</label>
<input type='text' name='profile[city]' placeholder='City' value='<%= @profile.city %>'>
</fieldset>
<fieldset>
<label>State</label>
<%= render 'common/states_dropdown', name: 'profile[state_code]', default: @profile.state_code %>
</fieldset>
</div>
<%= render 'components/forms/submit_button', button_text: 'Next', scope: 'new_p2p_campaign_wiz', branded: true %>
</form>
</div>
<div class='wizard-step amount-step'>
<!--= wizard.set_step 'new_p2p_campaign_wiz' 'Customize' -->
<form parsley-validate>
<!--= on 'submit' create_p2p_campaign -->
<!--= log new_p2p_campaign -->
<div class='u-margin--auto'>
<fieldset class='col-right-12'>
<label>What's your reason for supporting <%= @parent_campaign.name %>?</label>
<p>Your passion is contagious! Inspire giving by telling your network why youre getting involved in this campaign.</p>
<textarea class='u-marginTop--15' rows='4' name='campaign[reason_for_supporting]' required><%= @parent_campaign.default_reason_for_supporting %></textarea>
</fieldset>
<fieldset class='group u-marginBottom--0'>
<label class='u-paddingTop--5'>Goal Amount</label>
<div class='prepend--dollar'>
<input class='input--100 u-marginBottom--5' value='1000' type='number' name='campaign[goal_amount_dollars]' required min='1'>
</div>
</fieldset>
</div>
<%= render 'components/forms/submit_button', button_text: 'Preview Campaign!', scope: 'new_p2p_campaign_wiz', branded: true %>
</form>
</div>
</div>
</div>
</div>
<!-- end partial campaigns/new-modal -->

View file

@ -4,160 +4,205 @@
<%= render 'common/modal_header', title: 'Campaign Settings' %>
<form class='form--flatFields' autosubmit action='/nonprofits/<%=@nonprofit.id%>/campaigns/<%=@campaign.id%>' method='put' data-reload parsley-validate>
<% if @campaign.child_campaign? %>
<div class='modal-body'>
<section class='layout--three'>
<fieldset>
<label>Goal</label>
<div class='prepend--dollar u-inlineBlock'>
<input type='number' parsley-type='number' class='input--150' name='campaign[goal_amount_dollars]' value='<%= sprintf("%.2f", @campaign.goal_amount / 100.0) %>'>
</div>
</fieldset>
</section>
</div>
<% else %>
<div class='modal-body'>
<section class='layout--two'>
<fieldset>
<label>Title </label>
<input type='text' class='u-marginBottom--0' name='campaign[name]' value='<%= @campaign.name %>'>
</fieldset>
<section class='layout--two'>
<fieldset>
<label>Title </label>
<input type='text' class='u-marginBottom--0' name='campaign[name]' value='<%= @campaign.name %>'>
</fieldset>
<fieldset>
<label class='u-block'>Tagline</label>
<input type='text' name='campaign[tagline]' value='<%= @campaign.tagline %>'>
</fieldset>
</section>
<fieldset>
<label class='u-block'>Tagline</label>
<input type='text' name='campaign[tagline]' value='<%= @campaign.tagline %>'>
</fieldset>
</section>
<hr>
<hr>
<section class='layout--three'>
<fieldset>
<label>Goal</label>
<div class='prepend--dollar u-inlineBlock'>
<input type='number' parsley-type='number' class='input--150' name='campaign[goal_amount_dollars]' value='<%= sprintf("%.2f", @campaign.goal_amount / 100.0) %>'>
</div>
</fieldset>
</fieldset>
<section class='layout--three'>
<fieldset>
<label>Goal</label>
<div class='prepend--dollar u-inlineBlock'>
<input type='number' parsley-type='number' class='input--150' name='campaign[goal_amount_dollars]' value='<%= sprintf("%.2f", @campaign.goal_amount / 100.0) %>'>
</div>
</fieldset>
<fieldset>
<label>End Date & Time</label>
<div pikaday-timepicker='MM/DD/YYYY hh:mm a'>
<input class='u-width--200 u-bold u-inlineBlock' type='text' name='campaign[end_datetime]' required parsley-trigger='change' placeholder='MM/DD/YYYY HH:MM' value='<%= Format::Date.full(@campaign.end_datetime, @nonprofit.timezone) %>'>
<a class='button edit u-inlineBlock'>Set</a>
</div>
</fieldset>
</section>
<hr>
<%= render 'components/forms/slug_field', fundraiser: @campaign, url: @nonprofit.url + '/campaigns' %>
<hr>
<section class='layout--three u-overflow--hidden'>
<fieldset>
<label>Main Image <small>(at least 500x400)</small></label>
<p><small>Used for previews and social media</small></p>
<div class='image-upload u-inlineBlock' style='background-image: url("<%= @campaign.main_image_url(:thumb) %>");'>
<span><i class='fa fa-pencil'></i> Edit</span>
<input type='file' name='campaign[main_image]'>
</div>
</fieldset>
<fieldset>
<label>Header Image <small>(1000x600)</small></label>
<p><small>Used at top of campaign page</small></p>
<% if @campaign_background_image %>
<div class='image-upload u-inlineBlock' style='background-image:url(<%= @campaign_background_image %>)'>
<!--= on 'click' (open_modal 'uploadBackgroundImage') -->
<span><i class='fa fa-pencil'></i> Edit</span>
</div>
<% else %>
<a class='button edit'>
<!--= on 'click' (open_modal 'uploadBackgroundImage') -->
<i class='fa fa-plus'></i> Add Image </i>
</a>
<% end %>
</fieldset>
<fieldset>
<label>Campaign Video </label>
<p><small>Videos are optional but recommended</small></p>
<% if @campaign.vimeo_video_id || @campaign.youtube_video_id%>
<% if @campaign.vimeo_video_id %>
<div class='image-upload u-inlineBlock'>
<!--= set_attr 'style' vimeo_image_url -->
<!--= on 'click' (open_modal 'videoModal') -->
<span> <i class='fa fa-pencil'></i> Edit</span>
</div>
<% else%>
<div class='image-upload u-inlineBlock' style="background-image:url('http://img.youtube.com/vi/<%=@campaign.youtube_video_id%>/0.jpg')">
<!--= on 'click' (open_modal 'videoModal') -->
<span> <i class='fa fa-pencil'></i> Edit</span>
</div>
<% end %>
<% else %>
<a class='button edit'><i class='fa fa-plus'></i> Add Video</a>
<!--= on 'click' (open_modal 'videoModal') -->
<% end %>
</fieldset>
</section>
<hr>
<section class='layout--two'>
<fieldset>
<input type='hidden' name='campaign[published]' value='false'>
<input id='campaign-published-input' type='checkbox' name='campaign[published]' <%= @campaign.published ? "checked='checked'" : '' %>>
<label class='u-marginBottom--10' for='campaign-published-input'>Published?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[show_total_raised]'>
<input id='show_total_raised-checkbox' type='checkbox' name='campaign[show_total_raised]' <%= @campaign.show_total_raised ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='show_total_raised-checkbox'>Show total dollars raised?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[show_total_count]'>
<input id='show_total_count-checkbox' type='checkbox' name='campaign[show_total_count]' <%= @campaign.show_total_count ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='show_total_count-checkbox'>Show total supporters?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_goal]'>
<input id='hide_goal-checkbox' type='checkbox' name='campaign[hide_goal]' <%= @campaign.hide_goal ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_goal-checkbox'>Hide goal amount?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_thermometer]'>
<input id='hide_thermometer-checkbox' type='checkbox' name='campaign[hide_thermometer]' <%= @campaign.hide_thermometer ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_thermometer-checkbox'>Hide thermometer?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_activity_feed]'>
<input id='hide_activity_feed-checkbox' type='checkbox' name='campaign[hide_activity_feed]' <%= @campaign.hide_activity_feed ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_activity_feed-checkbox'>Hide activity feed?</label>
</fieldset>
<% if @campaign_background_image %>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_title]'>
<input id='hide_title-checkbox' type='checkbox' name='campaign[hide_title]' <%= @campaign.hide_title ? 'checked="checked"' : '' %>>
<label for='hide_title-checkbox'>Hide main title?</label>
<label>End Date & Time</label>
<div pikaday-timepicker='MM/DD/YYYY hh:mm a'>
<input class='u-width--200 u-bold u-inlineBlock' type='text' name='campaign[end_datetime]' required parsley-trigger='change' placeholder='MM/DD/YYYY HH:MM' value='<%= Format::Date.full(@campaign.end_datetime, @nonprofit.timezone) %>'>
<a class='button edit u-inlineBlock'>Set</a>
</div>
</fieldset>
</section>
<hr>
<%= render 'components/forms/slug_field', fundraiser: @campaign, url: @nonprofit.url + '/campaigns' %>
<hr>
<section class='layout--three u-overflow--hidden'>
<fieldset>
<label>Main Image <small>(at least 500x400)</small></label>
<p><small>Used for previews and social media</small></p>
<div class='image-upload u-inlineBlock' style='background-image: url("<%= @campaign.main_image_url(:thumb) %>");'>
<span><i class='fa fa-pencil'></i> Edit</span>
<input type='file' name='campaign[main_image]'>
</div>
</fieldset>
<fieldset>
<label>Header Image <small>(1000x600)</small></label>
<p><small>Used at top of campaign page</small></p>
<% if @campaign_background_image %>
<div class='image-upload u-inlineBlock' style='background-image:url(<%= @campaign_background_image %>)'>
<!--= on 'click' (open_modal 'uploadBackgroundImage') -->
<span><i class='fa fa-pencil'></i> Edit</span>
</div>
<% else %>
<a class='button edit'>
<!--= on 'click' (open_modal 'uploadBackgroundImage') -->
<i class='fa fa-plus'></i> Add Image </i>
</a>
<% end %>
</fieldset>
<fieldset>
<label>Campaign Video </label>
<p><small>Videos are optional but recommended</small></p>
<% if @campaign.vimeo_video_id || @campaign.youtube_video_id%>
<% if @campaign.vimeo_video_id %>
<div class='image-upload u-inlineBlock'>
<!--= set_attr 'style' vimeo_image_url -->
<!--= on 'click' (open_modal 'videoModal') -->
<span> <i class='fa fa-pencil'></i> Edit</span>
</div>
<% else%>
<div class='image-upload u-inlineBlock' style="background-image:url('http://img.youtube.com/vi/<%=@campaign.youtube_video_id%>/0.jpg')">
<!--= on 'click' (open_modal 'videoModal') -->
<span> <i class='fa fa-pencil'></i> Edit</span>
</div>
<% end %>
<% else %>
<a class='button edit'><i class='fa fa-plus'></i> Add Video</a>
<!--= on 'click' (open_modal 'videoModal') -->
<% end %>
</fieldset>
</section>
<hr>
<section class='layout--one'>
<fieldset>
<label>Banner <small>(1000x120)</small></label>
<p><small>Custom image at the very top of the campaign page</small></p>
<% if @campaign.banner_image_url.present? %>
<div class='image-upload image-upload--banner u-inlineBlock u-marginBottom--15' style='background-image:url(<%= @campaign.banner_image_url %>)'>
<!--= on 'click' (open_modal 'uploadBannerImage') -->
<span><i class='fa fa-pencil'></i> Edit</span>
</div>
<% else %>
<a class='button edit u-marginBottom--15'>
<!--= on 'click' (open_modal 'uploadBannerImage') -->
<i class='fa fa-plus'></i> Add Image </i>
</a>
<% end %>
</fieldset>
</section>
<% if !@campaign.child_campaign? %>
<hr>
<section class='layout--two'>
<fieldset>
<label>Default reason for peer-to-peer campaigns</label>
<p><small>Use it to suggest a good, pre-filled reason for campaigners</small></p>
<textarea class='u-marginTop--15' rows='4' name='campaign[default_reason_for_supporting]'><%= @campaign.default_reason_for_supporting %></textarea>
</fieldset>
</section>
<% end %>
<hr>
<section class='layout--two'>
<fieldset>
<input type='hidden' name='campaign[published]' value='false'>
<input id='campaign-published-input' type='checkbox' name='campaign[published]' <%= @campaign.published ? "checked='checked'" : '' %>>
<label class='u-marginBottom--10' for='campaign-published-input'>Published?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[show_total_raised]'>
<input id='show_total_raised-checkbox' type='checkbox' name='campaign[show_total_raised]' <%= @campaign.show_total_raised ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='show_total_raised-checkbox'>Show total dollars raised?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[show_total_count]'>
<input id='show_total_count-checkbox' type='checkbox' name='campaign[show_total_count]' <%= @campaign.show_total_count ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='show_total_count-checkbox'>Show total supporters?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_goal]'>
<input id='hide_goal-checkbox' type='checkbox' name='campaign[hide_goal]' <%= @campaign.hide_goal ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_goal-checkbox'>Hide goal amount?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_thermometer]'>
<input id='hide_thermometer-checkbox' type='checkbox' name='campaign[hide_thermometer]' <%= @campaign.hide_thermometer ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_thermometer-checkbox'>Hide thermometer?</label>
</fieldset>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_activity_feed]'>
<input id='hide_activity_feed-checkbox' type='checkbox' name='campaign[hide_activity_feed]' <%= @campaign.hide_activity_feed ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_activity_feed-checkbox'>Hide activity feed?</label>
</fieldset>
<% if @campaign_background_image %>
<fieldset>
<input type='hidden' value='false' name='campaign[hide_title]'>
<input id='hide_title-checkbox' type='checkbox' name='campaign[hide_title]' <%= @campaign.hide_title ? 'checked="checked"' : '' %>>
<label for='hide_title-checkbox'>Hide main title?</label>
</fieldset>
<% end %>
<fieldset>
<!--= show_if length gift_options.data -->
<input type='hidden' value='false' name='campaign[hide_custom_amounts]'>
<input id='hide_custom_amounts' type='checkbox' name='campaign[hide_custom_amounts]' <%= @campaign.hide_custom_amounts ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_custom_amounts'>Disallow regular contributions during campaign?</label>
</fieldset>
</section>
<hr>
<fieldset>
<!--= show_if length gift_options.data -->
<input type='hidden' value='false' name='campaign[hide_custom_amounts]'>
<input id='hide_custom_amounts' type='checkbox' name='campaign[hide_custom_amounts]' <%= @campaign.hide_custom_amounts ? 'checked="checked"' : '' %>>
<label class='u-marginBottom--10' for='hide_custom_amounts'>Disallow regular contributions during campaign?</label>
<label>Short Description <small>(used for sharing on social media)</small></label>
<textarea rows='4' name='campaign[summary]'><%= @campaign.summary %></textarea>
</fieldset>
</section>
<div><p class='error'></p></div>
</div>
<hr>
<% end %>
<fieldset>
<label>Short Description <small>(used for sharing on social media)</small></label>
<textarea rows='4' name='campaign[summary]'><%= @campaign.summary %></textarea>
</fieldset>
<div><p class='error'></p></div>
</div>
<footer class='modal-footer u-padding--15 layout--two'>
<span>

View file

@ -10,15 +10,27 @@
<% content_for :javascripts do %>
<script>
<% if @npo %>
appl.def('selected_result', {name: '<%= @npo.name.html_safe %>', id: <%= @npo.id %>})
app.nonprofit_id = <%= @npo.id %>
<% if @nonprofit %>
appl.def('selected_result', {
name: '<%= @nonprofit.name.html_safe %>',
id: <%= @nonprofit.id %>,
modal_id: 'newCampaign'
})
app.nonprofit_id = <%= @nonprofit.id %>
<% elsif @parent_campaign %>
appl.def('selected_result', {
name: '<%= @parent_campaign.nonprofit.name.html_safe %>',
parent_campaign_id: <%= @parent_campaign.id %>,
campaign_name: '<%= @parent_campaign.name %>',
modal_id: 'newPeerToPeerCampaign'
})
app.nonprofit_id = <%= @parent_campaign.nonprofit.id %>
<% end %>
<% if current_user %>
appl.def('is_logged_in', true)
appl.def('is_confirmed', <%= !current_user.confirmed_at.nil? %>)
appl.def('user', {
name: '<%= current_user.profile.name %>',
name: '<%= current_user.profile.name || current_user.email %>',
id: <%= current_user.id %>
})
<% end %>
@ -26,13 +38,15 @@
<%= IncludeAsset.js '/client/js/campaigns/peer_to_peer/page.js' %>
<% end %>
<%= render 'layouts/top_nav' %>
<% if @parent_campaign && @parent_campaign.banner_image_url %>
<div class='campaign-banner u-margin'>
<img src='<%= @parent_campaign.banner_image_url %>'>
</div>
<% end %>
<main>
<header class='u-paddingX--15 container'>
<h1>Start a campaign</h1>
<h4>Start raising funds for your favorite nonprofit in less than 10 minutes.</h4>
</div>
<h1>Start my campaign</h1>
</header>
<hr>
@ -43,19 +57,39 @@
<i class='icon icon-bicycle u-fontSize--50'></i>
</td>
<td>
<h5 class='u-paddingLeft--20'>Below is a roadmap to get you started on your campaign</h5>
<h5 class='u-paddingLeft--20'>Below is a roadmap to get you started with your campaign</h5>
</td>
</tr></table>
<ul class='u-paddingLeft--10 timeline--checklist'>
<li>
<!--= set_attr_if selected_result 'class' 'is-complete' -->
<span class='annot'>10 seconds</span>
<p class='strong u-marginBottom--15'>Search for a nonprofit that you want to fundraise for</p>
<div class='u-paddingLeft--20 u-maxWidth--500'>
<%= render 'search_for_npo' %>
</div>
</li>
<% if @parent_campaign.present? %>
<li>
<!--= set_attr_if selected_result 'class' 'is-complete' -->
<span class='annot'>10 seconds</span>
<p class='strong u-marginBottom--15'>My campaign</p>
<div class='u-paddingLeft--20 u-maxWidth--500'>
<h4><%= @parent_campaign.name %></h4>
</div>
</li>
<% elsif @nonprofit.present?%>
<li>
<!--= set_attr_if selected_result 'class' 'is-complete' -->
<span class='annot'>10 seconds</span>
<p class='strong u-marginBottom--15'>My nonprofit</p>
<div class='u-paddingLeft--20 u-maxWidth--500'>
<h4><%= @nonprofit.name %></h4>
</div>
</li>
<% else %>
<li>
<!--= set_attr_if selected_result 'class' 'is-complete' -->
<span class='annot'>10 seconds</span>
<p class='strong u-marginBottom--15'>Search for a nonprofit that you want to fundraise for</p>
<div class='u-paddingLeft--20 u-maxWidth--500'>
<%= render 'search_for_npo' %>
</div>
</li>
<% end %>
<li>
<!--= set_attr_if is_logged_in 'class' 'is-complete' -->
@ -81,6 +115,8 @@
<p>
<!--= hide_if is_confirmed -->
Simply click the confirmation link in the email we sent you when you created your <%= Settings.general.name %> account.
<br/><br/>
Once you've clicked the confirmation link, come back here and <a href="javascript:window.location.reload(true)">reload this page</a>.
</p>
<p class='pastelBox--green u-padding--5'>
<!--= show_if is_confirmed -->
@ -90,33 +126,44 @@
</li>
<li>
<p class='strong u-marginBottom--15'>Start your campaign</p>
<span class='annot'>5 minutes</span>
<div class='u-paddingLeft--20 u-maxWidth--500'>
<p>Use our simple wizard to create your campaign for
<strong>
<!--= show_if selected_result -->
<!--= put selected_result.name -->.
</strong>
<strong>
<!--= hide_if selected_result -->
your favorite nonprofit.
</strong>
</p>
<p><small>If you need any further help setting up your campaign, check out <a target='_blank' href='/help/peer-to-peer-campaigns-essentials-package-feature'>this handy article</a>.</small></p>
<div class='u-centered u-padding--15'>
<button class='button orange'>
<!--= show_if (all is_logged_in is_confirmed selected_result) -->
<!--= on 'click' (open_modal 'newCampaign') -->
Start Campaign
</button>
<% if !@child_campaign %>
<p class='strong u-marginBottom--15'>Start your campaign</p>
<span class='annot'>5 minutes</span>
<div class='u-paddingLeft--20 u-maxWidth--500'>
<p>Use our simple wizard to create your campaign for
<strong>
<!--= show_if selected_result -->
<!--= put selected_result.name -->.
</strong>
<strong>
<!--= hide_if selected_result -->
your favorite nonprofit.
</strong>
</p>
<p><small>If you need any further help setting up your campaign, check out <a target='_blank' href='/help/peer-to-peer-campaigns-essentials-package-feature'>this handy article</a>.</small></p>
<div class='u-centered u-padding--15'>
<button class='button orange'>
<!--= show_if (all is_logged_in is_confirmed selected_result) -->
<!--= on 'click' (open_modal selected_result.modal_id) -->
Start Campaign
</button>
</div>
</div>
</div>
<% else %>
<p>
Seems like you already have a campaign for that cause! <%= link_to "Click here to see it", @child_campaign.url %>.
</p>
<% end %>
</li>
</ul>
</div>
</main>
<% if current_user %>
<%= render 'campaigns/new_modal' %>
<% if @nonprofit.present? %>
<%= render 'campaigns/new_modal' %>
<% elsif @parent_campaign.present? %>
<%= render 'campaigns/new_peer_to_peer_modal' %>
<% end %>
<% end %>

View file

@ -16,14 +16,14 @@
app.days_remaining = '<%= @campaign.days_left %>'
app.recurring_fund = <%= @campaign.recurring_fund? %>
app.vimeo_id = "<%= @campaign.vimeo_video_id ? @campaign.vimeo_video_id : '' %>"
app.current_campaign_editor = <%= current_campaign_editor? %>
app.current_campaign_editor = <%= current_campaign_editor? %>
app.is_parent_campaign = <%= @campaign.parent_campaign? %>
appl.def('has_video', <%= @campaign.video_url.present? %>)
appl.def('campaign_is_deleted', <%= @campaign.deleted || false %>)
appl.def('has_main_image', <%= @campaign.main_image.file.present? %>)
</script>
<%= render 'schema', campaign: @campaign, url: @url %>
<%= render 'common/froala' if current_campaign_editor? %>
appl.def('has_main_image', <%= @campaign.main_image.file.present? %>)
</script>
<%= render 'schema', campaign: @campaign, url: @url %>
<%= IncludeAsset.js '/client/js/campaigns/show/page.js' %>
<% end %>
@ -64,27 +64,35 @@
image_url: @campaign_background_image,
is_editor: current_campaign_editor?,
hide_title: @campaign.hide_title && @campaign_background_image,
header_content_partial: 'header_content' %>
header_content_partial: 'header_content',
banner_image_url: @campaign.banner_image&.url %>
<%= render 'components/preview_mode_notification' %>
<div class="container u-marginTop--15 <%= @brand_color ? 'is-branded' : '' %>" data-id='<%= @campaign.id %>'>
<section class='box'>
<section class='box'>
<%= render 'campaign_media' %>
</section>
<section class='box-r'>
<% if current_campaign_editor? %>
<% if current_campaign_editor? && !@campaign.child_campaign? %>
<!-- Campaign editor gift option management modal -->
<button class='button edit u-width--full u-marginBottom--15'>
<!--= on 'click' (open_modal 'manageGiftOptionsModal') -->
<i class='fa fa-gift'></i> Manage Gift Options
</button>
<% end %>
<% if !@campaign.child_campaign? %>
<aside class='u-marginBottom--15 pastelBox--grey'>
<a class='button u-width--full' target='_blank' if-branded='background-color, dark' href='/peer-to-peer?campaign_id=<%= @peer_to_peer_campaign_param %>'>
Start Your Own Campaign for <%= @nonprofit.name %>
</a>
</aside>
<% end %>
<!-- flimflam gift options javascript gets rendered into this div: -->
<div class='ff-sidebar'></div>
@ -95,16 +103,22 @@
</div>
</aside>
<% unless current_campaign_editor? %>
<aside class='u-marginTop--15'>
<a class='button u-width--full' target='_blank' if-branded='background-color, dark' href='/peer-to-peer?npo_id=<%=@nonprofit.id %>'>
Start Your Own Campaign for <%= @nonprofit.name %>
</a>
</aside>
<% end %>
</section>
<% if @campaign.child_campaign? %>
<%= render 'components/fundraising_pages/campaigner_profile',
profile: @campaign.profile,
campaign_name: @campaign.name,
reason_for_supporting: @campaign.reason_for_supporting
%>
<section class='box u-centered' style="margin-top:20px; margin-bottom:20px;">
<a class='button--jumbo' target='_blank' if-branded='background-color, dark' href='/peer-to-peer?campaign_id=<%= @peer_to_peer_campaign_param %>'>
Start Your Own Campaign for <%= @nonprofit.name %>
</a>
</section>
<% end %>
<section class='box'>
<div id='js-campaignBody' class='editable' data-path='/nonprofits/<%= @nonprofit.id %>/campaigns/<%= @campaign.id %>.json' data-key='campaign[body]'>
@ -130,6 +144,10 @@
end_point: "/nonprofits/#{@nonprofit.id}/campaigns/#{@campaign.id}",
image_url: @campaign_background_image,
input_name: 'campaign[background_image]' %>
<%= render 'components/upload_banner_image',
end_point: "/nonprofits/#{@nonprofit.id}/campaigns/#{@campaign.id}",
image_url: @campaign.banner_image_url,
input_name: 'campaign[banner_image]' %>
<%= render 'components/custom_receipt_modal',
title: 'Campaign Receipt Message',
type: 'campaign',
@ -141,4 +159,3 @@
<%= render 'components/share_modal', name: @campaign.name, type: 'campaign' %>
<%= render 'common/email_share_modal', fundraiser: @campaign.name, fundraiser_url: @url %>

View file

@ -20,7 +20,7 @@
Upload <%= image_url ? 'Different Image' : '' %>
</button>
<a class='button--small red u-marginTop--10 u-width--full' <%= image_url ? '' : 'disabled' %>>
<!--= on 'click' (confirm 'Are you sure?' remove_this_image -->
<!--= on 'click' (confirm 'Are you sure?' remove_background_image -->
Remove Image
</a>
</div>

View file

@ -0,0 +1,31 @@
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
<div class='modal' id='uploadBannerImage'>
<%= render 'common/modal_header', title: 'Banner Image' %>
<div class='modal-body'>
<fieldset class='u-centered'>
<p>Select and upload a banner image for this page.</p>
<p class='u-marginBottom--20'>For best results, the image should be <strong>980px by 120px</strong> in size.</p>
<form autosubmit action='<%= end_point %>' method='put' data-reload>
<div class='image-upload image-upload--banner' style='background-image:url(<%= image_url ? image_url : '' %>)'>
<span><i class='fa fa-image'></i> Select</span>
<input type='file' name="<%= input_name %>" required>
</div>
<p class='u-color--red'>
<!--= show_if (length image_upload.error) -->
<small><!--= put image_upload.error --></small>
</p>
<div class='u-width--500 u-margin--auto'>
<button type='submit' class='button--small u-width--full u-marginTop--20' data-loading='Uploading...'>
Upload <%= image_url ? 'Different Image' : '' %>
</button>
<a class='button--small red u-marginTop--10 u-width--full' <%= image_url ? '' : 'disabled' %>>
<!--= on 'click' (confirm 'Are you sure?' remove_banner_image ) -->
Remove Image
</a>
</div>
</form>
</fieldset>
</div> <!-- modal-body -->
</div>

View file

@ -0,0 +1,5 @@
<section class='box' style="font-style: italic">
<div class="well" style="padding-left:16px;">
<h4><%= profile.name %> <small>(<%= profile.city %><% if profile.state_code && profile.city %><%= profile.city_state %> <%= profile.state_code %><% end %>)</small></h4>
<blockquote><p>"<%= reason_for_supporting %>"</p></blockquote></div>
</section>

View file

@ -6,6 +6,11 @@
<header class="container fundraisingHeader--plain" <%= css_style.html_safe %> id='js-fundraisingHeader'>
<% if banner_image_url %>
<div class='campaign-banner u-margin'>
<img src='<%= banner_image_url %>'>
</div>
<% end %>
<div class='fundraisingHeader--image-container' id='js-fundraisingHeader-image' style='display: none;'>
<img class='fundraisingHeader--image-aspectRatio' src="<%= asset_path 'graphics/10x6.jpg' %>">
</div>
@ -16,4 +21,4 @@
</section>
<% end %>
</header>
<!-- partial end: fundaising_pages/_header -->
<!-- partial end: fundraising_pages/_header -->

View file

@ -4,7 +4,7 @@
<section class='sideNav-section'>
<a class='sideNav-link' href='<%= profile_url(current_user.profile) %>'>
<% if current_user.profile.picture? %>
<img class='sideNav-profile'src="<%= current_user.profile.get_profile_picture(:tiny) %>')">
<img class='sideNav-profile' src="<%= current_user.profile.get_profile_picture(:tiny) %>">
<% else %>
<i class="sideNav-icon icon-user-1"></i>
<% end %>
@ -46,4 +46,3 @@
</section>
<% end %>

View file

@ -64,7 +64,8 @@
<%= render 'components/fundraising_pages/header',
image_url: @nonprofit_background_image,
is_editor: current_nonprofit_user?,
header_content_partial: 'header_content' %>
header_content_partial: 'header_content',
banner_image_url: nil %>
<%= render 'components/preview_mode_notification' %>

View file

@ -0,0 +1,40 @@
require('../../components/wizard')
var format_err = require('../../common/format_response_error')
appl.def('advance_p2p_campaign_name_step', function(form_obj) {
var name = form_obj['campaign[name]']
appl.def('new_p2p_campaign', form_obj)
appl.wizard.advance('new_p2p_campaign_wiz')
})
// Post a new campaign.
appl.def('create_p2p_campaign', function(el) {
var form_data = utils.toFormData(appl.prev_elem(el))
form_data = utils.mergeFormData(form_data, appl.new_p2p_campaign)
appl.def('new_p2p_campaign_wiz.loading', true)
post_campaign(form_data)
.then(function(req) {
appl.notify("Redirecting to your campaign...")
appl.redirect(JSON.parse(req.response).url)
})
.catch(function(req) {
appl.def('new_p2p_campaign_wiz.loading', false)
appl.def('new_p2p_campaign_wiz.error', req.responseText)
})
})
// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
function post_campaign(form_data) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest()
req.open("POST", '/nonprofits/' + app.nonprofit_id + '/campaigns')
req.setRequestHeader('X-CSRF-Token', window._csrf)
console.log(form_data)
req.send(form_data)
req.onload = function(ev) {
if(req.status === 200) resolve(req)
else reject(req)
}
})
}

View file

@ -20,6 +20,8 @@ appl.def('create_campaign', function(el) {
form_data = utils.mergeFormData(form_data, appl.new_campaign)
appl.def('new_campaign_wiz.loading', true)
// TODO: for p2p capmaigns, merge with preset campaing params
post_campaign(form_data)
.then(function(req) {
appl.notify("Redirecting to your campaign...")

View file

@ -1,13 +1,12 @@
// License: LGPL-3.0-or-later
require('../new/peer_to_peer_wizard')
require('../../common/image_uploader')
require('../new/wizard')
var request = require("../../common/client")
require('../../common/onboard')
// setting up some default values
appl.def('is_signing_up', true)
.def('selected_result_index', -1)
.def('selected_result_index', -1)
appl.def('search_nonprofits', function(value){
@ -23,8 +22,8 @@ appl.def('search_nonprofits', function(value){
// if the the input is empty, it clears the dropdown
if (!value) {
appl.def('search_results', [])
return
appl.def('search_results', [])
return
}
// logic for controlling the dropdown options with up
@ -67,12 +66,12 @@ function setIndexWithArrows(dir) {
if(dir === 'down') {
var search_length = appl.search_results.length -1
appl.def('selected_result_index', appl.selected_result_index === search_length
? search_length
: appl.selected_result_index += 1)
? search_length
: appl.selected_result_index += 1)
} else {
appl.def('selected_result_index', appl.selected_result_index === 0
? 0
: appl.selected_result_index -= 1)
? 0
: appl.selected_result_index -= 1)
}
}

View file

@ -17,10 +17,12 @@ require('../../components/ajax/toggle_soft_delete')(url, 'campaign')
// Initialize the froala wysiwyg
var editable = require('../../common/editable')
editable($('#js-campaignBody'), {
sticky: true,
placeholder: "Add your campaign's story here. We strongly recommend that this section is filled out with at least 250 words. It will be saved automatically as you type. You can add images, videos and custom HTML too."
})
if (app.is_parent_campaign) {
editable($('#js-campaignBody'), {
sticky: true,
placeholder: "Add your campaign's story here. We strongly recommend that this section is filled out with at least 250 words. It will be saved automatically as you type. You can add images, videos and custom HTML too."
})
}
editable($('#js-customReceipt'), {
button: ["bold", "italic", "formatBlock", "align", "createLink",
@ -34,8 +36,17 @@ editable($('#js-customReceipt'), {
var path = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id
appl.def('remove_this_image', function() {
appl.remove_background_image(path, 'campaign')
appl.def('remove_banner_image', function() {
var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id
var notification = 'Removing banner image...'
var payload = {remove_banner_image : true}
appl.remove_image(url, 'campaign', notification, payload)
})
appl.def('remove_background_image', function(url, resource) {
var notification = 'Removing background image...'
var payload = {remove_background_image : true}
appl.remove_image(url, resource, notification, payload)
})
appl.def('count_story_words', function() {

View file

@ -30,7 +30,7 @@ function init(giftsNeedRefresh$, parentState) {
state.giftOptions$ = flyd.mergeAll([
pageloadGifts$
, refreshedGifts$
, flyd.stream([]) // default before ajax loads
, flyd.stream([]) // default before ajax loads
])
return state
}
@ -59,7 +59,7 @@ const giftBox = state => gift => {
, h('p.u-marginBottom--15', gift.description)
, h('div', [ giftButton(state, gift) ])
])
, app.current_campaign_editor // Show edit button only if the current user is a campaign editor
, (app.current_campaign_editor && app.is_parent_campaign) // Show edit button only if the current user is a parent campaign editor
? h('button.button--tiny.absolute.edit.hasShadow', {
on: {click: ev => state.openEditGiftModal$(gift)}
}, [
@ -79,4 +79,3 @@ const totalContributions = gift => {
}
module.exports = {view, init}

View file

@ -7,7 +7,17 @@ module.exports = function(npo_id) {
request.get(campaignsPath).end(function(err, resp){
resp.body.unshift(false)
appl.def('campaigns.data', resp.body)
let campaign_id_names = resp.body.map((i) => {
if (i.isChildCampaign)
{
return {id: i.id, name: i.name + " - " + i.creator}
}
else
{
return {id: i.id, name: i.name}
}
})
appl.def('campaigns.data', campaign_id_names)
})
request.get(eventsPath).end(function(err, resp){
resp.body.unshift(false)

View file

@ -17,10 +17,10 @@ $('.image-upload input').change(function(e) {
}
})
appl.def('remove_background_image', function(url, resource) {
appl.def('remove_image', function(url, resource, notification, payload) {
var data = {}
data[resource] = {remove_background_image: true}
appl.notify('Removing background image...')
data[resource] = payload
appl.notify(notification)
appl.def('loading', true)
$.ajax({
type: 'put',

View file

@ -99,10 +99,10 @@ const init = params$ => {
(ev, params) => {
if(!parent) return
if(params.redirect) parent.postMessage(`commitchange:redirect:${params.redirect}`, '*')
else if(params.mode !== 'embedded'){
else if(params.mode !== 'embedded'){
parent.postMessage('commitchange:close', '*');
} else {
if (window.parent) {window.parent.postMessage('commitchange:close', '*');};
if (window.parent) {window.parent.postMessage('commitchange:close', '*');};
}
}
, state.clickFinish$, state.params$ )

View file

@ -63,7 +63,7 @@ Config.schema do
required(:aws).schema do
# the region your AWS bucket is in
required(:region).filled(:str?)
optional(:region).filled(:str?)
# the name of your aws bucket
required(:bucket).filled(:str?)
@ -295,4 +295,3 @@ Settings.reload!
# Initialize the rails application
Commitchange::Application.initialize!

View file

@ -8,4 +8,3 @@ AWS.config({
s3 = AWS::S3.new
S3Bucket = s3.buckets[Settings.aws.bucket]

View file

@ -4,7 +4,7 @@ CarrierWave.configure do |config|
config.storage = :aws
config.aws_bucket = Settings.aws.bucket
config.aws_acl = :public_read
config.asset_host = Settings.image&.host || "https://s3-#{Settings.aws.region}.amazonaws.com/#{Settings.aws.bucket}"
config.asset_host = Settings.image&.host || "https://#{Settings.aws.bucket}.s3.amazonaws.com"
config.aws_authenticated_url_expiration = 60 * 60 * 24 * 365
config.aws_credentials = {
access_key_id: Settings.aws.access_key_id,

View file

@ -144,6 +144,7 @@ Commitchange::Application.routes.draw do
get(:activities, {on: :member})
put(:soft_delete, {on: :member})
get(:name_and_id, {on: :collection})
post :create_from_template, on: :collection
resources(:campaign_gift_options, {only: [:index, :show, :create, :update, :destroy]}) do
put(:update_order, {on: :collection})
end

View file

@ -20,7 +20,6 @@ default:
aws:
access_key_id: <%= ENV['AWS_ACCESS_KEY'] %>
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
region: test
bucket: <%= ENV['S3_BUCKET_NAME'] %>
mailer:

View file

@ -0,0 +1,5 @@
class AddParentCampaignIdToCampaigns < ActiveRecord::Migration
def change
add_column :campaigns, :parent_campaign_id, :integer
end
end

View file

@ -0,0 +1,5 @@
class AddReasonForSupportingToCampaigns < ActiveRecord::Migration
def change
add_column :campaigns, :reason_for_supporting, :text
end
end

View file

@ -0,0 +1,5 @@
class AddDefaultReasonForSupportingToCampaigns < ActiveRecord::Migration
def change
add_column :campaigns, :default_reason_for_supporting, :text
end
end

View file

@ -0,0 +1,5 @@
class AddBannerImageToCampaigns < ActiveRecord::Migration
def change
add_column :campaigns, :banner_image, :string
end
end

View file

@ -0,0 +1,5 @@
class AddIndexParentCampaignIdToCampaign < ActiveRecord::Migration
def change
add_index :campaigns, :parent_campaign_id
end
end

View file

@ -3,7 +3,7 @@
--
-- Dumped from database version 9.6.5
-- Dumped by pg_dump version 9.6.10
-- Dumped by pg_dump version 9.6.11
SET statement_timeout = 0;
SET lock_timeout = 0;
@ -392,7 +392,11 @@ CREATE TABLE public.campaigns (
hide_custom_amounts boolean,
show_recurring_amount boolean DEFAULT false,
end_datetime timestamp without time zone,
external_identifier character varying(255)
external_identifier character varying(255),
parent_campaign_id integer,
reason_for_supporting text,
default_reason_for_supporting text,
banner_image character varying(255)
);
@ -3143,6 +3147,13 @@ CREATE INDEX index_activities_on_supporter_id ON public.activities USING btree (
CREATE INDEX index_campaign_gifts_on_campaign_gift_option_id ON public.campaign_gifts USING btree (campaign_gift_option_id);
--
-- Name: index_campaigns_on_parent_campaign_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_campaigns_on_parent_campaign_id ON public.campaigns USING btree (parent_campaign_id);
--
-- Name: index_cards_on_id_and_holder_type_and_holder_id_and_inactive; Type: INDEX; Schema: public; Owner: -
--
@ -4337,6 +4348,18 @@ INSERT INTO schema_migrations (version) VALUES ('20180608205049');
INSERT INTO schema_migrations (version) VALUES ('20180608212658');
INSERT INTO schema_migrations (version) VALUES ('20180703165400');
INSERT INTO schema_migrations (version) VALUES ('20180703165401');
INSERT INTO schema_migrations (version) VALUES ('20180703165402');
INSERT INTO schema_migrations (version) VALUES ('20180703165403');
INSERT INTO schema_migrations (version) VALUES ('20180703165404');
INSERT INTO schema_migrations (version) VALUES ('20180703165405');
INSERT INTO schema_migrations (version) VALUES ('20180713213748');
INSERT INTO schema_migrations (version) VALUES ('20180713215825');
@ -4347,3 +4370,5 @@ INSERT INTO schema_migrations (version) VALUES ('20181002160627');
INSERT INTO schema_migrations (version) VALUES ('20181003212559');
INSERT INTO schema_migrations (version) VALUES ('20181120182105');

View file

@ -0,0 +1,39 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
module CreatePeerToPeerCampaign
def self.create(campaign_params, profile_id)
begin
parent_campaign = Campaign.find(campaign_params[:parent_campaign_id])
rescue ActiveRecord::RecordNotFound
return { errors: { parent_campaign_id: 'not found' } }.as_json
end
p2p_params = campaign_params.except(:nonprofit_id, :summary,:goal_amount)
p2p_params.merge!(parent_campaign.child_params)
profile = Profile.find(profile_id)
base_slug = Format::Url.convert_to_slug "#{p2p_params[:name]}-#{profile.name}"
algo = SlugP2pCampaignNamingAlgorithm.new(p2p_params[:nonprofit_id])
p2p_params[:slug] = algo.create_copy_name(base_slug)
campaign = Campaign.create(p2p_params)
campaign.published = true
campaign.profile = profile
campaign.save
campaign.update_attribute(:main_image, parent_campaign.main_image) unless !parent_campaign.main_image rescue AWS::S3::Errors::NoSuchKey
campaign.update_attribute(:background_image, parent_campaign.background_image) unless !parent_campaign.background_image rescue AWS::S3::Errors::NoSuchKey
campaign.update_attribute(:banner_image, parent_campaign.banner_image) unless !parent_campaign.banner_image rescue AWS::S3::Errors::NoSuchKey
return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty?
gift_option_params = []
parent_campaign.campaign_gift_options.each do |option|
excluded_for_peer_to_peer = %w(id campaign_id created_at updated_at)
campaign.campaign_gift_options.create option.attributes.except(*excluded_for_peer_to_peer)
end
campaign.as_json
end
end

View file

@ -2,21 +2,31 @@
module QueryCampaignMetrics
def self.on_donations(campaign_id)
Qx.select(
campaign = Campaign.find(campaign_id)
result = Qx.select(
"COALESCE(COUNT(DISTINCT donations.id), 0) AS supporters_count",
"COALESCE(SUM(payments.gross_amount), 0) AS total_raised",
"campaigns.goal_amount",
"campaigns.show_total_count",
"campaigns.show_total_raised")
"COALESCE(SUM(payments.gross_amount), 0) AS total_raised"
)
.from("campaigns")
.left_join(
.join(
["donations", "donations.campaign_id=campaigns.id"],
["payments", "payments.donation_id=donations.id"]
)
.where("campaigns.id=$id", id: campaign_id)
.group_by('campaigns.id')
.where("campaigns.id IN (#{QueryCampaigns
.get_campaign_and_children(campaign_id)
.parse
})")
.execute
.last
return {
'supporters_count' => result['supporters_count'],
'total_raised'=> result['total_raised'],
'goal_amount'=> campaign.goal_amount,
'show_total_count'=> campaign.show_total_count,
'show_total_raised'=> campaign.show_total_raised
}
end
end

View file

@ -50,7 +50,7 @@ module QueryCampaigns
["payments AS onetime", "onetime.id=payments.id AND onetime.kind='Donation'"],
["payments AS offsite", "offsite.id=payments.id AND offsite.kind='OffsitePayment'"],
["payments AS recurring", "recurring.id=payments.id AND recurring.kind='RecurringDonation'"])
.where("donations.campaign_id" => campaign_id)
.where("donations.campaign_id IN (#{QueryCampaigns.get_campaign_and_children(campaign_id).parse})")
end
@ -62,15 +62,25 @@ module QueryCampaigns
def self.name_and_id(npo_id)
Psql.execute(
Qexpr.new.select(
"campaigns.name",
"campaigns.id")
.from("campaigns")
.where("campaigns.nonprofit_id=$id", id: npo_id)
.where("campaigns.deleted='f' OR campaigns.deleted IS NULL")
.order_by("campaigns.name ASC")
)
np = Nonprofit.find(npo_id)
campaigns = np.campaigns.not_deleted.includes(:profile).order('campaigns.name ASC')
output = campaigns.map do |i|
{
'name' => i.name,
'id' => i.id,
'isChildCampaign' => i.child_campaign?,
'creator' => i.profile&.name || "user ##{i.profile.id}"
}
end
output
end
def self.get_campaign_and_children(campaign_id)
Qx.select("id")
.from('campaigns')
.where("campaigns.id = $id OR campaigns.parent_campaign_id=$id",
id: campaign_id)
end

View file

@ -19,7 +19,9 @@ module QueryDonations
.join(:supporters, "supporters.id=donations.supporter_id")
.left_outer_join(:campaign_gifts, "campaign_gifts.donation_id=donations.id")
.left_outer_join(:campaign_gift_options, "campaign_gift_options.id=campaign_gifts.campaign_gift_option_id")
.where("donations.campaign_id=$id", id: campaign_id)
.where("donations.campaign_id IN (#{QueryCampaigns
.get_campaign_and_children(campaign_id)
.parse})")
.group_by("donations.id", "supporters.id")
.order_by("donations.date")
)
@ -27,7 +29,9 @@ module QueryDonations
def self.for_campaign_activities(id)
QueryDonations.activities_expression(['donations.recurring'])
.where('donations.campaign_id=$id', id: id)
.where("donations.campaign_id IN (#{QueryCampaigns
.get_campaign_and_children(id)
.parse})")
.execute
end

View file

@ -140,7 +140,8 @@ module QueryPayments
inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id])
end
if (query[:campaign_id].present?)
inner_donation_search = inner_donation_search.where('donations.campaign_id=$id', id: query[:campaign_id])
campaign_search = campaign_and_child_query_as_raw_string
inner_donation_search = inner_donation_search.where("donations.campaign_id IN (#{campaign_search})", id: query[:campaign_id])
end
expr = Qexpr.new.select('payments.id').from('payments')
.left_outer_join('supporters', "supporters.id=payments.supporter_id")
@ -191,9 +192,10 @@ module QueryPayments
expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(','))
end
if query[:campaign_id].present?
campaign_search = campaign_and_child_query_as_raw_string
expr = expr
.left_outer_join("campaigns", "campaigns.id=donations.campaign_id" )
.where("campaigns.id=$id", id: query[:campaign_id])
.where("campaigns.id IN (#{campaign_search})", id: query[:campaign_id])
end
if query[:event_id].present?
tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by("payment_id").as("tix")
@ -224,7 +226,8 @@ module QueryPayments
inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id])
end
if (query[:campaign_id].present?)
inner_donation_search = inner_donation_search.where('donations.campaign_id=$id', id: query[:campaign_id])
campaign_search = campaign_and_child_query_as_raw_string
inner_donation_search = inner_donation_search.where("donations.campaign_id IN (#{campaign_search})", id: query[:campaign_id])
end
expr = Qexpr.new.select('payments.id').from('payments')
.left_outer_join('supporters', "supporters.id=payments.supporter_id")
@ -278,9 +281,10 @@ module QueryPayments
expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(','))
end
if query[:campaign_id].present?
campaign_search = campaign_and_child_query_as_raw_string
expr = expr
.left_outer_join("campaigns", "campaigns.id=donations.campaign_id" )
.where("campaigns.id=$id", id: query[:campaign_id])
.where("campaigns.id IN (#{campaign_search})", id: query[:campaign_id])
end
if query[:event_id].present?
tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by("payment_id").as("tix")
@ -446,4 +450,8 @@ module QueryPayments
}
end
def self.campaign_and_child_query_as_raw_string
"SELECT c_temp.id from campaigns c_temp where c_temp.id=$id OR c_temp.parent_campaign_id=$id"
end
end

View file

@ -14,7 +14,9 @@ module QuerySupporters
.left_outer_join('campaign_gifts', 'donations.id=campaign_gifts.donation_id')
.left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id')
.where("supporters.nonprofit_id=$id", id: np_id)
.where("donations.campaign_id=$id", id: campaign_id)
.where("donations.campaign_id IN (#{QueryCampaigns
.get_campaign_and_children(campaign_id)
.parse})")
.group_by('supporters.id')
.order_by('MAX(donations.date) DESC')
@ -253,7 +255,9 @@ module QuerySupporters
expr = expr.and_where("tags.ids @> ARRAY[$tag_ids]", tag_ids: tag_ids)
end
if query[:campaign_id].present?
expr = expr.add_join("donations", "donations.supporter_id=supporters.id AND donations.campaign_id=#{query[:campaign_id].to_i}")
expr = expr.add_join("donations", "donations.supporter_id=supporters.id AND donations.campaign_id IN (#{QueryCampaigns
.get_campaign_and_children(query[:campaign_id].to_i)
.parse})")
end
if query[:event_id].present?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,78 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
require 'rails_helper'
describe CreatePeerToPeerCampaign do
describe '.create' do
let!(:profile) { force_create(:profile, :user => force_create(:user)) }
let!(:parent_campaign) { force_create(:campaign, name: 'Parent campaign') }
context 'on success' do
it 'returns a hash' do
campaign_params = { name: 'Child campaign', parent_campaign_id: parent_campaign.id, goal_amount_dollars: "1000" }
Timecop.freeze(2020, 4, 5) do
result = CreatePeerToPeerCampaign.create(campaign_params, profile.id)
expect(result).to be_kind_of Hash
end
end
it 'returns created peer-to-peer campaign' do
campaign_params = { name: 'Child campaign', parent_campaign_id: parent_campaign.id, goal_amount_dollars: "1000" }
Timecop.freeze(2020, 4, 5) do
result = CreatePeerToPeerCampaign.create(campaign_params, profile.id)
expect(result).not_to include 'errors'
expect(result['parent_campaign_id']).to eq parent_campaign.id
expect(result['created_at']).to eq 'Sun, 05 Apr 2020 00:00:00 UTC +00:00'
end
end
it 'assigns proper slug' do
campaign_params = { name: 'Child campaign', parent_campaign_id: parent_campaign.id, goal_amount_dollars: "1000" }
Timecop.freeze(2020, 4, 5) do
result = CreatePeerToPeerCampaign.create(campaign_params, profile.id)
expect(result).not_to include 'errors'
expect(result['slug']).to eq 'child-campaign_000'
end
end
it 'saves campaign' do
campaign_params = { name: 'Child campaign', parent_campaign_id: parent_campaign.id, goal_amount_dollars: "1000" }
Timecop.freeze(2020, 4, 5) do
expect { CreatePeerToPeerCampaign.create(campaign_params, profile.id) }.to change(Campaign, :count).by 1
end
end
end
context 'on failure' do
it "returns an error if parent campaign can't be found" do
campaign_params = {}
Timecop.freeze(2020, 4, 5) do
result = CreatePeerToPeerCampaign.create(campaign_params, profile.id)
expect(result).to be_kind_of Hash
expect(result['errors']['parent_campaign_id']).to eq 'not found'
end
end
it 'returns a list of error messages for attribute validation' do
campaign_params = { parent_campaign_id: parent_campaign.id }
Timecop.freeze(2020, 4, 5) do
result = CreatePeerToPeerCampaign.create(campaign_params, profile.id)
expect(result).to be_kind_of Hash
expect(result).to include 'errors'
expect(result['errors']['goal_amount']).to match ["can't be blank", 'is not a number']
end
end
it "doesn't save campaign" do
campaign_params = {}
Timecop.freeze(2020, 4, 5) do
expect { CreatePeerToPeerCampaign.create(campaign_params, profile.id) }.not_to change(Campaign, :count)
end
end
end
end
end

View file

@ -114,7 +114,8 @@ describe InsertDuplicate do
expect(result.attributes.with_indifferent_access).to eq(common_result_attributes.merge(
{
id: result.id,
end_datetime: nil
end_datetime: nil,
banner_image: nil
}).with_indifferent_access)
validate_cgo(result)
end

View file

@ -0,0 +1,81 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
require 'rails_helper'
describe QueryCampaignMetrics do
describe 'calculates your metrics plus children' do
let(:nonprofit) {force_create(:nonprofit)}
let(:campaign) {force_create(:campaign, nonprofit:nonprofit, show_total_count:false, show_total_raised: false, goal_amount: 16000)}
let(:campaign_child) {force_create(:campaign, nonprofit:nonprofit, parent_campaign:campaign, show_total_count:true, show_total_raised: true, goal_amount: 8000)}
let(:campaign_child_2) {force_create(:campaign, nonprofit:nonprofit, parent_campaign:campaign, show_total_count:true, show_total_raised: true, goal_amount: 4000 )}
let(:donation) { force_create(:donation, campaign: campaign, amount: 1000)}
let(:payment) { force_create(:payment, donation: donation, gross_amount:1000)}
let(:donation2) { force_create(:donation, campaign: campaign, amount: 2000)}
let(:payment2) { force_create(:payment, donation: donation2, gross_amount:2000)}
let(:donation3) { force_create(:donation, campaign: campaign_child, amount: 4000)}
let(:payment3) { force_create(:payment, donation: donation3, gross_amount:4000)}
let(:donation4) { force_create(:donation, campaign: campaign_child_2, amount: 8000)}
let(:payment4) { force_create(:payment, donation: donation4, gross_amount:8000)}
let(:payments) do
payment
payment2
payment3
payment4
end
let (:campaign_metric) do
payments
QueryCampaignMetrics.on_donations(campaign.id)
end
let(:campaign_child_metric) do
payments
QueryCampaignMetrics.on_donations(campaign_child.id)
end
let(:campaign_child_2_metric) do
payments
QueryCampaignMetrics.on_donations(campaign_child_2.id)
end
it 'campaign metric is valid' do
expect(campaign_metric['supporters_count']).to eq 4
expect(campaign_metric['total_raised']).to eq 15000
expect(campaign_metric['goal_amount']).to eq 16000
expect(campaign_metric['show_total_count']).to eq false
expect(campaign_metric['show_total_raised']).to eq false
end
it 'campaign child metric is valid' do
expect(campaign_child_metric['supporters_count']).to eq 1
expect(campaign_child_metric['total_raised']).to eq 4000
expect(campaign_child_metric['goal_amount']).to eq 8000
expect(campaign_child_metric['show_total_count']).to eq true
expect(campaign_child_metric['show_total_raised']).to eq true
end
it 'campaign child 2metric is valid' do
expect(campaign_child_2_metric['supporters_count']).to eq 1
expect(campaign_child_2_metric['total_raised']).to eq 8000
expect(campaign_child_2_metric['goal_amount']).to eq 4000
expect(campaign_child_2_metric['show_total_count']).to eq true
expect(campaign_child_2_metric['show_total_raised']).to eq true
end
end
end

View file

@ -46,20 +46,8 @@ describe SlugP2pCampaignNamingAlgorithm do
expect(algo.create_copy_name(@name2)).to eq short_slug_copy_today_plus_1
end
it 'errors when at max copies' do
@name = short_slug
@copy_base = copy_base
campaign
campaigns_at_max_copies
expect{ algo.create_copy_name(@name) }.to(raise_error{|error|
expect(error).to be_a ArgumentError
})
campaigns_at_max_copies.each {|i|
expect {algo.create_copy_name(i.slug)}.to(raise_error{|error|
expect(error).to be_a ArgumentError
})
}
it 'has 999 as the max_copies' do
expect(algo.max_copies).to eq 999
end
end
end