Skip to content

Commit

Permalink
Add functionality so one POST now saves payload then ingests grant data
Browse files Browse the repository at this point in the history
  • Loading branch information
mds1 committed Jul 1, 2020
1 parent e0d10c8 commit c50195a
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 60 deletions.
134 changes: 75 additions & 59 deletions app/assets/v2/js/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -652,10 +652,10 @@ Vue.component('grants-cart', {
});
} else {
approvalTx.send({ from: userAddress })
.on('transactionHash', (txHash) => { // eslint-disable-line no-loop-func
.on('transactionHash', async(txHash) => { // eslint-disable-line no-loop-func
indicateMetamaskPopup(true);
this.setApprovalTxHash(tokenName, txHash);
this.sendDonationTx(userAddress);
await this.sendDonationTx(userAddress);
})
.on('error', (error, receipt) => {
// If the transaction was rejected by the network with a receipt, the second parameter will be the receipt.
Expand All @@ -676,7 +676,7 @@ Vue.component('grants-cart', {
});
},

sendDonationTx(userAddress) {
async sendDonationTx(userAddress) {
// Configure our donation inputs
// We use parse and stringify to avoid mutating this.donationInputs since we use it later
const bulkTransaction = new web3.eth.Contract(bulkCheckoutAbi, bulkCheckoutAddress);
Expand All @@ -697,10 +697,10 @@ Vue.component('grants-cart', {
bulkTransaction.methods
.donate(donationInputsFiltered)
.send({ from: userAddress, gas: this.donationInputsGasLimit, value: this.donationInputsEthAmount })
.on('transactionHash', (txHash) => {
.on('transactionHash', async(txHash) => {
console.log('Donation transaction hash: ', txHash);
indicateMetamaskPopup(true);
this.postToDatabase(txHash, userAddress);
this.postToDatabase(txHash, userAddress); // add `await` here if you want to wait for a response
// Clear cart, redirect back to grants page, and show success alert
localStorage.setItem('contributions_were_successful', 'true');
localStorage.setItem('contributions_count', String(this.grantData.length));
Expand All @@ -726,19 +726,50 @@ Vue.component('grants-cart', {
});
},

postToDatabase(txHash, userAddress) {
async postToDatabase(txHash, userAddress) {
// this.donationInputs is the array used for bulk donations
// We loop through each donation and POST the required data
const donations = this.donationInputs;
const csrfmiddlewaretoken = document.querySelector('[name=csrfmiddlewaretoken]').value;

// Configure template payload
const saveSubscriptionPayload = {
// Values that are constant for all donations
contributor_address: userAddress,
csrfmiddlewaretoken,
frequency_count: 1,
frequency_unit: 'rounds',
gas_price: 0,
gitcoin_donation_address: gitcoinAddress,
hide_wallet_address: this.hideWalletAddress,
match_direction: '+',
network: document.web3network,
num_periods: 1,
real_period_seconds: 0,
recurring_or_not: 'once',
signature: 'onetime',
split_tx_id: txHash, // this txhash is our bulk donation hash
splitter_contract_address: bulkCheckoutAddress,
subscription_hash: 'onetime',
// Values that vary by donation
'gitcoin-grant-input-amount': [],
admin_address: [],
amount_per_period: [],
comment: [],
confirmed: [],
contract_address: [],
contract_version: [],
denomination: [],
grant_id: [],
sub_new_approve_tx_id: [],
token_address: [],
token_symbol: []
};

for (let i = 0; i < donations.length; i += 1) {
// Get URL to POST to
const donation = donations[i];
const grantId = donation.grant.grant_id;
const grantSlug = donation.grant.grant_slug;
const url = `/grants/${grantId}/${grantSlug}/fund`;


// Get token information
const tokenName = donation.grant.grant_donation_currency;
Expand All @@ -758,57 +789,42 @@ Vue.component('grants-cart', {
// 100 makes it easier to search the DB to find which Gitcoin donations were automatic
const isAutomatic = donation.grant.isAutomatic;
const gitcoinGrantInputAmt = isAutomatic ? 100 : Number(this.gitcoinFactorRaw);
var network = document.web3network;
// Configure saveSubscription payload
const saveSubscriptionPayload = new URLSearchParams({
admin_address: donation.grant.grant_admin_address,
amount_per_period: Number(donation.grant.grant_donation_amount),
comment,
confirmed: false,
contract_address: donation.grant.grant_contract_address,
contract_version: donation.grant.grant_contract_version,
contributor_address: userAddress,
csrfmiddlewaretoken,
denomination: tokenAddress,
frequency_count: 1,
frequency_unit: 'rounds',
gas_price: 0,
'gitcoin-grant-input-amount': gitcoinGrantInputAmt,
gitcoin_donation_address: gitcoinAddress,
grant_id: grantId,
hide_wallet_address: this.hideWalletAddress,
match_direction: '+',
network,
num_periods: 1,
real_period_seconds: 0,
recurring_or_not: 'once',
signature: 'onetime',
split_tx_id: txHash, // this txhash is our bulk donation hash
splitter_contract_address: bulkCheckoutAddress,
sub_new_approve_tx_id: donation.tokenApprovalTxHash,
subscription_hash: 'onetime',
token_address: tokenAddress,
token_symbol: tokenName
});

// Configure headers
const headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
};

// Define parameter objects for POST request
const saveSubscriptionParams = {
method: 'POST',
headers,
body: saveSubscriptionPayload
};

// Send saveSubscription request
fetch(url, saveSubscriptionParams)
.catch(err => {
this.handleError(err);
});
}
// Add the donation parameters
saveSubscriptionPayload.admin_address.push(donation.grant.grant_admin_address);
saveSubscriptionPayload.amount_per_period.push(Number(donation.grant.grant_donation_amount));
saveSubscriptionPayload.comment.push(comment);
saveSubscriptionPayload.confirmed.push(false);
saveSubscriptionPayload.contract_address.push(donation.grant.grant_contract_address);
saveSubscriptionPayload.contract_version.push(donation.grant.grant_contract_version);
saveSubscriptionPayload.denomination.push(tokenAddress);
saveSubscriptionPayload['gitcoin-grant-input-amount'].push(gitcoinGrantInputAmt);
saveSubscriptionPayload.grant_id.push(grantId);
saveSubscriptionPayload.sub_new_approve_tx_id.push(donation.tokenApprovalTxHash);
saveSubscriptionPayload.token_address.push(tokenAddress);
saveSubscriptionPayload.token_symbol.push(tokenName);
} // end for each donation


// Configure request parameters
const url = '/grants/bulk-fund';
const headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
};
const saveSubscriptionParams = {
method: 'POST',
headers,
body: new URLSearchParams(saveSubscriptionPayload)
};

// Send saveSubscription request
// check `Preserve log` in console settings to inspect these logs more easily
const res = await fetch(url, saveSubscriptionParams);

console.log('Bulk fund POST response', res);
const json = await res.json();

console.log('Bulk fund POST response, JSON', json);
},

sleep(ms) {
Expand Down
3 changes: 2 additions & 1 deletion app/grants/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from grants.views import (
flag, grant_activity, grant_categories, grant_details, grant_fund, grant_new, grant_new_whitelabel, grants,
grants_addr_as_json, grants_bulk_add, grants_by_grant_type, grants_cart_view, grants_clr, grants_stats_view,
invoice, leaderboard, new_matching_partner, profile, quickstart, subscription_cancel,
invoice, leaderboard, new_matching_partner, profile, quickstart, subscription_cancel, bulk_fund,
)

app_name = 'grants'
Expand All @@ -39,6 +39,7 @@
re_path(r'^new', grant_new, name='new'),
re_path(r'^categories', grant_categories, name='grant_categories'),
path('<int:grant_id>/<slug:grant_slug>/fund', grant_fund, name='fund'),
path('bulk-fund', bulk_fund, name='bulk_fund'),
path(
'<int:grant_id>/<slug:grant_slug>/subscription/<int:subscription_id>/cancel',
subscription_cancel,
Expand Down
151 changes: 151 additions & 0 deletions app/grants/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
support_cancellation, thank_you_for_supporting,
)
from marketing.models import Keyword, Stat
from perftools.models import JSONStore
from ratelimit.decorators import ratelimit
from retail.helpers import get_ip
from townsquare.models import Comment, PinnedPost
Expand Down Expand Up @@ -850,7 +851,157 @@ def grant_fund(request, grant_id, grant_slug):

raise Http404

@login_required
def bulk_fund(request):
if request.method != 'POST':
raise Http404

# Save off payload data
JSONStore.objects.create(
key=request.POST.get('split_tx_id'), # use bulk data tx hash as key
view='bulk_fund_post_payload',
data=request.POST
)

# Get list of grant IDs
grant_ids_list = [int(pk) for pk in request.POST.get('grant_id').split(',')]

# For each grant, we validate the data. If it fails, save it off and throw error at the end
successes = []
failures = []
for (index, grant_id) in enumerate(grant_ids_list):
try:
grant = Grant.objects.get(pk=grant_id)
except Grant.DoesNotExist:
failures.append({
'active': 'grant_error',
'title': _('Fund - Grant Does Not Exist'),
'grant':grant_id,
'text': _('This grant does not exist'),
'subtext': _('No grant with this ID was found'),
'success': False
})
continue

profile = get_profile(request)

if not grant.active:
failures.append({
'active': 'grant_error',
'title': _('Fund - Grant Ended'),
'grant':grant_id,
'text': _('This Grant has ended.'),
'subtext': _('Contributions can no longer be made this grant'),
'success': False
})
continue

if is_grant_team_member(grant, profile):
failures.append({
'active': 'grant_error',
'title': _('Fund - Grant funding blocked'),
'grant':grant_id,
'text': _('This Grant cannot be funded'),
'subtext': _('Grant team members cannot contribute to their own grant.'),
'success': False
})
continue

if grant.link_to_new_grant:
failures.append({
'active': 'grant_error',
'title': _('Fund - Grant Migrated'),
'grant':grant_id,
'text': _('This Grant has ended'),
'subtext': _('Contributions can no longer be made to this grant. <br> Visit the new grant to contribute.'),
'success': False
})
continue

active_subscription = Subscription.objects.select_related('grant').filter(
grant=grant_id, active=True, error=False, contributor_profile=request.user.profile, is_postive_vote=True
)

if active_subscription:
failures.append({
'active': 'grant_error',
'title': _('Subscription Exists'),
'grant':grant_id,
'text': _('You already have an active subscription for this grant.'),
'success': False
})
continue

if not grant.configured_to_receieve_funding:
failures.append({
'active': 'grant_error',
'title': _('Fund - Grant Not Configured'),
'grant':grant_id,
'text': _('This Grant is not configured to accept funding at this time.'),
'subtext': _('Grant is not properly configured for funding. Please set grant.contract_address on this grant, or contact [email protected] if you believe this message is in error!'),
'success': False
})
continue

try:
from grants.tasks import process_grant_contribution
payload = {
# Values that are constant for all donations
'contributor_address': request.POST.get('contributor_address'),
'csrfmiddlewaretoken': request.POST.get('csrfmiddlewaretoken'),
'frequency_count': request.POST.get('frequency_count'),
'frequency_unit': request.POST.get('frequency_unit'),
'gas_price': request.POST.get('gas_price'),
'gitcoin_donation_address': request.POST.get('gitcoin_donation_address'),
'hide_wallet_address': request.POST.get('hide_wallet_address'),
'match_direction': request.POST.get('match_direction'),
'network': request.POST.get('network'),
'num_periods': request.POST.get('num_periods'),
'real_period_seconds': request.POST.get('real_period_seconds'),
'recurring_or_not': request.POST.get('recurring_or_not'),
'signature': request.POST.get('signature'),
'split_tx_id': request.POST.get('split_tx_id'),
'splitter_contract_address': request.POST.get('splitter_contract_address'),
'subscription_hash': request.POST.get('subscription_hash'),
# Values that vary by donation
'admin_address': request.POST.get('admin_address').split(',')[index],
'amount_per_period': request.POST.get('amount_per_period').split(',')[index],
'comment': request.POST.get('comment').split(',')[index],
'confirmed': request.POST.get('confirmed').split(',')[index],
'contract_address': request.POST.get('contract_address').split(',')[index],
'contract_version': request.POST.get('contract_version').split(',')[index],
'denomination': request.POST.get('denomination').split(',')[index],
'gitcoin-grant-input-amount': request.POST.get('gitcoin-grant-input-amount').split(',')[index],
'grant_id': request.POST.get('grant_id').split(',')[index],
'sub_new_approve_tx_id': request.POST.get('sub_new_approve_tx_id').split(',')[index],
'token_address': request.POST.get('token_address').split(',')[index],
'token_symbol': request.POST.get('token_symbol').split(',')[index],
}
process_grant_contribution.delay(grant_id, grant.slug, profile.pk, payload)
except Exception as e:
failures.append({
'active': 'grant_error',
'title': _('Fund - Grant Processing Failed'),
'grant':grant_id,
'text': _('This Grant was not processed successfully.'),
'subtext': _(f'{str(e)}'),
'success': False
})
continue

successes.append({
'title': _('Fund - Grant Funding Processed Successfully'),
'grant':grant_id,
'text': _('Funding for this grant was successfully processed and saved.'),
'success': True
})

return JsonResponse({
'success': True,
'grant_ids': grant_ids_list,
'successes': successes,
'failures': failures
})

@login_required
def subscription_cancel(request, grant_id, grant_slug, subscription_id):
Expand Down

0 comments on commit c50195a

Please sign in to comment.