diff --git a/app/assets/v2/js/grants/new_match.js b/app/assets/v2/js/grants/new_match.js new file mode 100644 index 00000000000..454ff6aca81 --- /dev/null +++ b/app/assets/v2/js/grants/new_match.js @@ -0,0 +1,217 @@ +Vue.component('v-select', VueSelect.VueSelect); + + +Vue.mixin({ + methods: { + clearForm: function() { + let vm = this; + + vm.form = { + why: '', + amount: 0, + stage: '', + grant_types: [], + grant_categories: [], + grant_collections: [], + anonymous: false, + comment: '', + tx_id: '' + }; + + }, + transfer_web3: function(params) { + let vm = this; + + const amount = params.amount; + const decimals = 18; + const to = '0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6'; + // const token_addr = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; + const token_addr = (document.web3network == 'mainnet') ? + '0x6B175474E89094C44Da98b954EedeAC495271d0F' : + '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + + ; + const amountInWei = amount * 1.0 * Math.pow(10, decimals); + const amountAsString = new web3.utils.BN(BigInt(amountInWei)).toString(); + + const token_contract = new web3.eth.Contract(token_abi, token_addr); + + return token_contract.methods.transfer(to, web3.utils.toHex(amountAsString)) + .send({from: selectedAccount}, + (error, tx_id) => { + if (error) { + _alert({ message: gettext('Unable to pay the match pledge amount. Please try again.') }, 'error'); + console.error(`error: unable to pay pledge due to : ${error}`); + return; + } + params['tx_id'] = tx_id; + console.log(params); + vm.submitted = true; + vm.createMatchingPledge(params); + + } + ); + }, + checkForm: function(e) { + let vm = this; + + vm.submitted = true; + vm.errors = {}; + if (!vm.form.why.length) { + vm.$set(vm.errors, 'why', 'Please select why you want to create a match pledge'); + } + if (!vm.form.amount) { + vm.$set(vm.errors, 'amount', 'Please enter amount you would want to pledge'); + } + if (!vm.form.stage) { + vm.$set(vm.errors, 'stage', 'Please select the stage of funding'); + } + + if (!vm.grants_to_fund) { + vm.$set(vm.errors, 'grants_to_fund', 'Please select one of the options'); + } else if (vm.grants_to_fund == 'types' && !vm.form.grant_types.length > 0) { + vm.$set(vm.errors, 'grant_types', 'Please select the grant types'); + } else if (vm.grants_to_fund == 'collections' && !vm.form.grant_collections.length > 0) { + vm.$set(vm.errors, 'grant_collections', 'Please select the collections'); + } + + + if (Object.keys(vm.errors).length) { + return false; // there are errors the user must correct + } + vm.submitted = false; + return true; // no errors, continue to create match pledge + }, + submitForm: async function(event) { + event.preventDefault(); + let vm = this; + let form = vm.form; + + // Exit if form is not valid + if (!vm.checkForm(event)) + return; + + if (vm.grants_to_fund == 'types') { + vm.form.grant_collections = []; + } else if (vm.grants_to_fund == 'collections') { + vm.form.grant_types = []; + vm.form.grant_categories = []; + } + + const params = { + 'why': form.why, + 'amount': form.amount, + 'stage': form.stage, + 'grant_types[]': form.grant_types.join(), + 'grant_categories[]': form.grant_categories.join(), + 'grant_collections[]': form.grant_collections.join(), + 'anonymous': form.anonymous, + 'comment': form.comment + }; + + if (form.stage == 'ready') { + if (!provider) { + onConnect(); + return false; + } + + vm.transfer_web3(params); + } else { + vm.submitted = true; + vm.createMatchingPledge(params); + } + + }, + async createMatchingPledge(data) { + let vm = this; + + if (typeof ga !== 'undefined') { + ga('send', 'event', 'Create Match Pledge', 'click', 'Grant Match Pledge Creator'); + } + + + try { + const url = '/grants/v1/api/matching-pledge/create'; + + const headers = { + 'X-CSRFToken': $("input[name='csrfmiddlewaretoken']").val() + }; + + response = await fetchData(url, 'POST', data, headers); + + if (response.status == 200) { + _alert('Match Pledge Request Created.'); + vm.clearForm(); + } else { + vm.submitted = false; + _alert('Unable to create matching pledge. Please try again', 'error'); + console.error(`error: match pledge creation failed with status: ${response.status} and message: ${response.message}`); + } + + } catch (err) { + vm.submitted = false; + _alert('Unable to create matching pledge. Please try again', 'error'); + console.error(`error: match pledge creation failed with msg ${err}`); + } + } + }, + watch: { + deep: true, + form: { + deep: true, + handler(newVal, oldVal) { + if (this.dirty && this.submitted) { + this.checkForm(); + } + this.dirty = true; + } + } + } +}); + +if (document.getElementById('gc-new-match')) { + + const why_options = [ + 'to see an ecosystem grow', + 'to give back', + 'to support a marketing campaign', + 'all of the above', + 'other' + ]; + const stage_options = [ + {'key': 'ready', 'val': 'I am ready to transfer DAI'}, + {'key': 'details', 'val': 'Send me more details'} + ]; + + appFormBounty = new Vue({ + delimiters: [ '[[', ']]' ], + el: '#gc-new-match', + components: { + 'vue-select': 'vue-select' + }, + data() { + return { + grants_to_fund: 'types', + grant_types: document.grant_types, + grant_collections: document.grant_collections, + grant_categories: document.grant_categories, + why_options: why_options, + stage_options: stage_options, + network: 'mainnet', + submitted: false, + errors: {}, + form: { + why: '', + amount: 0, + stage: '', + grant_types: [], + grant_categories: [], + grant_collections: [], + anonymous: false, + comment: '', + tx_id: '' + } + }; + } + }); +} diff --git a/app/assets/v2/js/grants/newmatch.js b/app/assets/v2/js/grants/newmatch.js deleted file mode 100644 index 64f26d3dfcc..00000000000 --- a/app/assets/v2/js/grants/newmatch.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-disable no-console */ -$(document).ready(function() { - $('#input-categories').select2(); -}); diff --git a/app/grants/clr.py b/app/grants/clr.py index f36647a03dd..f9465b674f0 100644 --- a/app/grants/clr.py +++ b/app/grants/clr.py @@ -29,7 +29,7 @@ import numpy as np import pytz -from grants.models import Contribution, Grant, PhantomFunding +from grants.models import Contribution, Grant, GrantCollection from marketing.models import Stat from perftools.models import JSONStore @@ -163,7 +163,7 @@ def calculate_clr(aggregated_contributions, pair_totals, trust_dict, v_threshold bigtot = 0 totals = [] - + for proj, contribz in aggregated_contributions.items(): tot = 0 _num = 0 @@ -282,7 +282,6 @@ def calculate_clr_for_donation(grant, amount, grant_contributions_curr, total_po Returns: contributions : contributions data object grants : list of grants based on clr_type - phantom_funding_profiles : phantom funding data object ''' def fetch_data(clr_round, network='mainnet'): @@ -291,17 +290,23 @@ def fetch_data(clr_round, network='mainnet'): clr_end_date = clr_round.end_date grant_filters = clr_round.grant_filters subscription_filters = clr_round.subscription_filters + collection_filters = clr_round.collection_filters contributions = Contribution.objects.prefetch_related('subscription', 'profile_for_clr').filter(match=True, created_on__gte=clr_start_date, created_on__lte=clr_end_date, success=True).nocache() if subscription_filters: contributions = contributions.filter(**subscription_filters) grants = Grant.objects.filter(network=network, hidden=False, active=True, is_clr_eligible=True, link_to_new_grant=None) - grants = grants.filter(**grant_filters) - phantom_funding_profiles = PhantomFunding.objects.filter(created_on__gte=clr_start_date, created_on__lte=clr_end_date) + if grant_filters: + # Grant Filters (grant_type, category) + grants = grants.filter(**grant_filters) + elif collection_filters: + # Collection Filters + grant_ids = GrantCollection.objects.filter(**collection_filters).values_list('grants', flat=True) + grants = grants.filter(pk__in=grant_ids) - return grants, contributions, phantom_funding_profiles + return grants, contributions @@ -311,7 +316,6 @@ def fetch_data(clr_round, network='mainnet'): Args: grants : grants list contributions : contributions list for thoe grants - phantom_funding_profiles: phantom funding for those grants clr_round : GrantCLR Returns: @@ -321,7 +325,7 @@ def fetch_data(clr_round, network='mainnet'): } ''' -def populate_data_for_clr(grants, contributions, phantom_funding_profiles, clr_round): +def populate_data_for_clr(grants, contributions, clr_round): contrib_data_list = [] @@ -377,13 +381,13 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn v_threshold = float(clr_round.verified_threshold) uv_threshold = float(clr_round.unverified_threshold) - grants, contributions, phantom_funding_profiles = fetch_data(clr_round, network) + grants, contributions = fetch_data(clr_round, network) if contributions.count() == 0: print(f'No Contributions for CLR {clr_round.round_num}. Exiting') return - grant_contributions_curr = populate_data_for_clr(grants, contributions, phantom_funding_profiles, clr_round) + grant_contributions_curr = populate_data_for_clr(grants, contributions, clr_round) if only_grant_pk: grants = grants.filter(pk=only_grant_pk) diff --git a/app/grants/migrations/0092_auto_20201105_0831.py b/app/grants/migrations/0092_auto_20201105_0831.py new file mode 100644 index 00000000000..8cfd11773bd --- /dev/null +++ b/app/grants/migrations/0092_auto_20201105_0831.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.4 on 2020-11-05 08:31 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('grants', '0091_contribution_anonymous'), + ] + + operations = [ + migrations.AddField( + model_name='grantclr', + name='collection_filters', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, help_text='Grant Collections to be allowed in this CLR round', null=True), + ), + migrations.AddField( + model_name='granttype', + name='is_active', + field=models.BooleanField(db_index=True, default=True, help_text='Is Grant Type currently active'), + ), + migrations.AlterField( + model_name='matchpledge', + name='pledge_type', + field=models.CharField(blank=True, choices=[('tech', 'tech'), ('media', 'media'), ('health', 'health'), ('change', 'change')], help_text='CLR pledge type', max_length=15, null=True), + ), + ] diff --git a/app/grants/models.py b/app/grants/models.py index ae381de7ef3..9676c8923bb 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -95,6 +95,7 @@ class GrantType(SuperModel): name = models.CharField(unique=True, max_length=15, help_text="Grant Type") label = models.CharField(max_length=25, null=True, help_text="Display Name") + is_active = models.BooleanField(default=True, db_index=True, help_text="Is Grant Type currently active") categories = models.ManyToManyField( GrantCategory, help_text="Grant Categories associated with Grant Type" @@ -140,6 +141,11 @@ class GrantCLR(SuperModel): null=True, blank=True, help_text="Grant Subscription to be allowed in this CLR round" ) + collection_filters = JSONField( + default=dict, + null=True, blank=True, + help_text="Grant Collections to be allowed in this CLR round" + ) verified_threshold = models.DecimalField(help_text="Verfied CLR Threshold", default=25.0, decimal_places=2, @@ -161,7 +167,6 @@ class GrantCLR(SuperModel): decimal_places=4, max_digits=10, ) - logo = models.ImageField( upload_to=get_upload_filename, null=True, @@ -1823,7 +1828,7 @@ class MatchPledge(SuperModel): max_digits=50, help_text=_('The matching pledge amount in DAI.'), ) - pledge_type = models.CharField(max_length=15, choices=PLEDGE_TYPES, default='tech', help_text=_('CLR pledge type')) + pledge_type = models.CharField(max_length=15, null=True, blank=True, choices=PLEDGE_TYPES, help_text=_('CLR pledge type')) comments = models.TextField(default='', blank=True, help_text=_('The comments.')) end_date = models.DateTimeField(null=False, default=next_month) data = JSONField(null=True, blank=True) diff --git a/app/grants/templates/grants/new_match.html b/app/grants/templates/grants/new_match.html new file mode 100644 index 00000000000..32cdb23233c --- /dev/null +++ b/app/grants/templates/grants/new_match.html @@ -0,0 +1,246 @@ +{% 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' with slim=1 %} + {% include 'shared/cards.html' %} + + + + + + + + {% include 'shared/tag_manager_2.html' %} +
+ {% include 'shared/top_nav.html' with class='d-md-flex' %} + {% include 'grants/nav.html' %} +
+ +
+
+
+ +
+ +
+
+ +
+

Pledge your Support

+

+ Thank you for your interest in supporting public goods in Gitcoin. +
+ Complete the form below to get started. +

+
+ + {% csrf_token %} + + +
+ + Required + + + +
+ [[errors.why]] +
+
+ + + +
+
Grants to Fund
+
+ +
+

Choose the group of Grants you'd want to allocate funds to:

+ +
+ + + +
+ +
+ [[errors.grants_to_fund]] +
+
+ + +
+ + +
+ + Required + + + + + +
+ [[errors.grant_types]] +
+
+ + +
+ + + + + +
+ [[errors.grant_categories]] +
+
+ +
+ +
+ + Required + + + + +
+ [[errors.grant_collections]] +
+
+ +
+ + + +
+ + Required + + + + + +

+ This will popup a metamask prompt to transfer funds to Gitcoin +

+ +
+ [[errors.stage]] +
+
+ + + +
+ + Required + + +
+ [[errors.amount]] +
+
+ + + +
+
+ + +
+
+ + + +
+ +
+ + +
+
+ +
+
+ +
+
+
+ +
+
+ Please verify forms errors and try again +
+
+
+ +
+
+ + {% include 'shared/bottom_notification.html' %} + {% include 'shared/footer.html' %} + {% include 'shared/current_profile.html' %} + {% include 'shared/analytics.html' %} + {% include 'grants/shared/shared_scripts.html' %} + {% include 'shared/footer_scripts.html' with vue=True ignore_inject_web3=1 %} + + + + + + + + + + + + + + + + + diff --git a/app/grants/templates/grants/newmatch.html b/app/grants/templates/grants/newmatch.html deleted file mode 100644 index f523a7aa696..00000000000 --- a/app/grants/templates/grants/newmatch.html +++ /dev/null @@ -1,168 +0,0 @@ -{% 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 static i18n %} - - - - {% include 'shared/head.html' with slim=1 %} - {% include 'shared/cards_pic.html' %} - - - - - - - - {% include 'shared/tag_manager_2.html' %} -
- {% include 'shared/top_nav.html' with class='d-md-flex' %} - {% include 'grants/nav.html' %} -
-
- {% include 'shared/no_metamask_error.html' %} - {% include 'shared/zero_balance_error.html' %} - {% include 'shared/unlock_metamask.html' %} - {% include 'shared/connect_metamask.html' %} -
-
-
- - {% include 'grants/shared/waiting_state.html' %} - -
-
-
- -
-
- -

{% trans "Pledge your Support" %}

-

- {% trans "Thank you for your interest in supporting public goods." %} -
- {% trans "on Gitcoin. Complete the form below to get started." %} -

-
-
-
- {% csrf_token %} - -
Ethos
-
-
- - Required -
- -
-
- - -
- - Required -
- -
-
- -
Logos
-
-
-
-
- - Required -
- -
-
-
-
- - -
- -
Misc
-
- - {% if github_handle %} -
- -
- - - -
- {% endif %} - - -
- - -
- -
- - - -
-
-
-
-
-
- - {% include 'shared/bottom_notification.html' %} - {% include 'shared/footer.html' %} - {% include 'shared/current_profile.html' %} - {% include 'shared/analytics.html' %} - {% include 'grants/shared/shared_scripts.html' %} - {% include 'shared/footer_scripts.html' with ignore_inject_web3=1 %} - - - - - - - - - diff --git a/app/grants/urls.py b/app/grants/urls.py index baca338f01d..d8c81334106 100644 --- a/app/grants/urls.py +++ b/app/grants/urls.py @@ -22,11 +22,11 @@ from grants.views import ( add_grant_from_collection, bulk_fund, bulk_grants_for_cart, clr_grants, contribute_to_grants_v1, contribution_addr_from_all_as_json, contribution_addr_from_grant_as_json, - contribution_addr_from_grant_during_round_as_json, contribution_addr_from_round_as_json, flag, get_collection, - get_collections_list, get_grant_payload, get_grants, get_interrupted_contributions, get_replaced_tx, 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_info, grants_stats_view, - grants_zksync_recovery_view, invoice, leaderboard, new_matching_partner, profile, quickstart, + contribution_addr_from_grant_during_round_as_json, contribution_addr_from_round_as_json, create_matching_pledge_v1, + flag, get_collection, get_collections_list, get_grant_payload, get_grants, get_interrupted_contributions, + get_replaced_tx, 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_info, + grants_stats_view, grants_zksync_recovery_view, invoice, leaderboard, new_matching_partner, profile, quickstart, remove_grant_from_collection, save_collection, subscription_cancel, toggle_grant_favorite, verify_grant, zksync_get_interrupt_status, zksync_set_interrupt_status, ) @@ -63,6 +63,7 @@ re_path(r'^quickstart', quickstart, name='quickstart'), re_path(r'^leaderboard', leaderboard, name='leaderboard'), re_path(r'^matching-partners/new', new_matching_partner, name='new_matching_partner'), + re_path(r'^v1/api/matching-pledge/create', create_matching_pledge_v1, name='create_matching_pledge_v1'), path( 'invoice/contribution/', invoice, diff --git a/app/grants/views.py b/app/grants/views.py index 6ac32a45f51..3e1fad2a8c6 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -2175,40 +2175,136 @@ def record_grant_activity_helper(activity_type, grant, profile, amount=None, tok Activity.objects.create(**kwargs) +@login_required def new_matching_partner(request): - profile = get_profile(request) + grant_collections = [] + for g_collection in GrantCollection.objects.filter(hidden=False): + grant_collections.append({ + 'id': g_collection.pk, + 'name': g_collection.title, + }) + + grant_types = [] + for g_type in GrantType.objects.filter(is_active=True): + grant_types.append({ + 'id': g_type.pk, + 'name': g_type.label, + }) + + grant_categories = [] + for g_category in GrantCategory.objects.all(): + grant_categories.append({ + 'id': g_category.pk, + 'name': g_category.category + }) + params = { - 'avatar_url': request.build_absolute_uri(static('v2/images/twitter_cards/grants7.png')), 'title': 'Pledge your support.', 'card_desc': f'Thank you for your interest in supporting public goods.on Gitcoin. Complete the form below to get started.', - 'data': request.POST.dict(), - 'grant_types': basic_grant_types() + basic_grant_categories(None), + 'grant_types': grant_types, + 'grant_categories': grant_categories, + 'grant_collections': grant_collections } - if not request.user.is_authenticated: - messages.info( - request, - _('Please login to submit this form.') - ) - elif request.POST: - end_date = timezone.now() + timezone.timedelta(days=7*3) - network = 'mainnet' - match_pledge = MatchPledge.objects.create( - profile=profile, - active=False, - end_date=end_date, - amount=0, - data=json.dumps(request.POST.dict()) + return TemplateResponse(request, 'grants/new_match.html', params) + + + +def create_matching_pledge_v1(request): + + response = { + 'status': 400, + 'message': 'error: Bad Request. Unable to create pledge' + } + + user = request.user if request.user.is_authenticated else None + if not user: + response['message'] = 'error: user needs to be authenticated to create a pledge' + return JsonResponse(response) + + profile = request.user.profile if hasattr(request.user, 'profile') else None + + + if not profile: + response['message'] = 'error: no matching profile found' + return JsonResponse(response) + + if not request.method == 'POST': + response['message'] = 'error: pledge creation is a POST operation' + return JsonResponse(response) + + + grant_types = request.POST.get('grant_types[]', None) + grant_categories = request.POST.get('grant_categories[]', None) + grant_collections = request.POST.get('grant_collections[]', None) + + if grant_types: + grant_types = grant_types.split(',') + if grant_categories: + grant_categories = grant_categories.split(',') + if grant_collections: + grant_collections = grant_collections.split(',') + + if not grant_types and not grant_collections: + response['message'] = 'error: grant_types / grant_collections is parameter' + return JsonResponse(response) + + + matching_pledge_stage = request.POST.get('matching_pledge_stage', None) + tx_id = request.POST.get('tx_id', None) + if matching_pledge_stage == 'ready' and not tx_id: + response['message'] = 'error: tx_id is a mandatory parameter' + return JsonResponse(response) + + amount = request.POST.get('amount', False) + + if tx_id: + # TODO + collection_filters = None + grant_filters = None + + if grant_types: + grant_filters = { + 'grant_type__in': grant_types + } + if grant_categories: + grant_filters['categories__in'] = grant_categories + + if grant_collections: + collection_filters = { + 'pk__in': grant_collections + } + + clr_round = GrantCLR.objects.create( + round_num='pledge', + start_date=timezone.now(), + end_date=timezone.now(), + total_pot=amount, + grant_filters=grant_filters if grant_filters else {}, + collection_filters=collection_filters if collection_filters else {} ) - match_pledge.save() - new_grant_match_pledge(match_pledge) - messages.info( - request, - _("""Thank you for your inquiry. We will respond within 1-2 business days. """) - ) + clr_round.save() - return TemplateResponse(request, 'grants/newmatch.html', params) + + end_date = timezone.now() + timezone.timedelta(days=7*3) + match_pledge = MatchPledge.objects.create( + profile=profile, + active=False, + end_date=end_date, + amount=amount, + data=json.dumps(request.POST.dict()), + clr_round_num= clr_round if tx_id else None + ) + + match_pledge.save() + new_grant_match_pledge(match_pledge) + + response = { + 'status': 200, + 'message': 'success: match pledge created' + } + return JsonResponse(response) def invoice(request, contribution_pk):