From 7efb431cd371002ee58a55d7b02c2b3aafe275d6 Mon Sep 17 00:00:00 2001 From: Kevin Owocki Date: Mon, 20 Jul 2020 07:15:52 -0600 Subject: [PATCH 1/9] removes per tip txn limit (#7101) * removes tip per txn limit * removes tip per txn limit * removes tip per txn limit --- app/dashboard/tip_views.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/app/dashboard/tip_views.py b/app/dashboard/tip_views.py index 5fa24cb1842..b437a3adc75 100644 --- a/app/dashboard/tip_views.py +++ b/app/dashboard/tip_views.py @@ -415,33 +415,6 @@ def send_tip_3(request): sender_profile=get_profile(from_username), ) - is_over_tip_tx_limit = False - is_over_tip_weekly_limit = False - max_per_tip = request.user.profile.max_tip_amount_usdt_per_tx if request.user.is_authenticated and request.user.profile else 500 - if tip.value_in_usdt_now: - is_over_tip_tx_limit = tip.value_in_usdt_now > max_per_tip - if request.user.is_authenticated and request.user.profile: - tips_last_week_value = tip.value_in_usdt_now - tips_last_week = Tip.objects.send_happy_path().filter(sender_profile=get_profile(from_username), created_on__gt=timezone.now() - timezone.timedelta(days=7)) - for this_tip in tips_last_week: - if this_tip.value_in_usdt_now: - tips_last_week_value += this_tip.value_in_usdt_now - is_over_tip_weekly_limit = tips_last_week_value > request.user.profile.max_tip_amount_usdt_per_week - - increase_funding_form_title = _('Request a Funding Limit Increasement') - increase_funding_form = f'{increase_funding_form_title}' - - if is_over_tip_tx_limit: - response['status'] = 'error' - response['message'] = _('This tip is over the per-transaction limit of $') +\ - str(max_per_tip) + '. ' + increase_funding_form - elif is_over_tip_weekly_limit: - response['status'] = 'error' - response['message'] = _('You are over the weekly tip send limit of $') +\ - str(request.user.profile.max_tip_amount_usdt_per_week) +\ - '. ' + increase_funding_form - return JsonResponse(response) From 70cc70f9b2db775b9d9990123b78afab850a7ba6 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Mon, 20 Jul 2020 22:19:16 +0900 Subject: [PATCH 2/9] allow matic grant creation w/o web3 (#7118) * allow matic grant creation w/o web3 * remove consolelog * fix lint --- app/assets/v2/js/grants/new.js | 24 +++++++++++-------- .../templates/grants/new-whitelabel.html | 15 ------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/app/assets/v2/js/grants/new.js b/app/assets/v2/js/grants/new.js index 6ff77cbcb51..a50e72949e3 100644 --- a/app/assets/v2/js/grants/new.js +++ b/app/assets/v2/js/grants/new.js @@ -79,8 +79,10 @@ const init = () => { data[this.name] = this.value; }); - $('#token_symbol').val($('#js-token option:selected').text()); - $('#token_address').val($('#js-token option:selected').val()); + if ($('#token_address').length) { + $('#token_symbol').val($('#js-token option:selected').text()); + $('#token_address').val($('#js-token option:selected').val()); + } if (document.web3network) { $('#network').val(document.web3network); @@ -120,8 +122,13 @@ const init = () => { formData.append('reference_url', $('#input-url').val()); formData.append('admin_address', $('#input-admin_address').val()); formData.append('contract_owner_address', $('#contract_owner_address').val()); - formData.append('token_address', $('#token_address').val()); - formData.append('token_symbol', $('#token_symbol').val()); + if ($('#token_address').length) { + formData.append('token_address', $('#token_address').val()); + formData.append('token_symbol', $('#token_symbol').val()); + } else { + formData.append('token_address', '0x0000000000000000000000000000000000000000'); + formData.append('token_symbol', 'Any Token'); + } formData.append('contract_version', $('#contract_version').val()); formData.append('transaction_hash', $('#transaction_hash').val()); formData.append('network', $('#network').val()); @@ -151,14 +158,11 @@ const init = () => { }); }; -window.addEventListener('dataWalletReady', function(e) { - init(); -}, false); - $(document).ready(function() { $('.select2-selection__choice').removeAttr('title'); - + init(); + changeTokens(); }); function saveGrant(grantData, isFinal) { @@ -194,7 +198,7 @@ function saveGrant(grantData, isFinal) { $('#new_button').on('click', function(e) { - if (!provider) { + if (!provider && $('#token_address').length != 0) { e.preventDefault(); return onConnect().then(() => init()); } diff --git a/app/grants/templates/grants/new-whitelabel.html b/app/grants/templates/grants/new-whitelabel.html index 3d46a181307..e7d9decc44f 100644 --- a/app/grants/templates/grants/new-whitelabel.html +++ b/app/grants/templates/grants/new-whitelabel.html @@ -122,19 +122,6 @@
Project Information
Funding Information

-
-
-
- -
- -
-
-
-
-
@@ -156,8 +143,6 @@

- - From 23bd48fb6cfcbd3764cf45fc62333f6bc7e1c53a Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Mon, 20 Jul 2020 22:59:12 +0900 Subject: [PATCH 3/9] fix network assignment for grants w/o web3 --- app/assets/v2/js/grants/new.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/v2/js/grants/new.js b/app/assets/v2/js/grants/new.js index a50e72949e3..a5b62a026bb 100644 --- a/app/assets/v2/js/grants/new.js +++ b/app/assets/v2/js/grants/new.js @@ -131,7 +131,11 @@ const init = () => { } formData.append('contract_version', $('#contract_version').val()); formData.append('transaction_hash', $('#transaction_hash').val()); - formData.append('network', $('#network').val()); + if ($('#network').val()) { + formData.append('network', $('#network').val()); + } else { + formData.append('network', 'mainnet'); + } formData.append('team_members[]', $('#input-team_members').val()); formData.append('categories[]', $('#input-categories').val()); formData.append('grant_type', $('#input-grant_type').val().toLowerCase()); From 9a4a01340f1821043f34c9b71be46c2286ab967f Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Mon, 20 Jul 2020 23:54:56 +0900 Subject: [PATCH 4/9] fix matic label in grants index --- app/grants/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/grants/views.py b/app/grants/views.py index 1395e17614b..ed9db8cca2e 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -382,7 +382,7 @@ def grants_by_grant_type(request, grant_type): {'label': 'Tech', 'keyword': 'tech', 'count': tech_grants_count}, {'label': 'Community', 'keyword': 'media', 'count': media_grants_count}, # {'label': 'Health', 'keyword': 'health', 'count': health_grants_count}, - {'label': 'Matic', 'keyword': 'matic', 'count': matic_grants_count}, + {'label': 'Matic: Build-n-Earn', 'keyword': 'matic', 'count': matic_grants_count}, {'label': 'Crypto for Black Lives', 'keyword': 'change', 'count': change_count}, ] From 7e01054cf873522b3b24e2bb4e1086313be5bca7 Mon Sep 17 00:00:00 2001 From: owocki Date: Mon, 20 Jul 2020 13:41:37 -0600 Subject: [PATCH 5/9] gas prices threshold --- app/kudos/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/kudos/tasks.py b/app/kudos/tasks.py index dde228f74a6..dd90a649145 100644 --- a/app/kudos/tasks.py +++ b/app/kudos/tasks.py @@ -19,7 +19,7 @@ # Lock timeout of 2 minutes (just in the case that the application hangs to avoid a redis deadlock) LOCK_TIMEOUT = 60 * 2 delay_if_gas_prices_gt_redeem = 25 -delay_if_gas_prices_gt_mint = 60 +delay_if_gas_prices_gt_mint = 150 @app.shared_task(bind=True, max_retries=10) def mint_token_request(self, token_req_id, retry=False): From abfab51a71fae124a67b945891d35f96d09a2ca1 Mon Sep 17 00:00:00 2001 From: owocki Date: Wed, 22 Jul 2020 13:42:37 -0600 Subject: [PATCH 6/9] avatar URL --- app/dashboard/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/dashboard/views.py b/app/dashboard/views.py index f363f58fea3..1f38f922609 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -3746,12 +3746,13 @@ def hackathon_onboard(request, hackathon=''): except HackathonEvent.DoesNotExist: hackathon_event = HackathonEvent.objects.last() + avatar_url = hackathon_event.logo.url if hackathon_event.logo else request.build_absolute_uri(static('v2/images/twitter_cards/tw_cards-02.png')) params = { 'active': 'hackathon_onboard', 'title': f'{hackathon_event.name.title()} Onboard', 'hackathon': hackathon_event, 'referer': referer, - 'avatar_url': request.build_absolute_uri(static('v2/images/twitter_cards/tw_cards-02.png')), + 'avatar_url': avatar_url, 'is_registered': is_registered, 'sponsors': sponsors, 'onboard': True From 01502cce90eab8b10a2b9edafec0cfaeac402d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Octavio=20Amuch=C3=A1stegui?= Date: Fri, 24 Jul 2020 10:15:51 -0300 Subject: [PATCH 7/9] New bounty form (#7086) * create new form * set fee * add missing fields and fix datepicker * form fees and featured * url params * add subscriptions models * subscription validations and wallet tx counter * cleanup * migration * hide chains * add duplication validate * add decimals filter * fix validations * code review --- app/assets/v2/css/base.css | 4 + app/assets/v2/js/board.js | 6 - app/assets/v2/js/pages/new_bounty.js | 1275 ++++++++--------- app/assets/v2/js/user_card.js | 2 +- app/assets/v2/js/vue-components.js | 43 + app/assets/v2/js/vue-filters.js | 16 +- app/dashboard/admin.py | 10 +- app/dashboard/helpers.py | 10 + .../migrations/0129_tribessubscription.py | 32 + app/dashboard/models.py | 28 +- .../templates/bounty/new_bounty.html | 723 ++++++++++ app/dashboard/templates/quickstart.html | 8 +- app/dashboard/views.py | 6 +- 13 files changed, 1469 insertions(+), 694 deletions(-) create mode 100644 app/dashboard/migrations/0129_tribessubscription.py create mode 100644 app/dashboard/templates/bounty/new_bounty.html diff --git a/app/assets/v2/css/base.css b/app/assets/v2/css/base.css index 987af39c7a6..8aead5ce539 100644 --- a/app/assets/v2/css/base.css +++ b/app/assets/v2/css/base.css @@ -137,6 +137,10 @@ a { font-weight: 600; } +.text-through { + text-decoration-line: line-through; +} + #tutorials li{ list-style: none; } diff --git a/app/assets/v2/js/board.js b/app/assets/v2/js/board.js index c55a289950e..c4daf707e6b 100644 --- a/app/assets/v2/js/board.js +++ b/app/assets/v2/js/board.js @@ -224,12 +224,6 @@ if (document.getElementById('gc-board')) { }); } -Vue.filter('pluralize', (word, amount, singular, plural) => { - plural = plural || 's'; - singular = singular || ''; - return amount !== 1 ? `${word + plural}` : `${word + singular}`; -}); - Vue.filter('truncate', (account, num) => { num = !num ? num = 4 : num; return account.substr(0, num + 2) + '\u2026' + account.substr(-num); diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index 8ad27bcc6b1..0ff55c31509 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -1,747 +1,670 @@ -/* eslint-disable no-console */ -/* eslint-disable nonblock-statement-body-position */ -/* eslint-disable no-lonely-if */ -// document.web3network = 'mainnet'; -load_tokens(); -needWalletConnection(); - -const qr_tokens = [ 'ETC', 'cUSD', 'CELO', 'ZIL' ]; -const fiat_tokens = ['USD']; - -const isQRToken = tokenName => qr_tokens.includes(tokenName); -const isFiatToken = tokenName => fiat_tokens.includes(tokenName); - -const updateOnNetworkOrTokenChange = () => { - const tokenName = $('select[name=denomination]').select2('data')[0] && - $('select[name=denomination]').select2('data')[0].text; - - if (!tokenName) { - // tokens haven't loaded yet - } else if (isQRToken(tokenName) || isFiatToken(tokenName)) { - document.web3network = 'mainnet'; - - $('#navbar-network-banner').hide(); - $('.navbar-network').hide(); - $('.eth-chain').hide(); - $('.web3-alert').hide(); - - FEE_PERCENTAGE = 0; - - if (isQRToken(tokenName)) { - $('.funder-address-container').show(); - $('#funderAddress').attr('required', true); - $('#fiat_text').addClass('d-none'); - } else { - $('.funder-address-container').hide(); - $('#funderAddress').removeAttr('required'); - $('#funderAddress').val(''); - $('#fiat_text').removeClass('d-none'); - } - - } else { - if (!provider) { - onConnect().then(()=> { - changeUi(); - }); - } else { - web3Modal.on('connect', async() => { - try { - provider = await web3Modal.connect().then(()=> { - changeUi(); - }); - } catch (e) { - console.log('Could not get a wallet connection', e); - return; - } - }); - } - } -}; - -function changeUi() { - $('.eth-chain').show(); - FEE_PERCENTAGE = document.FEE_PERCENTAGE / 100.0; - - $('#navbar-network-banner').show(); - $('.navbar-network').show(); - - $('.funder-address-container').hide(); - $('#funderAddress').removeAttr('required'); - $('#funderAddress').val(''); - $('#fiat_text').addClass('d-none'); - - $('.web3-alert').show(); - if (!document.web3network) { - $('.web3-alert').html('To continue, please setup a web3 wallet.'); - $('.web3-alert').addClass('wallet-not-connected'); - } else if (document.web3network == 'locked') { - $('.web3-alert').html('To continue, please unlock your web3 wallet'); - $('.web3-alert').addClass('wallet-not-connected'); - } else if (document.web3network == 'rinkeby') { - $('.web3-alert').html(`connected to address ${selectedAccount} on rinkeby`); - $('.web3-alert').addClass('wallet-success'); - } else { - $('.web3-alert').html(`connected to address ${selectedAccount} on mainnet`); - $('.web3-alert').addClass('wallet-success'); - } -} - -var localStorage = window.localStorage ? window.localStorage : {}; -const quickstartURL = document.location.origin + '/bounty/quickstart'; - -let params = (new URL(document.location)).searchParams; - -let FEE_PERCENTAGE = document.FEE_PERCENTAGE / 100.0; - -var new_bounty = { - last_sync: new Date() -}; - -if (localStorage['quickstart_dontshow'] !== 'true' && - doShowQuickstart(document.referrer) && - doShowQuickstart(document.URL)) { - window.location = quickstartURL; -} - -function doShowQuickstart(url) { - let blacklist = []; - - blacklist.push(document.location.origin + '/bounty/quickstart'); - blacklist.push(document.location.origin + '/bounty/new\\?'); - blacklist.push(document.location.origin + '/funding/new\\?'); - blacklist.push(document.location.origin + '/new\\?'); - - for (let i = 0; i < blacklist.length; i++) { - if (url.match(blacklist[i])) - return false; - } - - return true; -} - -var processedData; -var usersBySkills; - -$('.select2-tag__choice').on('click', function() { - $('#invite-contributors.js-select2').data('select2').dataAdapter.select(processedData[0].children[$(this).data('id')]); -}); - -$('.select2-add_byskill').on('click', function(e) { - e.preventDefault(); - $('#invite-contributors.js-select2').val(usersBySkills.map((item) => { - return item.id; - })).trigger('change'); -}); - -$('.select2-clear_invites').on('click', function(e) { - e.preventDefault(); - $('#invite-contributors.js-select2').val(null).trigger('change'); -}); - - -const getSuggestions = () => { - let queryParams = {}; - - queryParams.keywords = $('#keywords').val(); - queryParams.invite = params.get('invite') || ''; - - let searchParams = new URLSearchParams(queryParams); - - const settings = { - url: `/api/v0.1/get_suggested_contributors?${searchParams}`, - method: 'GET', - processData: false, - dataType: 'json', - contentType: false - }; - - $.ajax(settings).done(function(response) { - let groups = { - 'contributors': 'Recently worked with you', - 'recommended_developers': 'Recommended based on skills', - 'verified_developers': 'Verified contributors', - 'invites': 'Invites' - }; - - let options = Object.entries(response).map(([ text, children ]) => ( - { text: groups[text], children } - )); - - usersBySkills = [].map.call(response['recommended_developers'], function(obj) { - return obj; - }); - - if (queryParams.keywords.length && usersBySkills.length) { - $('#invite-all-container').show(); - $('.select2-add_byskill span').text(queryParams.keywords.join(', ')); - } else { - $('#invite-all-container').hide(); - } - - var generalIndex = 0; +let appFormBounty; + +window.addEventListener('dataWalletReady', function(e) { + appFormBounty.network = networkName; + appFormBounty.form.funderAddress = selectedAccount; +}, false); + +Vue.component('v-select', VueSelect.VueSelect); +Vue.mixin({ + methods: { + getIssueDetails: function(url) { + let vm = this; + + if (!url) { + vm.$set(vm.errors, 'issueDetails', undefined); + vm.form.issueDetails = null; + return vm.form.issueDetails; + } - processedData = $.map(options, function(obj, index) { - if (obj.children.length < 1) { + if (url.indexOf('github.com/') < 0) { + vm.form.issueDetails = undefined; + vm.$set(vm.errors, 'issueDetails', 'Please paste a github issue url'); return; } - obj.children.forEach((children, childIndex) => { - children.text = children.fulfiller_github_username || children.user__profile__handle || children.profile__handle || children.handle; - children.id = generalIndex; - if (obj.text == 'Invites') { - children.selected = true; - $('#reserve-section').collapse('show'); - } - generalIndex++; - }); - return obj; - }); - - $('#invite-contributors').select2().empty(); - $('#invite-contributors.js-select2').select2({ - data: processedData, - placeholder: 'Select contributors', - escapeMarkup: function(markup) { - return markup; - }, - templateResult: formatUser, - templateSelection: formatUserSelection - }); - - }).fail(function(error) { - console.log('Could not fetch contributors', error); - }); -}; + let ghIssueUrl = new URL(url); -getSuggestions(); -$('#keywords').on('change', getSuggestions); + vm.orgSelected = ghIssueUrl.pathname.split('/')[1].toLowerCase(); -function formatUser(user) { - if (user.children) { - return user.text; - } - - let markup = `
-
- -
-
${user.text}
-
`; - - return markup; -} + if (vm.checkBlocked(vm.orgSelected)) { + vm.$set(vm.errors, 'issueDetails', 'This repo is not bountyable at the request of the maintainer.'); + vm.form.issueDetails = undefined; + return; + } + vm.$delete(vm.errors, 'issueDetails'); -function formatUserSelection(user) { - let selected; + const apiUrldetails = `/sync/get_issue_details?url=${encodeURIComponent(url.trim())}&duplicates=true`; - if (user.id) { - selected = ` - - ${user.text}`; - } else { - selected = user.text; - } - return selected; -} -function lastSynced(current, last_sync) { - return timeDifference(current, last_sync); -} + vm.form.issueDetails = undefined; + const getIssue = fetchData(apiUrldetails, 'GET'); -/** - * Checks if token used to fund bounty is authed. - */ -const handleTokenAuth = () => { - return new Promise((resolve) => { - const tokenName = $('#token option:selected').text(); - const tokenAddress = $('#token option:selected').val(); - let isTokenAuthed = true; - - const authedTokens = ['ETH'].concat(qr_tokens); - - if (!token) { - isTokenAuthed = false; - tokenAuthAlert(isTokenAuthed); - resolve(isTokenAuthed); - } else if (authedTokens.includes(tokenName)) { - tokenAuthAlert(isTokenAuthed); - resolve(isTokenAuthed); - } else { - const token_contract = new web3.eth.Contract(token_abi, tokenAddress); - const to = bounty_address(); - - token_contract.methods.allowance(selectedAccount, to).call({from: selectedAccount}, (error, result) => { - if (error || Number(result) == 0) { - isTokenAuthed = false; + $.when(getIssue).then((response) => { + if (!Object.keys(response).length) { + return vm.$set(vm.errors, 'issueDetails', 'Nothing found. Please check the issue URL.'); } - tokenAuthAlert(isTokenAuthed, tokenName); - resolve(isTokenAuthed); - }); - - } - }); -}; - -/** - * Toggles alert to notify user while bounty creation using an - * un-authed token. - * @param {boolean} isTokenAuthed - Token auth status for user - * @param {string=} tokenName - token name - */ -const tokenAuthAlert = (isTokenAuthed, tokenName) => { - $('.alert').remove(); - - if (isTokenAuthed) { - $('.alert').remove(); - $('#add-token-dialog').bootstrapModal('hide'); - $('#token-denomination').html(''); - } else { - tokenName = tokenName ? tokenName : ''; - _alert( - gettext(` - This token ${tokenName} needs to be enabled to fund this bounty, click on - - the Token Settings page and enable it. - This is only needed once per token.` - ), - 'warning' - ); - - $('#token-denomination').html(tokenName); - $('#add-token-dialog').bootstrapModal('show'); - } -}; - -$(function() { - $('#last-synced').hide(); - $('.js-select2').each(function() { - $(this).select2({ - minimumResultsForSearch: Infinity - }); - }); - - params.append('type', 'public'); - window.history.replaceState({}, '', location.pathname + '?' + params); - retrieveIssueDetails(); - - populateBountyTotal(); + vm.form.issueDetails = response; + // vm.$set(vm.errors, 'issueDetails', undefined); + }).catch((err) => { + console.log(err); + vm.form.issueDetails = undefined; + vm.$set(vm.errors, 'issueDetails', err.responseJSON.message); + }); - // Load sidebar radio buttons from localStorage - if (getParam('source')) { - $('input[name=issueURL]').val(getParam('source')); - } else if (getParam('url')) { - $('input[name=issueURL]').val(getParam('url')); - } else if (localStorage['issueURL']) { - $('input[name=issueURL]').val(localStorage['issueURL']); - } + }, + getTokens: function() { + let vm = this; + const apiUrlTokens = '/api/v1/tokens/'; + const getTokensData = fetchData(apiUrlTokens, 'GET'); + + $.when(getTokensData).then((response) => { + vm.tokens = response; + vm.form.token = vm.filterByChainId[0]; + vm.getAmount(vm.form.token.symbol); + + }).catch((err) => { + console.log(err); + }); + }, + getAmount: function(token) { + let vm = this; - setTimeout(setUsdAmount, 1000); + if (!token) { + return; + } + const apiUrlAmount = `/sync/get_amount?amount=1&denomination=${token}`; + const getAmountData = fetchData(apiUrlAmount, 'GET'); - // fetch issue URL related info - $('input[name=hours]').keyup(setUsdAmount); - $('input[name=hours]').blur(setUsdAmount); - $('input[name=amount]').keyup(setUsdAmount); + $.when(getAmountData).then(tokens => { + vm.coinValue = tokens[0].usdt; + vm.calcValues('usd'); - $('input[name=usd_amount]').on('focusin', function() { - $('input[name=usd_amount]').attr('prev_usd_amount', $(this).val()); - $('input[name=amount]').trigger('change'); + }).catch((err) => { + console.log(err); + }); + }, + calcValues: function(direction) { + let vm = this; - }); + if (direction == 'usd') { + let usdValue = vm.form.amount * vm.coinValue; - $('input[name=usd_amount]').on('focusout', function() { - $('input[name=usd_amount]').attr('prev_usd_amount', $(this).val()); - $('input[name=amount]').trigger('change'); - }); + vm.form.amountusd = Number(usdValue.toFixed(2)); + } else { + vm.form.amount = Number(vm.form.amountusd * 1 / vm.coinValue).toFixed(4); + } - $('input[name=usd_amount]').keyup(() => { - const prev_usd_amount = $('input[name=usd_amount]').attr('prev_usd_amount'); - const usd_amount = $('input[name=usd_amount').val(); + }, + addKeyword: function(item) { + let vm = this; - $('input[name=amount]').trigger('change'); + vm.form.keywords.push(item); + }, + checkForm: async function(e) { + let vm = this; - if (prev_usd_amount != usd_amount) { - usdToAmount(usd_amount); - } - }); + vm.submitted = true; + vm.errors = {}; - $('input[name=amount]').on('change', function() { - const amount = $('input[name=amount]').val(); + if (!vm.form.keywords.length) { + vm.$set(vm.errors, 'keywords', 'Please select the prize keywords'); + } + if (!vm.form.experience_level || !vm.form.project_length || !vm.form.bounty_type) { + vm.$set(vm.errors, 'experience_level', 'Please select the details options'); + } + if (!vm.chainId) { + vm.$set(vm.errors, 'chainId', 'Please select an option'); + } + if (!vm.form.issueDetails || vm.form.issueDetails < 1) { + vm.$set(vm.errors, 'issueDetails', 'Please input a GitHub issue'); + } + if (vm.form.bounty_categories.length < 1) { + vm.$set(vm.errors, 'bounty_categories', 'Select at least one category'); + } + if (!vm.form.funderAddress) { + vm.$set(vm.errors, 'funderAddress', 'Fill the owner wallet address'); + } + if (!vm.form.project_type) { + vm.$set(vm.errors, 'project_type', 'Select the project type'); + } + if (!vm.form.permission_type) { + vm.$set(vm.errors, 'permission_type', 'Select the permission type'); + } + if (!vm.form.terms) { + vm.$set(vm.errors, 'terms', 'You need to accept the terms'); + } + if (!vm.form.termsPrivacy) { + vm.$set(vm.errors, 'termsPrivacy', 'You need to accept the terms'); + } + if (Object.keys(vm.errors).length) { + return false; + } + }, + web3Type() { + let vm = this; + let type; + + switch (vm.chainId) { + case '1': + // ethereum + type = 'web3_modal'; + break; + case '666': + // paypal + type = 'fiat'; + break; + case '61': // ethereum classic + case '102': // zilliqa + case '42220': // celo mainnet + case '44786': // celo alfajores tesnet + case '717171': // other + type = 'qr'; + break; + default: + type = 'web3_modal'; + } - $('#summary-bounty-amount').html(amount); - $('#summary-fee-amount').html((amount * FEE_PERCENTAGE).toFixed(4)); - populateBountyTotal(); - }); + vm.form.web3_type = type; + return type; + }, + getParams: async function() { + let vm = this; - var triggerDenominationUpdate = function(e) { - setUsdAmount(); - handleTokenAuth(); + let params = new URLSearchParams(window.location.search); - updateOnNetworkOrTokenChange(); + if (params.has('invite')) { + vm.expandedGroup.reserve = [1]; + } - const token_address = $('select[name=denomination]').val(); - const tokenName = $('select[name=denomination]').select2('data')[0] && - $('select[name=denomination]').select2('data')[0].text; + if (params.has('reserved')) { + vm.expandedGroup.reserve = [1]; + await vm.getUser(null, params.get('reserved'), true); + } - const tokendetails = isQRToken(tokenName) || isFiatToken(tokenName) ? - tokenAddressToDetailsByNetwork(token_address, 'mainnet') : - tokenAddressToDetails(token_address); + }, + showQuickStart: function(force) { + let quickstartDontshow = localStorage['quickstart_dontshow'] ? JSON.parse(localStorage['quickstart_dontshow']) : false; + + if (quickstartDontshow !== true || force) { + fetch('/bounty/quickstart') + .then(function(response) { + return response.text(); + }).then(function(html) { + let parser = new DOMParser(); + let doc = parser.parseFromString(html, 'text/html'); + + doc.querySelector('.show_video').href = 'https://www.youtube.com/watch?v=m1X0bDpVcf4'; + doc.querySelector('.show_video').target = '_blank'; + doc.querySelector('.btn-closeguide').dataset.dismiss = 'modal'; + + let docArticle = doc.querySelector('.content').innerHTML; + const content = $.parseHTML( + ``); + + $(content).appendTo('body'); + document.getElementById('dontshow').checked = quickstartDontshow; + $('#gitcoin_updates').bootstrapModal('show'); + + $(document).on('change', '#dontshow', function(e) { + if ($(this)[0].checked) { + localStorage['quickstart_dontshow'] = true; + } else { + localStorage['quickstart_dontshow'] = false; + } + }); + }); - if (!tokendetails) { - return; - } - const token = tokendetails['name']; + $(document, '#gitcoin_updates').on('hidden.bs.modal', function(e) { + $('#gitcoin_updates').remove(); + $('#gitcoin_updates').bootstrapModal('dispose'); + }); + } + }, + isExpanded(key, type) { + return this.expandedGroup[type].indexOf(key) !== -1; + }, + toggleCollapse(key, type) { + if (this.isExpanded(key, type)) { + this.expandedGroup[type].splice(this.expandedGroup[type].indexOf(key), 1); + } else { + this.expandedGroup[type].push(key); + } + }, + updateDate(date) { + let vm = this; - $('#summary-bounty-token').html(token); - $('#summary-fee-token').html(token); - populateBountyTotal(); - }; + vm.form.expirationTimeDelta = date.format('MM/DD/YYYY'); - $('select[name=denomination]').change(triggerDenominationUpdate); + }, + userSearch(search, loading) { + let vm = this; - waitforWeb3(function() { - let denominationId = setInterval(function() { - if ($('select[name=denomination]').val()) { - triggerDenominationUpdate(); - clearInterval(denominationId); + if (search.length < 3) { + return; } - }, 1000); - }); + loading(true); + vm.getUser(loading, search); - $('#featuredBounty').on('change', function() { - if ($(this).prop('checked')) { - if (document.FEE_PERCENTAGE == 0) - $('#confirmation').html('2'); - else - $('#confirmation').html('3'); - - $('.feature-amount').show(); - } else { - if (document.FEE_PERCENTAGE == 0) - $('#confirmation').html('1'); - else - $('#confirmation').html('2'); - - $('.feature-amount').hide(); - } - populateBountyTotal(); - }); + }, + getUser: async function(loading, search, selected) { + let vm = this; + let myHeaders = new Headers(); + let url = `/api/v0.1/users_search/?token=${currentProfile.githubToken}&term=${escape(search)}`; + + myHeaders.append('X-Requested-With', 'XMLHttpRequest'); + return new Promise(resolve => { + + fetch(url, { + credentials: 'include', + headers: myHeaders + }).then(res => { + res.json().then(json => { + vm.usersOptions = json; + if (selected) { + vm.$set(vm.form, 'reservedFor', vm.usersOptions[0].text); + } + resolve(); + }); + if (loading) { + loading(false); + } + }); + }); + }, + checkBlocked(org) { + let vm = this; + let blocked = vm.blockedUrls.toLocaleString().toLowerCase().split(','); + return blocked.indexOf(org.toLowerCase()) > -1; + }, + featuredValue() { + let vm = this; + const apiUrlAmount = `/sync/get_amount?amount=${vm.usdFeaturedPrice}&denomination=USDT`; + const getAmountData = fetchData(apiUrlAmount, 'GET'); - $('[name=project_type]').on('change', function() { - let val = $('input[name=project_type]:checked').val(); + $.when(getAmountData).then(value => { + vm.ethFeaturedPrice = value[0].eth.toFixed(4); - if (val !== 'traditional') { - $('#reservedFor').attr('disabled', true); - $('#reservedFor').select2().trigger('change'); - } else { - $('#reservedFor').attr('disabled', false); - userSearch('#reservedFor', false); - } - }); + }).catch((err) => { + console.log(err); + }); + }, + payFeaturedBounty: async function() { + let vm = this; - if ($('input[name=issueURL]').val() != '') { - retrieveIssueDetails(); - } + if (!provider) { + onConnect(); + return false; + } + return new Promise(resolve => + web3.eth.sendTransaction({ + to: '0x00De4B13153673BCAE2616b67bf822500d325Fc3', + from: selectedAccount, + value: web3.utils.toWei(String(vm.ethFeaturedPrice), 'ether'), + gas: web3.utils.toHex(318730), + gasLimit: web3.utils.toHex(318730) + }, function(error, result) { + if (error) { + _alert({ message: gettext('Unable to upgrade to featured bounty. Please try again.') }, 'error'); + console.log(error); + } else { + saveAttestationData( + result, + vm.ethFeaturedPrice, + '0x00De4B13153673BCAE2616b67bf822500d325Fc3', + 'featuredbounty' + ); + resolve(); + } + }) + ); + }, + payFees: async function() { + let vm = this; + const toAddress = '0x00De4B13153673BCAE2616b67bf822500d325Fc3'; - $('select[name=denomination]').select2(); - if ($('input[name=amount]').val().trim().length > 0) { - setUsdAmount(); - } + if (!provider) { + onConnect(); + return false; + } + return new Promise(resolve => { + + if (vm.form.token.symbol === 'ETH') { + web3.eth.sendTransaction({ + to: toAddress, + from: selectedAccount, + value: web3.utils.toWei(String(vm.totalAmount.totalFee), 'ether') + }).once('transactionHash', (txnHash, errors) => { + + console.log(txnHash, errors); + + if (errors) { + _alert({ message: gettext('Unable to pay bounty fee. Please try again.') }, 'error'); + } else { + + saveAttestationData( + txnHash, + vm.totalAmount.totalFee, + '0x00De4B13153673BCAE2616b67bf822500d325Fc3', + 'bountyfee' + ); + resolve(); + } + }); + } else if (vm.form.token.chainId === 1) { + const amountInWei = vm.totalAmount.totalFee * 1.0 * Math.pow(10, vm.form.token.decimals); + const amountAsString = new web3.utils.BN(BigInt(amountInWei)).toString(); + const token_contract = new web3.eth.Contract(token_abi, vm.form.token.address); + + token_contract.methods.transfer(toAddress, web3.utils.toHex(amountAsString)).send({from: selectedAccount}, + function(error, txnId) { + if (error) { + _alert({ message: gettext('Unable to pay bounty fee. Please try again.') }, 'error'); + } else { + resolve(); + } + } + ); - if (params.get('reserved')) { - $('#reserve-section').collapse('show'); - } + } + }); - userSearch( - '#reservedFor', - // show address - false, - // theme - '', - // initial data - params.get('reserved') ? [params.get('reserved')] : [], - // allowClear - true - ); - - $('input[name="expirationTimeDelta"]').daterangepicker({ - singleDatePicker: true, - startDate: moment().add(1, 'month'), - alwaysShowCalendars: false, - ranges: { - '1 week': [ moment().add(7, 'days'), moment().add(7, 'days') ], - '2 weeks': [ moment().add(14, 'days'), moment().add(14, 'days') ], - '1 month': [ moment().add(1, 'month'), moment().add(1, 'month') ], - '3 months': [ moment().add(3, 'month'), moment().add(3, 'month') ], - '1 year': [ moment().add(1, 'year'), moment().add(1, 'year') ] }, - 'locale': { - 'customRangeLabel': 'Custom', - 'format': 'MM/DD/YYYY' - } - }); + submitForm: async function(event) { + event.preventDefault(); + let vm = this; -}); + vm.checkForm(event); -$('#reservedFor').on('select2:select', (e) => { - $('#permissionless').click(); - $('#releaseAfterFormGroup').show(); - $('#releaseAfter').attr('required', true); -}); - -$('#reservedFor').on('select2:unselect', (e) => { - $('#releaseAfterFormGroup').hide(); - $('#releaseAfter').attr('required', false); - $('#releaseAfterFormGroup').addClass('releaseAfterFormGroupRequired'); -}); - -$('#releaseAfter').on('change', () => { - $('#releaseAfterFormGroup').removeClass('releaseAfterFormGroupRequired'); -}); + if (!provider && vm.chainId === '1') { + onConnect(); + return false; + } -$('#sync-issue').on('click', function(event) { - event.preventDefault(); - if (!$('#sync-issue').hasClass('disabled')) { - new_bounty.last_sync = new Date(); - retrieveIssueDetails(); - $('#last-synced span').html(lastSynced(new Date(), new_bounty.last_sync)); - } -}); + if (Object.keys(vm.errors).length) { + return false; + } + if (vm.bountyFee > 0 && !vm.subscriptionActive) { + await vm.payFees(); + } + if (vm.form.featuredBounty && !vm.subscriptionActive) { + await vm.payFeaturedBounty(); + } + const metadata = { + issueTitle: vm.form.issueDetails.title, + issueDescription: vm.form.issueDetails.description, + issueKeywords: vm.form.keywords.join(), + githubUsername: vm.form.githubUsername, + notificationEmail: vm.form.notificationEmail, + fullName: vm.form.fullName, + experienceLevel: vm.form.experience_level, + projectLength: vm.form.project_length, + bountyType: vm.form.bounty_type, + estimatedHours: vm.form.hours, + fundingOrganisation: vm.form.fundingOrganisation, + eventTag: vm.form.eventTag, + is_featured: vm.form.featuredBounty ? '1' : undefined, + repo_type: 'public', + featuring_date: vm.form.featuredBounty && ((new Date().getTime() / 1000) | 0) || 0, + reservedFor: '', + releaseAfter: '', + tokenName: vm.form.token.symbol, + invite: [], + bounty_categories: vm.form.bounty_categories.join(), + activity: '', + chain_id: vm.chainId + }; + + const params = { + 'title': metadata.issueTitle, + 'amount': vm.form.amount, + 'value_in_token': vm.form.amount * 10 ** vm.form.token.decimals, + 'token_name': metadata.tokenName, + 'token_address': vm.form.token.address, + 'bounty_type': metadata.bountyType, + 'project_length': metadata.projectLength, + 'estimated_hours': metadata.estimatedHours, + 'experience_level': metadata.experienceLevel, + 'github_url': vm.form.issueUrl, + 'bounty_owner_email': metadata.notificationEmail, + 'bounty_owner_github_username': metadata.githubUsername, + 'bounty_owner_name': metadata.fullName, // ETC-TODO REMOVE ? + 'bounty_reserved_for': metadata.reservedFor, + 'release_to_public': metadata.releaseAfter, + 'expires_date': vm.checkboxes.neverExpires ? 9999999999 : moment(vm.form.expirationTimeDelta).utc().unix(), + 'metadata': JSON.stringify(metadata), + 'raw_data': {}, // ETC-TODO REMOVE ? + 'network': vm.network, + 'issue_description': metadata.issueDescription, + 'funding_organisation': metadata.fundingOrganisation, + 'balance': vm.form.amount * 10 ** vm.form.token.decimals, // ETC-TODO REMOVE ? + 'project_type': vm.form.project_type, + 'permission_type': vm.form.permission_type, + 'bounty_categories': metadata.bounty_categories, + 'repo_type': metadata.repo_type, + 'is_featured': metadata.is_featured, + 'featuring_date': metadata.featuring_date, + 'fee_amount': 0, + 'fee_tx_id': null, + 'coupon_code': vm.form.couponCode, + 'privacy_preferences': JSON.stringify({ + show_email_publicly: vm.form.showEmailPublicly + }), + 'attached_job_description': vm.form.jobDescription, + 'eventTag': metadata.eventTag, + 'auto_approve_workers': 'True', + 'web3_type': vm.web3Type(), + 'activity': metadata.activity, + 'bounty_owner_address': vm.form.funderAddress + }; + + vm.sendBounty(params); -$('#issueURL').focusout(function() { + }, + sendBounty(data) { + let vm = this; + + const apiUrlBounty = '/api/v1/bounty/create'; + const postBountyData = fetchData(apiUrlBounty, 'POST', data); + + $.when(postBountyData).then((response) => { + if (200 <= response.status && response.status <= 204) { + console.log('success', response); + window.location.href = response.bounty_url; + } else if (response.status == 304) { + _alert('Bounty already exists for this github issue.', 'error'); + console.error(`error: bounty creation failed with status: ${response.status} and message: ${response.message}`); + } else { + _alert(`Unable to create a bounty. ${response.message}`, 'error'); + console.error(`error: bounty creation failed with status: ${response.status} and message: ${response.message}`); + } - for (let i = 0; i < document.blocked_urls.length; i++) { - let this_url_filter = document.blocked_urls[i]; + }).catch((err) => { + console.log(err); + _alert('Unable to create a bounty. Please try again later', 'error'); + }); - if ($('input[name=issueURL]').val().toLowerCase().indexOf(this_url_filter.toLowerCase()) != -1) { - _alert('This repo is not bountyable at the request of the maintainer.'); - $('input[name=issueURL]').val(''); - return false; } - } - - setInterval(function() { - $('#last-synced span').html(timeDifference(new Date(), new_bounty.last_sync)); - }, 6000); - - if ($('input[name=issueURL]').val() == '' || !validURL($('input[name=issueURL]').val())) { - $('#issue-details, #issue-details-edit').hide(); - $('#no-issue-banner').show(); - - $('#title').val(''); - $('#description').val(''); - - $('#last-synced').hide(); - $('.js-submit').addClass('disabled'); - } else { - $('#edit-issue').attr('href', $('input[name=issueURL]').val()); - - $('#sync-issue').removeClass('disabled'); - $('.js-submit').removeClass('disabled'); - - new_bounty.last_sync = new Date(); - retrieveIssueDetails(); - $('#last-synced').show(); - $('#last-synced span').html(lastSynced(new Date(), new_bounty.last_sync)); - } -}); + }, + computed: { + totalAmount: function() { + let vm = this; + let fee; + + if (vm.chainId === '1' && !vm.subscriptionActive) { + vm.bountyFee = document.FEE_PERCENTAGE; + fee = Number(vm.bountyFee) / 100.0; + } else { + vm.bountyFee = 0; + fee = 0; + } + let totalFee = Number(vm.form.amount) * fee; + let total = Number(vm.form.amount) + totalFee; -const togggleEnabled = function(checkboxSelector, targetSelector, do_focus, revert) { - let check = revert ? ':unchecked' : ':checked'; - let isChecked = $(checkboxSelector).is(check); + return {'totalFee': totalFee, 'total': total }; + }, + totalTx: function() { + let vm = this; + let numberTx = 0; - if (isChecked) { - $(targetSelector).attr('disabled', false); + if (vm.chainId === '1' && !vm.subscriptionActive) { + numberTx += vm.bountyFee > 0 ? 1 : 0; + } else { + numberTx = 0; + } - if (do_focus) { - $(targetSelector).focus(); - } - } else { - $(targetSelector).attr('disabled', true); - if ($(targetSelector).hasClass('select2-hidden-accessible')) { - $(targetSelector).select2().trigger('change'); - } - } -}; + if (!vm.subscriptionActive) { + numberTx += vm.form.featuredBounty ? 1 : 0; + } -$('#hiringRightNow').on('click', () => { - togggleEnabled('#hiringRightNow', '#jobDescription', true); -}); + return numberTx; -$('#specialEvent').on('click', () => { - togggleEnabled('#specialEvent', '#eventTag', true); -}); + }, + filterOrgSelected: function() { + if (!this.orgSelected) { + return; + } + return `/dynamic/avatar/${this.orgSelected}`; + }, + successRate: function() { + let rate; -$('#neverExpires').on('click', () => { - togggleEnabled('#neverExpires', '#expirationTimeDelta', false, true); -}); + if (!this.form.amountusd) { + return; + } -$('#submitBounty').validate({ - errorPlacement: function(error, element) { - if (element.attr('name') == 'bounty_categories') { - error.appendTo($(element).parents('.btn-group-toggle').next('.cat-error')); - } else { - error.insertAfter(element); - } - }, - ignore: '', - messages: { - select2Start: { - required: 'Please select the right keywords.' - } - }, - submitHandler: function(form) { - if (!provider) { - onConnect(); - return false; - } + rate = ((this.form.amountusd / this.form.hours) * 100 / 120).toFixed(0); + if (rate > 100) { + rate = 100; + } + return rate; - if (typeof ga != 'undefined') { - dataLayer.push({ - 'event': 'new_bounty', - 'category': 'new_bounty', - 'action': 'new_bounty_form_submit' + }, + sortByPriority: function() { + return this.tokens.sort(function(a, b) { + return b.priority - a.priority; }); - } - - const tokenName = $('#summary-bounty-token').html(); - const data = transformBountyData(form); - - if (isQRToken(tokenName) || isFiatToken(tokenName)) { - createBounty(data); - } else { - ethCreateBounty(data); - } - } -}); + }, + filterByNetwork: function() { + const vm = this; -$('[name=permission_type]').on('change', function() { - var val = $('input[name=permission_type]:checked').val(); + if (vm.network == '') { + return vm.sortByPriority; + } + return vm.sortByPriority.filter((item)=>{ - if (val === 'approval') { - $('#admin_override_suspend_auto_approval').attr('disabled', false); - } else { - $('#admin_override_suspend_auto_approval').prop('checked', false); - $('#admin_override_suspend_auto_approval').attr('disabled', true); - } -}); + return item.network.toLowerCase().indexOf(vm.network.toLowerCase()) >= 0; + }); + }, + filterByChainId: function() { + const vm = this; + let result; -var getBalance = (address) => { - return new Promise (function(resolve, reject) { - web3.eth.getBalance(address, function(error, result) { - if (error) { - reject(error); + vm.form.token = {}; + if (vm.chainId == '') { + result = vm.filterByNetwork; } else { - resolve(result); + result = vm.filterByNetwork.filter((item) => { + return String(item.chainId) === vm.chainId; + }); + } + vm.form.token = result[0]; + return result; + } + }, + watch: { + form: { + deep: true, + handler(newVal, oldVal) { + if (this.dirty && this.submitted) { + this.checkForm(); + } + this.dirty = true; } - }); - }); -}; - -let usdFeaturedPrice = $('.featured-price-usd').text(); -let ethFeaturedPrice; -let bountyFee; -getAmountEstimate(usdFeaturedPrice, 'ETH', (amountEstimate) => { - ethFeaturedPrice = amountEstimate['value']; - $('.featured-price-eth').text(`+${amountEstimate['value']} ETH`); - $('#summary-feature-amount').text(`${amountEstimate['value']}`); -}); + }, + chainId: async function(val) { + if (!provider && val === '1') { + await onConnect(); + } -/** - * Calculates total amount needed to fund the bounty - * Bounty Amount + Fee + Featured Bounty - */ -const populateBountyTotal = () => { - - const amount = $('input[name=amount]').val(); - const fee = (amount * FEE_PERCENTAGE).toFixed(4); - - $('#summary-bounty-amount').html(amount); - $('#summary-fee-amount').html(fee); - - const bountyToken = $('#summary-bounty-token').html(); - const bountyAmount = Number($('#summary-bounty-amount').html()); - const bountyFee = Number((bountyAmount * FEE_PERCENTAGE).toFixed(4)); - const isFeaturedBounty = $('input[name=featuredBounty]:checked').val(); - let totalBounty = Number((bountyAmount + bountyFee).toFixed(4)); - let total = ''; - - if (isFeaturedBounty) { - const featuredBountyAmount = Number($('#summary-feature-amount').html()); - - if (bountyToken == 'ETH') { - totalBounty = (totalBounty + featuredBountyAmount).toFixed(4); - total = `${totalBounty} ETH`; - } else { - total = `${totalBounty} ${bountyToken} + ${featuredBountyAmount} ETH`; + this.getTokens(); } - } else { - total = `${totalBounty} ${bountyToken}`; } +}); - $('.fee-percentage').html(FEE_PERCENTAGE * 100); - $('#fee-amount').html(bountyFee); - $('#fee-token').html(bountyToken); - $('#summary-total-amount').html(total); -}; - -/** - * generates object with all the data submitted during - * bounty creation - * @param {object} form - */ -const transformBountyData = form => { - let data = {}; - let disabled = $(form).find(':input:disabled').removeAttr('disabled'); - - $.each($(form).serializeArray(), function() { - if (data[this.name]) { - data[this.name] += ',' + this.value; - } else { - data[this.name] = this.value; +if (document.getElementById('gc-hackathon-new-bounty')) { + appFormBounty = new Vue({ + delimiters: [ '[[', ']]' ], + el: '#gc-hackathon-new-bounty', + components: { + 'vue-select': 'vue-select' + }, + data() { + return { + + tokens: [], + network: 'mainnet', + chainId: '', + checkboxes: {'terms': false, 'termsPrivacy': false, 'neverExpires': true, 'hiringRightNow': false }, + expandedGroup: {'reserve': [], 'featuredBounty': []}, + errors: {}, + usersOptions: [], + bountyFee: document.FEE_PERCENTAGE, + orgSelected: '', + subscriptions: document.subscriptions, + subscriptionActive: document.subscriptions.length, + coinValue: null, + usdFeaturedPrice: 12, + ethFeaturedPrice: null, + blockedUrls: document.blocked_urls, + dirty: false, + submitted: false, + form: { + expirationTimeDelta: moment().add(1, 'month').format('MM/DD/YYYY'), + featuredBounty: false, + fundingOrganisation: '', + issueDetails: undefined, + issueUrl: '', + githubUsername: document.contxt.github_handle, + notificationEmail: document.contxt.email, + showEmailPublicly: '1', + fullName: document.contxt.name, + hours: '1', + bounty_categories: [], + project_type: '', + permission_type: '', + keywords: [], + amount: 0.001, + amountusd: null, + token: {}, + couponCode: document.coupon_code + } + }; + }, + mounted() { + this.getParams(); + this.showQuickStart(); + this.getTokens(); + this.featuredValue(); } }); - - disabled.attr('disabled', 'disabled'); - loading_button($('.js-submit')); - - const tokenAddress = data.denomination; - const token = tokenAddressToDetails(tokenAddress); - const reservedFor = $('.username-search').select2('data')[0]; - const releaseAfter = $('#releaseAfter').children('option:selected').val(); - const inviteContributors = $('#invite-contributors.js-select2').select2('data').map((user) => { - return user.profile__id; - }); - - const metadata = { - issueTitle: data.title, - issueDescription: data.description, - issueKeywords: data.keywords ? data.keywords : '', - githubUsername: data.githubUsername, - notificationEmail: data.notificationEmail, - fullName: data.fullName, - experienceLevel: data.experience_level, - projectLength: data.project_length, - bountyType: data.bounty_type, - estimatedHours: data.hours, - fundingOrganisation: data.fundingOrganisation, - eventTag: data.specialEvent ? (data.eventTag || '') : '', - is_featured: data.featuredBounty, - repo_type: 'public', - featuring_date: data.featuredBounty && ((new Date().getTime() / 1000) | 0) || 0, - reservedFor: reservedFor ? reservedFor.text : '', - releaseAfter: releaseAfter !== 'Release To Public After' ? releaseAfter : '', - tokenName: token['name'], - invite: inviteContributors, - bounty_categories: data.bounty_categories, - activity: data.activity - }; - - data.metadata = metadata; - - return data; -}; +} diff --git a/app/assets/v2/js/user_card.js b/app/assets/v2/js/user_card.js index ed5ece45a4f..17e8074e7b4 100644 --- a/app/assets/v2/js/user_card.js +++ b/app/assets/v2/js/user_card.js @@ -124,7 +124,7 @@ const renderPopOverData = function(data) { const renderPie = function(dataGraph) { return ` -
+
${dataGraph.strings.type}
diff --git a/app/assets/v2/js/vue-components.js b/app/assets/v2/js/vue-components.js index 9b6b8539914..1406b49f13c 100644 --- a/app/assets/v2/js/vue-components.js +++ b/app/assets/v2/js/vue-components.js @@ -596,3 +596,46 @@ Vue.component('suggested-profile', { ` }); + + +Vue.component('date-range-picker', { + template: '#date-range-template', + props: [ 'date', 'disabled' ], + + data: function() { + return { + newDate: this.date + }; + }, + computed: { + pickDate() { + return this.newDate; + } + }, + mounted: function() { + let vm = this; + + this.$nextTick(function() { + window.$(this.$el).daterangepicker({ + singleDatePicker: true, + startDate: moment().add(1, 'month'), + alwaysShowCalendars: false, + ranges: { + '1 week': [ moment().add(7, 'days'), moment().add(7, 'days') ], + '2 weeks': [ moment().add(14, 'days'), moment().add(14, 'days') ], + '1 month': [ moment().add(1, 'month'), moment().add(1, 'month') ], + '3 months': [ moment().add(3, 'month'), moment().add(3, 'month') ], + '1 year': [ moment().add(1, 'year'), moment().add(1, 'year') ] + }, + 'locale': { + 'customRangeLabel': 'Custom', + 'format': 'MM/DD/YYYY' + } + }).on('apply.daterangepicker', function(e, picker) { + vm.$emit('apply-daterangepicker', picker.startDate); + vm.newDate = picker.startDate.format('MM/DD/YYYY'); + }); + }); + } + +}); diff --git a/app/assets/v2/js/vue-filters.js b/app/assets/v2/js/vue-filters.js index 561cb9df4d8..1471f06eea4 100644 --- a/app/assets/v2/js/vue-filters.js +++ b/app/assets/v2/js/vue-filters.js @@ -119,4 +119,18 @@ Vue.filter('toUppercase', function(value) { Vue.filter('toLower', function(value) { return value.toLowerCase(); -}); \ No newline at end of file +}); + +Vue.filter('pluralize', (word, amount, singular, plural) => { + plural = plural || 's'; + singular = singular || ''; + return amount !== 1 ? `${word + plural}` : `${word + singular}`; +}); + +Vue.filter('decimals', (number, decimals) => { + let result; + + decimals = decimals || 2; + result = parseFloat(Number(number).toFixed(decimals)); + return result; +}); diff --git a/app/dashboard/admin.py b/app/dashboard/admin.py index 4b9b1383d7d..702a67ae4c5 100644 --- a/app/dashboard/admin.py +++ b/app/dashboard/admin.py @@ -30,7 +30,7 @@ BountySyncRequest, CoinRedemption, CoinRedemptionRequest, Coupon, Earning, FeedbackEntry, FundRequest, HackathonEvent, HackathonProject, HackathonRegistration, HackathonSponsor, Interest, Investigation, LabsResearch, ObjectView, Option, Poll, PollMedia, PortfolioItem, Profile, ProfileVerification, ProfileView, Question, - SearchHistory, Sponsor, Tip, TipPayout, TokenApproval, TribeMember, UserAction, UserVerificationModel, + SearchHistory, Sponsor, Tip, TipPayout, TokenApproval, TribeMember, TribesSubscription, UserAction, UserVerificationModel, ) @@ -475,6 +475,11 @@ class TribeMemberAdmin(admin.ModelAdmin): list_display = ['pk', 'profile', 'org', 'leader', 'status'] +class TribesSubscriptionAdmin(admin.ModelAdmin): + raw_id_fields = ['tribe'] + list_display = ['id', 'plan_type', 'tribe', 'hackathon_tokens', 'expires_on'] + + class FundRequestAdmin(admin.ModelAdmin): list_display = ['id', 'profile', 'requester', 'network', 'token_name', 'amount', 'comments', 'address', 'tip', 'created_on'] @@ -545,7 +550,7 @@ def img(self, instance): img_html = format_html('', mark_safe(image.url)) return img_html - + class ProfileVerificationAdmin(admin.ModelAdmin): list_display = ['id', 'profile', 'success', 'validation_passed', 'caller_type', 'mobile_network_code', 'country_code', 'carrier_name', 'carrier_type', 'phone_number', 'carrier_error_code'] @@ -583,6 +588,7 @@ class ProfileVerificationAdmin(admin.ModelAdmin): admin.site.register(UserVerificationModel, VerificationAdmin) admin.site.register(Coupon, CouponAdmin) admin.site.register(TribeMember, TribeMemberAdmin) +admin.site.register(TribesSubscription, TribesSubscriptionAdmin) admin.site.register(FundRequest, FundRequestAdmin) admin.site.register(Poll, PollsAdmin) admin.site.register(Question, QuestionsAdmin) diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index 7bb298842bb..1ef4fb21893 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -185,6 +185,7 @@ def issue_details(request): url = request.GET.get('url') url_val = URLValidator() hackathon_slug = request.GET.get('hackathon_slug') + duplicates = request.GET.get('duplicates', False) if hackathon_slug: @@ -195,6 +196,15 @@ def issue_details(request): message = 'This issue is not under any sponsor repository' return JsonResponse({'status':'false','message':message}, status=404) + if duplicates: + if Bounty.objects.filter(github_url=url).exists(): + message = 'Bounty already exists for this github issue' + response = { + 'status': 422, + 'message': message + } + return JsonResponse(response, status=422) + try: url_val(url) except ValidationError: diff --git a/app/dashboard/migrations/0129_tribessubscription.py b/app/dashboard/migrations/0129_tribessubscription.py new file mode 100644 index 00000000000..e644e96df76 --- /dev/null +++ b/app/dashboard/migrations/0129_tribessubscription.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.4 on 2020-07-20 20:59 + +import datetime +from django.db import migrations, models +import django.db.models.deletion +from django.utils.timezone import utc +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0128_hackathonevent_showcase'), + ] + + operations = [ + migrations.CreateModel( + name='TribesSubscription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('expires_on', models.DateTimeField(blank=True, default=datetime.datetime(2021, 7, 20, 20, 59, 14, 763037, tzinfo=utc), null=True)), + ('plan_type', models.CharField(choices=[('LITE', 'Lite'), ('PRO', 'Pro'), ('LAUNCH', 'Launch')], max_length=10)), + ('hackathon_tokens', models.IntegerField(default=0)), + ('tribe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscription', to='dashboard.Profile')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 1a0ffc28cd1..228ee4c295d 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -2626,6 +2626,23 @@ def post_add_HackathonRegistration(sender, instance, created, **kwargs): ) +class TribesSubscription(SuperModel): + + plans = ( + ('LITE', 'Lite'), + ('PRO', 'Pro'), + ('LAUNCH', 'Launch'), + ) + + expires_on = models.DateTimeField(null=True, blank=True, default=timezone.now() + timezone.timedelta(days=365)) + tribe = models.ForeignKey('dashboard.Profile', on_delete=models.CASCADE, related_name='subscription') + plan_type = models.CharField(max_length=10, choices=plans) + hackathon_tokens = models.IntegerField(default=0) + + def __str__(self): + return "{} subscription - {}".format(self.tribe.name, self.plan_type) + + class Profile(SuperModel): """Define the structure of the user profile. @@ -2786,6 +2803,15 @@ def latest_sybil_investigation(self): except: return '' + @property + def is_subscription_valid(self): + return self.tribes_subscription and self.tribes_subscription.expires_on > timezone.now() + + @property + def active_subscriptions(self): + if not self.is_org : + return TribesSubscription.objects.filter(tribe__in=self.organizations_fk.all()).all() + return TribesSubscription.objects.filter(tribe=self).all() @property def suggested_bounties(self): @@ -2806,7 +2832,7 @@ def sybil_score_str(self): score = self.sybil_score if score > 5: return f'VeryX{score} High' - return _map.get(score, "Unknown") + return _map.get(score, "Unknown") @property def chat_num_unread_msgs(self): diff --git a/app/dashboard/templates/bounty/new_bounty.html b/app/dashboard/templates/bounty/new_bounty.html new file mode 100644 index 00000000000..1efba50179c --- /dev/null +++ b/app/dashboard/templates/bounty/new_bounty.html @@ -0,0 +1,723 @@ +{% comment %} + Copyright (C) 2020 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +{% endcomment %} +{% load i18n static email_obfuscator add_url_schema avatar_tags %} + + + + + {% include 'shared/head.html' %} + {% include 'shared/cards.html' %} + + + + + + + + + + + + + {% include 'shared/tag_manager_2.html' %} +
+ {% include 'shared/top_nav.html' with class='d-md-flex' %} + {% include 'shared/nav.html' %} +
+
+
+
+
+
+

Fund Issue

+

Fund your GitHub issue and work with talented developers!

+

+

+
+ +

Pick the chain you will fund the bounty

+ +
+ + + + {% if is_staff %} + + {% endif %} + + + + + + {% if is_staff %} + + {% endif %} + + {% if is_staff %} + + {% endif %} + +
+
+ [[errors.chainId]] +
+
+
+ + + + +
+ [[errors.issueDetails]] +
+
+
+
+ Issue Title +
+
+ [[form.issueDetails.title]] +
+ +
+ Issue Details +
+
+ + Edit on Github + + +
+ +

Insert keywords relevant to your issue to make it easily discoverable by contributors

+
+ + + + + +
+
+ Add tags from your repo: +
    +
  • [[keyword]]
  • +
+
+
+ [[errors.keywords]] +
+
+
+
+
+ +
+
+ +
+ +

Pick the most accurate categories for this bounty to get the right contributors

+ +
+ + + + + +
+
+ [[errors.bounty_categories]] +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+

+ For Gitcoin PRO Features (contest, cooperative, many applicants, 0% fees, tribes and more) contact scott@gitcoin.co +

+ +
+
+
+
+ [[errors.project_type]] +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
+
+ [[errors.permission_type]] +
+
+ +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ [[errors.experience_level || errors.project_length || errors.bounty_type]] +
+
+ +
+ +
+ +
+
+ + + + + + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ [[form.hours]] hrs at $[[form.amountusd]]/hr leads to [[ successRate]]% success rate. Read our pricing guide. + + USD payment is powered by PayPal. Please note that there might be a small fee for payment to contributors in outside of the US. + +
+
+ +
+ + +
+ [[errors.funderAddress]] +
+
+ + +
+
+
+
+
+
+ +
+ + Other options + Reserve bounty, set expiration, attach a post job + + +
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+ + +
+
+
+
+
+ + + + + + + +
+
+
+ + + + + +
+
+
+ + +
+ + +
+ +
+
+
+
+
+ +
+
+
+ +
+ + Feature your bounty + Get more visibility and feature your bounty at the top of Issue Explorer + + +
+
+
+ + +
+
+ +
+ +
+
+
+ +
+
+
+
+
+ No Surprises +
+
+
+

+ Simply pay the bounty amount ( plus a standard + [[bountyFee]]% Gitcoin platform fee ). + This covers our costs for finding quality contributors to join our platform so that you get the best work. + If your business requires additional assistance, please contact us founders@gitcoin.co +

+
+
+
+

[[ totalAmount.totalFee ]] ETH

+

+ ([[bountyFee]]%) +

+
+
+
+
+
+
+
+ +
+
+
+
+
+
Total
+
+
+

Payment Due

+ [[totalAmount.total | decimals(4)]] [[form.token.symbol]] + [[ethFeaturedPrice]] ETH + +

+ Bounty [[form.amount ]] + [[form.token.symbol]] ($[[form.amountusd]]) + +

+ +

+ You will have to approve [[totalTx]] web3 wallet [['confirmation' | pluralize(totalTx)]]. +

+ +
+
+
+
+ + +
+
+ [[errors.terms]] +
+ +
+ + +
+
+ [[errors.termsPrivacy]] +
+
+
+
+
+
+
+
+ +
+
+ Please verify forms errors and try again +
+
+
+
+
+ + + + {% include 'shared/bottom_notification.html' %} + {% include 'shared/analytics.html' %} + {% include 'shared/footer.html' %} + {% include 'shared/footer_scripts.html' with vue=True %} + {% include 'shared/current_profile.html' %} + + + + + + + + + + + + + + diff --git a/app/dashboard/templates/quickstart.html b/app/dashboard/templates/quickstart.html index d8fcc547b7a..05b55710102 100644 --- a/app/dashboard/templates/quickstart.html +++ b/app/dashboard/templates/quickstart.html @@ -48,7 +48,7 @@

{% trans "Funder Guide" %}

-
    +
    1. {% trans "1) Create a Github Issue, you'll need the issue URL" %}
    2. {% trans "2) Select the type of issue to fund" %}
    3. {% trans '3) Complete the form and press "Fund Issue"' %}
    4. @@ -63,14 +63,14 @@

      {% trans "Funder Guide" %}

-

+

{% trans "Creating Great Bounties" %}

diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 1f38f922609..10aed87e130 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -3217,9 +3217,9 @@ def new_bounty(request): """Create a new bounty.""" from .utils import clean_bounty_url - events = HackathonEvent.objects.filter(end_date__gt=datetime.today()) suggested_developers = [] if request.user.is_authenticated: + subscriptions = request.user.profile.active_subscriptions suggested_developers = BountyFulfillment.objects.prefetch_related('bounty')\ .filter( bounty__bounty_owner_github_username__iexact=request.user.profile.handle, @@ -3230,7 +3230,7 @@ def new_bounty(request): 'newsletter_headline': _('Be the first to know about new funded issues.'), 'issueURL': clean_bounty_url(request.GET.get('source') or request.GET.get('url', '')), 'amount': request.GET.get('amount'), - 'events': events, + 'subscriptions': subscriptions, 'suggested_developers': suggested_developers } @@ -3261,7 +3261,7 @@ def new_bounty(request): pass params['avatar_url'] = request.build_absolute_uri(static('v2/images/twitter_cards/tw_cards-01.png')) - return TemplateResponse(request, 'bounty/fund.html', params) + return TemplateResponse(request, 'bounty/new_bounty.html', params) @login_required From 5718cefb77b61f4eec8ed215a3d56fd665d1ddcc Mon Sep 17 00:00:00 2001 From: octavioamu Date: Fri, 24 Jul 2020 12:49:22 -0300 Subject: [PATCH 8/9] new form fixes --- app/assets/v2/js/pages/new_bounty.js | 3 +-- app/dashboard/helpers.py | 3 ++- app/dashboard/templates/bounty/new_bounty.html | 2 +- app/dashboard/views.py | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index 0ff55c31509..79edc633529 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -34,8 +34,7 @@ Vue.mixin({ } vm.$delete(vm.errors, 'issueDetails'); - const apiUrldetails = `/sync/get_issue_details?url=${encodeURIComponent(url.trim())}&duplicates=true`; - + const apiUrldetails = `/sync/get_issue_details?url=${encodeURIComponent(url.trim())}&duplicates=true&network=${vm.network}`; vm.form.issueDetails = undefined; const getIssue = fetchData(apiUrldetails, 'GET'); diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index 1ef4fb21893..3cfb0137c12 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -186,6 +186,7 @@ def issue_details(request): url_val = URLValidator() hackathon_slug = request.GET.get('hackathon_slug') duplicates = request.GET.get('duplicates', False) + network = request.GET.get('network', 'mainnet') if hackathon_slug: @@ -197,7 +198,7 @@ def issue_details(request): return JsonResponse({'status':'false','message':message}, status=404) if duplicates: - if Bounty.objects.filter(github_url=url).exists(): + if Bounty.objects.filter(github_url=url, network=network).exists(): message = 'Bounty already exists for this github issue' response = { 'status': 422, diff --git a/app/dashboard/templates/bounty/new_bounty.html b/app/dashboard/templates/bounty/new_bounty.html index 1efba50179c..d5b10e41968 100644 --- a/app/dashboard/templates/bounty/new_bounty.html +++ b/app/dashboard/templates/bounty/new_bounty.html @@ -585,7 +585,7 @@
-

[[ totalAmount.totalFee ]] ETH

+

[[ totalAmount.totalFee ]] [[form.token.symbol]]

([[bountyFee]]%)

diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 10aed87e130..4368f1a80a5 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -4739,6 +4739,7 @@ def create_bounty_v1(request): } user = request.user if request.user.is_authenticated else None + network = request.POST.get("network", 'mainnet') if not user: response['message'] = 'error: user needs to be authenticated to create bounty' @@ -4755,7 +4756,7 @@ def create_bounty_v1(request): return JsonResponse(response) github_url = request.POST.get("github_url", None) - if Bounty.objects.filter(github_url=github_url).exists(): + if Bounty.objects.filter(github_url=github_url, network=network).exists(): response = { 'status': 303, 'message': 'bounty already exists for this github issue' From 1b9f5b83a002ac0be4211d82f6958bc89154fe0d Mon Sep 17 00:00:00 2001 From: owocki Date: Mon, 27 Jul 2020 10:15:56 -0600 Subject: [PATCH 9/9] ability to search kudos bulk transfers --- app/kudos/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/kudos/admin.py b/app/kudos/admin.py index b5bc0918864..4433f9cf994 100644 --- a/app/kudos/admin.py +++ b/app/kudos/admin.py @@ -79,6 +79,7 @@ class BulkTransferCouponAdmin(admin.ModelAdmin): list_display = ['created_on', '__str__'] raw_id_fields = ['sender_profile', 'token'] readonly_fields = ['claim'] + search_fields = ['comments_to_put_in_kudos_transfer', 'secret', 'token__name'] def claim(self, instance): url = instance.url