Add donations causes

This commit is contained in:
Angus McLeod 2018-09-22 14:03:30 +10:00
parent d686ece85b
commit 45ece34200
19 changed files with 433 additions and 33 deletions

View File

@ -48,6 +48,7 @@ module DiscourseDonations
begin
Rails.logger.debug "Creating a Stripe charge for #{user_params[:amount]}"
opts = {
cause: user_params[:cause],
email: @email,
token: user_params[:stripeToken],
amount: user_params[:amount]
@ -153,7 +154,7 @@ module DiscourseDonations
end
def user_params
params.permit(:user_id, :name, :username, :email, :password, :stripeToken, :type, :amount, :create_account)
params.permit(:user_id, :name, :username, :email, :password, :stripeToken, :cause, :type, :amount, :create_account)
end
def set_user

View File

@ -1 +1,2 @@
load File.expand_path('../regular/donation_user.rb', __FILE__)
load File.expand_path('../scheduled/update_category_donation_statistics.rb', __FILE__)

View File

@ -0,0 +1,69 @@
module Jobs
class UpdateCategoryDonationStatistics < ::Jobs::Scheduled
every 1.hour
def execute(args)
return unless SiteSetting.discourse_donations_cause_category
::Stripe.api_key = SiteSetting.discourse_donations_secret_key
totals = {}
backers = {}
categories = []
raw_charges = ::Stripe::Charge.list(
expand: ['data.invoice.subscription', 'data.customer']
)
raw_charges = raw_charges.is_a?(Object) ? raw_charges['data'] : []
raw_charges.each do |c|
cause_base = c['invoice'] && c['invoice']['subscription'] ? c['invoice']['subscription'] : c
category_id = cause_base['metadata']['discourse_cause'].to_i
backer_base = c['customer']
backer_user_id = backer_base['metadata']['discourse_user_id'].to_i
backer_email = backer_base['email']
if category_id > 0 && Category.exists?(id: category_id)
categories.push(category_id)
current = totals[category_id] || {}
amount = c['amount'].to_i
date = Time.at(c['created']).to_datetime
totals[category_id] ||= {}
totals[category_id][:total] ||= 0
totals[category_id][:month] ||= 0
totals[category_id][:total] += amount
if date.month == Date.today.month
totals[category_id][:month] += amount
end
backers[category_id] ||= []
if backer_user_id > 0 && User.exists?(id: backer_user_id)
backers[category_id].push(backer_user_id) unless backers[category_id].include? backer_user_id
elsif user = User.find_by_email(backer_email)
backers[category_id].push(user.id) unless backers[category_id].include? user.id
end
end
end
categories.each do |category_id|
category = Category.find(category_id)
if totals[category_id]
category.custom_fields['donations_total'] = totals[category_id][:total]
category.custom_fields['donations_month'] = totals[category_id][:month]
end
if backers[category_id]
category.custom_fields['donations_backers'] = backers[category_id]
end
category.save_custom_fields(true)
end
end
end
end

View File

@ -41,7 +41,11 @@ module DiscourseDonations
amount: opts[:amount],
description: @description,
currency: @currency,
receipt_email: customer.email
receipt_email: customer.email,
metadata: {
discourse_cause: opts[:cause],
discourse_user_id: user.id
}
)
@charge
@ -72,7 +76,11 @@ module DiscourseDonations
customer: customer.id,
items: [{
plan: plan_id
}]
}],
metadata: {
discourse_cause: opts[:cause],
discourse_user_id: user.id
}
)
end
@ -186,10 +194,18 @@ module DiscourseDonations
end
if !customer && opts[:create]
customer = ::Stripe::Customer.create(
customer_opts = {
email: opts[:email],
source: opts[:source]
)
}
if user
customer_opts[:metadata] = {
discourse_user_id: user.id
}
end
customer = ::Stripe::Customer.create(customer_opts)
if user
user.custom_fields['stripe_customer_id'] = customer.id

View File

@ -1,8 +1,5 @@
import { ajax } from 'discourse/lib/ajax';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Component.extend({
classNames: 'donation-list',
hasSubscriptions: Ember.computed.notEmpty('subscriptions'),
hasCharges: Ember.computed.notEmpty('charges')
})
});

View File

@ -1,7 +1,7 @@
import { ajax } from 'discourse/lib/ajax';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { formatAnchor, formatAmount } from '../lib/donation-utilities';
import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
import { default as computed } from 'ember-addons/ember-computed-decorators';
import showModal from "discourse/lib/show-modal";
export default Ember.Component.extend({
@ -60,7 +60,7 @@ export default Ember.Component.extend({
period(anchor, interval) {
return I18n.t(`discourse_donations.period.${interval}`, {
anchor: formatAnchor(interval, moment.unix(anchor))
})
});
},
cancelSubscription() {
@ -93,4 +93,4 @@ export default Ember.Component.extend({
});
}
}
})
});

View File

@ -1,7 +1,7 @@
import { ajax } from 'discourse/lib/ajax';
import { formatAnchor, zeroDecimalCurrencies } from '../lib/donation-utilities';
import { default as computed } from 'ember-addons/ember-computed-decorators';
import { emailValid } from "discourse/lib/utilities";
import { emailValid as emailValidHelper } from "discourse/lib/utilities";
export default Ember.Component.extend({
result: [],
@ -29,6 +29,34 @@ export default Ember.Component.extend({
});
},
@computed
causes() {
const categoryEnabled = Discourse.SiteSettings.discourse_donations_cause_category;
if (categoryEnabled) {
let categoryIds = Discourse.SiteSettings.discourse_donations_causes_categories.split('|');
if (categoryIds.length) {
categoryIds = categoryIds.map(Number);
return this.site
.get("categoriesList")
.filter(c => {
return categoryIds.indexOf(c.id) > -1;
}).map(c => {
return {
id: c.id,
name: c.name
};
});
} else {
return [];
}
} else {
const causes = Discourse.SiteSettings.discourse_donations_causes;
return causes ? causes.split('|') : [];
}
},
@computed('types')
donationTypes(types) {
return types.map((type) => {
@ -97,7 +125,7 @@ export default Ember.Component.extend({
@computed('email')
emailValid(email) {
return emailValid(email);
return emailValidHelper(email);
},
@computed('email', 'emailValid')
@ -110,9 +138,14 @@ export default Ember.Component.extend({
return currentUser || emailValid;
},
@computed('userReady', 'stripeReady')
formIncomplete(userReady, stripeReady) {
return !userReady || !stripeReady;
@computed('cause')
causeValid(cause) {
return cause || !Discourse.SiteSettings.discourse_donations_cause_required;
},
@computed('userReady', 'stripeReady', 'causeValid')
formIncomplete(userReady, stripeReady, causeValid) {
return !userReady || !stripeReady || !causeValid;
},
@computed('transactionInProgress', 'formIncomplete')
@ -181,6 +214,7 @@ export default Ember.Component.extend({
let params = {
stripeToken: data.token.id,
cause: self.get('cause'),
type: self.get('type'),
amount,
email: self.get('email'),

View File

@ -0,0 +1,3 @@
{{#if siteSettings.discourse_donations_cause_category}}
{{mount-widget widget="category-header-widget"}}
{{/if}}

View File

@ -0,0 +1,11 @@
{{#if siteSettings.discourse_donations_cause_category}}
<section class='field'>
<label>{{i18n 'discourse_donations.cause.github.setting_label'}}</label>
{{text-field value=category.custom_fields.donations_github placeholderKey="discourse_donations.cause.github.setting_placeholder"}}
</section>
<section class='field'>
<label>{{i18n 'discourse_donations.cause.maintainers.label'}}</label>
{{user-selector usernames=category.custom_fields.donations_maintainers}}
</section>
{{/if}}

View File

@ -9,4 +9,4 @@ export default Ember.Controller.extend({
this.send('closeModal');
}
}
})
});

View File

@ -2,6 +2,7 @@ import { default as computed } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { ajax } from 'discourse/lib/ajax';
import { getOwner } from 'discourse-common/lib/get-owner';
import { emailValid } from "discourse/lib/utilities";
export default Ember.Controller.extend({
loadingDonations: false,
@ -41,8 +42,8 @@ export default Ember.Controller.extend({
Ember.run.later(() => {
this.set('hasEmailResult', false);
}, 6000)
})
}, 6000);
});
},
showLogin() {
@ -50,4 +51,4 @@ export default Ember.Controller.extend({
controller.send('showLogin');
}
}
})
});

View File

@ -2,28 +2,37 @@ import { withPluginApi } from 'discourse/lib/plugin-api';
export default {
name: 'donations-edits',
initialize() {
initialize(container) {
const siteSettings = container.lookup('site-settings:main');
withPluginApi('0.8.12', api => {
api.decorateCooked($post => {
const $form = $post.find('.stripe-checkout');
if ($form.length) {
const $input = $form.find('input');
const settings = Discourse.SiteSettings;
var s = document.createElement('script');
s.src = 'https://checkout.stripe.com/checkout.js';
s.setAttribute('class', 'stripe-button');
s.setAttribute('data-key', settings.discourse_donations_public_key);
s.setAttribute('data-key', siteSettings.discourse_donations_public_key);
s.setAttribute('data-amount', $input.attr('amount'));
s.setAttribute('data-name', settings.discourse_donations_shop_name);
s.setAttribute('data-name', siteSettings.discourse_donations_shop_name);
s.setAttribute('data-description', $form.attr('content'));
s.setAttribute('data-image', $form.attr('image') || '');
s.setAttribute('data-locale', 'auto');
s.setAttribute('data-zip-code', settings.discourse_donations_zip_code);
s.setAttribute('data-billing-address', settings.discourse_donations_billing_address);
s.setAttribute('data-currency', settings.discourse_donations_currency);
s.setAttribute('data-zip-code', siteSettings.discourse_donations_zip_code);
s.setAttribute('data-billing-address', siteSettings.discourse_donations_billing_address);
s.setAttribute('data-currency', siteSettings.discourse_donations_currency);
$form.append(s);
}
});
if (siteSettings.discourse_donations_cause_category) {
api.decorateWidget('category-header-widget:after', helper => {
helper.widget.appEvents.on('page:changed', () => {
helper.widget.scheduleRerender();
});
});
}
});
}
};

View File

@ -1,4 +1,13 @@
<form id="payment-form" class="form-horizontal">
<div class="control-group">
<label class="control-label">
{{i18n 'discourse_donations.cause.label'}}
</label>
<div class="controls controls-dropdown">
{{combo-box content=causes value=cause none='discourse_donations.cause.placeholder'}}
</div>
</div>
<div class="control-group">
<label class="control-label">
{{i18n 'discourse_donations.type'}}

View File

@ -0,0 +1,90 @@
import { createWidget } from 'discourse/widgets/widget';
import { h } from 'virtual-dom';
import { avatarFor } from 'discourse/widgets/post';
function donationDisplay(amount, type) {
return h(`div.donations-${type}`, [
h('span', I18n.t(`discourse_donations.cause.category.${type}`)),
h('span', `$${(amount/100).toFixed(2)}`)
]);
}
createWidget('category-header-widget', {
tagName: 'span',
html() {
const path = window.location.pathname;
let category;
const controller = this.container.lookup('controller:navigation/category');
category = controller.get("category");
if(/^\/c\//.test(path)) {
$("body").addClass("donations-category");
let contents = [
h('div.donations-category-contents', [
h('h1', category.name),
h('div.category-title-description', h('p', category.description_text))
]),
h('div.donations-category-metadata', [
donationDisplay(category.donations_total || 0, 'total'),
donationDisplay(category.donations_month || 0, 'month'),
h('div.donations-github', this.attach('link', {
icon: 'github',
label: 'discourse_donations.cause.github.label',
href: category.donations_github
}))
])
];
let userContents = [];
if (category.donations_backers.length) {
userContents.push(h('div.donations-backers', [
h('div.donations-backers-title', I18n.t('discourse_donations.cause.backers.label')),
category.donations_backers.map(user => {
return avatarFor('medium', {
template: user.avatar_template,
username: user.username,
name: user.name,
url: user.usernameUrl,
className: "backer-avatar"
});
})
]));
};
if (category.donations_maintainers.length) {
userContents.push(h('div.donations-maintainers', [
h('div.donations-maintainers-title', I18n.t('discourse_donations.cause.maintainers.label')),
category.donations_maintainers.map(user => {
if (user) {
return avatarFor('medium', {
template: user.avatar_template,
username: user.username,
name: user.name,
url: user.usernameUrl,
className: "maintainer-avatar"
});
} else {
return;
}
})
]));
}
if (userContents.length) {
contents.push(h('div.donations-category-users', userContents));
}
return h('div.donations-category-header', {
"attributes" : {
"style" : "background-color: #" + category.color + "; color: #" + category.text_color + ";"
}
}, contents);
} else {
$("body").removeClass("donations-category");
}
}
});

View File

@ -108,3 +108,70 @@ div.stripe-errors {
}
}
}
.donations-category-header {
padding-top: 60px;
text-align: center;
.donations-category-contents {
width: 500px;
margin: 0 auto;
padding: 40px 40px 10px;
text-align: center;
i {
margin-right: .25em;
font-size: 1.5em;
}
h1 {
text-transform: capitalize;
display: inline;
}
}
.donations-category-metadata {
width: 500px;
margin: 0 auto;
padding-bottom: 30px;
display: flex;
justify-content: space-around;
font-size: 1.2rem;
.donations-total span:first-of-type, .donations-month span:first-of-type {
margin-right: 5px;
}
.donations-github a {
color: inherit;
}
}
.donations-category-users {
width: 500px;
margin: 0 auto;
font-size: 1.2rem;
padding-top: 10px;
padding-bottom: 40px;
display: flex;
align-items: center;
.donations-backers, .donations-maintainers {
flex: 1 1 auto;
}
.backer-avatar, .maintainer-avatar {
margin: 0 5px;
}
.donations-backers-title, .donations-maintainers-title {
padding-bottom: 10px;
}
}
}
.donations-category {
#main-outlet {
padding-top: 20px;
}
}

View File

@ -18,6 +18,10 @@ en:
discourse_donations_amounts: "Donation amounts available to user. First listed will be the default."
discourse_donations_custom_amount: "Allow custom donation amount"
discourse_donations_types: "Donation types. First listed will be the default."
discourse_donations_causes: "Custom causes a user can donate to."
discourse_donations_causes_categories: "Categories of cause a user can donate do."
discourse_donations_cause_category: "Category causes enabled."
discourse_donations_cause_required: "Require user to select a cause when donating."
errors:
discourse_donations_amount_must_be_number: "Amounts must be numbers"
@ -32,6 +36,20 @@ en:
invoice: "invoice"
invoice_prefix: "You gave"
receipt: "Receipt sent to {{email}}."
cause:
label: "Cause"
placeholder: "Select a cause"
category:
total: "Total"
month: "Month"
backers:
label: "Backers"
github:
label: "Repository"
setting_label: "Github"
setting_placeholder: "repoistory url"
maintainers:
label: "Maintainers"
subscription:
cancel:
title: "Cancel Recurring Donation"

View File

@ -3,20 +3,20 @@ plugins:
default: false
client: true
discourse_donations_secret_key:
default: 'YOUR STRIPE API SECRET'
default: ''
client: false
discourse_donations_public_key:
default: 'YOUR STRIPE API KEY'
default: ''
client: true
discourse_donations_enable_create_accounts:
client: true
default: false
discourse_donations_description:
client: true
default: 'Donation'
default: ''
discourse_donations_shop_name:
client: true
default: 'Your shop name'
default: ''
discourse_donations_currency:
client: true
default: 'USD'
@ -53,9 +53,23 @@ plugins:
discourse_donations_types:
client: true
type: list
default: 'month|once'
default: 'once|month'
choices:
- year
- month
- week
- once
discourse_donations_causes:
client: true
type: list
default: ''
discourse_donations_causes_categories:
client: true
type: category_list
default: ''
discourse_donations_cause_category:
client: true
default: false
discourse_donations_cause_required:
client: true
default: false

View File

@ -34,4 +34,64 @@ after_initialize do
end
end
end
class ::Category
def donations_total
if custom_fields['donations_total']
custom_fields['donations_total']
else
0
end
end
def donations_month
if custom_fields['donations_month']
custom_fields['donations_month']
else
0
end
end
def donations_backers
if custom_fields['donations_backers']
[*custom_fields['donations_backers']].map do |user_id|
User.find_by(id: user_id.to_i)
end
else
[]
end
end
def donations_maintainers
if custom_fields['donations_maintainers']
custom_fields['donations_maintainers'].split(',').map do |username|
User.find_by(username: username)
end
else
[]
end
end
def donations_github
if custom_fields['donations_github']
custom_fields['donations_github']
else
''
end
end
end
if SiteSetting.discourse_donations_cause_category
add_to_serializer(:basic_category, :donations_total) { object.donations_total }
add_to_serializer(:basic_category, :donations_month) { object.donations_month }
add_to_serializer(:basic_category, :donations_backers) {
ActiveModel::ArraySerializer.new(object.donations_backers, each_serializer: BasicUserSerializer).as_json
}
add_to_serializer(:basic_category, :donations_maintainers) {
ActiveModel::ArraySerializer.new(object.donations_maintainers, each_serializer: BasicUserSerializer).as_json
}
add_to_serializer(:basic_category, :donations_github) { object.donations_github }
end
DiscourseEvent.trigger(:donations_ready)
end