diff --git a/app/app/urls.py b/app/app/urls.py index 970a43edf19..362a9039359 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -88,6 +88,7 @@ re_path(r'^kudos/(?P\d+)/(?P\w*)', kudos.views.details, name='kudos_details'), re_path(r'^kudos/address/(?P.*)', kudos.views.kudos_preferred_wallet, name='kudos_preferred_wallet'), re_path(r'^dynamic/kudos/(?P\d+)/(?P\w*)', kudos.views.image, name='kudos_dynamic_img'), + re_path(r'^kudos/new/?', kudos.views.newkudos, name='newkudos'), # mailing list url('mailing_list/funders/', dashboard.views.funders_mailing_list), diff --git a/app/assets/v2/css/onepagesubmit.css b/app/assets/v2/css/onepagesubmit.css new file mode 100644 index 00000000000..03127f32989 --- /dev/null +++ b/app/assets/v2/css/onepagesubmit.css @@ -0,0 +1,23 @@ +.slack--brand-tree { + max-width:80%; + margin: 0px auto; +} + + +h4 p { + background-color: #fee856; + padding: 5px 10px; +} + +@media (max-width: 768px) { + .form_container { + padding-left: 1.6rem !important; + padding-right: 1.6rem !important; + } +} + +@media (max-width: 992px) { + .slack--brand-tree { + max-width: 100%; + } +} \ No newline at end of file diff --git a/app/assets/v2/js/pages/newkudos.js b/app/assets/v2/js/pages/newkudos.js new file mode 100644 index 00000000000..372e93bf757 --- /dev/null +++ b/app/assets/v2/js/pages/newkudos.js @@ -0,0 +1,8 @@ +$(document).ready(function() { + $('#newkudos input.btn-go').click(function(e) { + mixpanel.track('New Kudos Request', {}); + setTimeout(function() { + $('#newkudos input.btn-go').attr('disabled', 'disabled'); + }, 1); + }); +}); \ No newline at end of file diff --git a/app/kudos/admin.py b/app/kudos/admin.py index d9bd5a72296..2f42db2420f 100644 --- a/app/kudos/admin.py +++ b/app/kudos/admin.py @@ -23,7 +23,8 @@ from django.utils.safestring import mark_safe from .models import ( - BulkTransferCoupon, BulkTransferRedemption, Contract, KudosTransfer, Token, TransferEnabledFor, Wallet, + BulkTransferCoupon, BulkTransferRedemption, Contract, KudosTransfer, Token, TokenRequest, TransferEnabledFor, + Wallet, ) @@ -32,6 +33,18 @@ class GeneralAdmin(admin.ModelAdmin): list_display = ['created_on', '__str__'] +class TokenRequestAdmin(admin.ModelAdmin): + ordering = ['-id'] + list_display = ['created_on', '__str__'] + raw_id_fields = ['profile'] + + def response_change(self, request, obj): + if "_mint_kudos" in request.POST: + obj.mint() + self.message_user(request, "Mint done") + return super().response_change(request, obj) + + class TransferEnabledForAdmin(admin.ModelAdmin): ordering = ['-id'] list_display = ['created_on', '__str__'] @@ -94,6 +107,7 @@ def claim(self, instance): admin.site.register(Token, TokenAdmin) admin.site.register(KudosTransfer, TransferAdmin) admin.site.register(Wallet, GeneralAdmin) +admin.site.register(TokenRequest, TokenRequestAdmin) admin.site.register(BulkTransferCoupon, BulkTransferCouponAdmin) admin.site.register(BulkTransferRedemption, BulkTransferRedemptionAdmin) admin.site.register(Contract, GeneralAdmin) diff --git a/app/kudos/management/commands/mint_all_kudos.py b/app/kudos/management/commands/mint_all_kudos.py index 8aea36d6cc1..38063bffddc 100644 --- a/app/kudos/management/commands/mint_all_kudos.py +++ b/app/kudos/management/commands/mint_all_kudos.py @@ -36,22 +36,28 @@ def mint_kudos(kudos_contract, kudos, account, private_key, gas_price_gwei, mint_to=None, live=False, skip_sync=True): - image_name = urllib.parse.quote(kudos.get('image')) - if image_name: - # Support Open Sea - if kudos_contract.network == 'rinkeby': - image_path = f'https://ss.gitcoin.co/static/v2/images/kudos/{image_name}' - external_url = f'https://stage.gitcoin.co/kudos/{kudos_contract.address}/{kudos_contract.getLatestId() + 1}' - elif kudos_contract.network == 'mainnet': - image_path = f'https://s.gitcoin.co/static/v2/images/kudos/{image_name}' - external_url = f'https://gitcoin.co/kudos/{kudos_contract.address}/{kudos_contract.getLatestId() + 1}' - elif kudos_contract.network == 'localhost': - image_path = f'v2/images/kudos/{image_name}' - external_url = f'http://localhost:8000/kudos/{kudos_contract.address}/{kudos_contract.getLatestId() + 1}' - else: - raise RuntimeError('Need to set the image path for that network') + image_path = kudos.get('artwork_url') + if not image_path: + image_name = urllib.parse.quote(kudos.get('image')) + if image_name: + # Support Open Sea + if kudos_contract.network == 'rinkeby': + image_path = f'https://ss.gitcoin.co/static/v2/images/kudos/{image_name}' + elif kudos_contract.network == 'mainnet': + image_path = f'https://s.gitcoin.co/static/v2/images/kudos/{image_name}' + elif kudos_contract.network == 'localhost': + image_path = f'v2/images/kudos/{image_name}' + else: + raise RuntimeError('Need to set the image path for that network') + + if kudos_contract.network == 'rinkeby': + external_url = f'https://stage.gitcoin.co/kudos/{kudos_contract.address}/{kudos_contract.getLatestId() + 1}' + elif kudos_contract.network == 'mainnet': + external_url = f'https://gitcoin.co/kudos/{kudos_contract.address}/{kudos_contract.getLatestId() + 1}' + elif kudos_contract.network == 'localhost': + external_url = f'http://localhost:8000/kudos/{kudos_contract.address}/{kudos_contract.getLatestId() + 1}' else: - image_path = '' + raise RuntimeError('Need to set the external url for that network') attributes = [] # "trait_type": "investor_experience", diff --git a/app/kudos/migrations/0007_tokenrequest.py b/app/kudos/migrations/0007_tokenrequest.py new file mode 100644 index 00000000000..7eed3b2bede --- /dev/null +++ b/app/kudos/migrations/0007_tokenrequest.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.3 on 2019-09-26 18:24 + +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0053_auto_20190920_1816'), + ('kudos', '0006_token_override_display_name'), + ] + + operations = [ + migrations.CreateModel( + name='TokenRequest', + 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)), + ('name', models.CharField(db_index=True, max_length=255)), + ('description', models.TextField(default='', max_length=500)), + ('priceFinney', models.IntegerField(default=18)), + ('network', models.CharField(db_index=True, max_length=25)), + ('artist', models.CharField(max_length=255)), + ('platform', models.CharField(max_length=255)), + ('to_address', models.CharField(max_length=255)), + ('artwork_url', models.CharField(max_length=255)), + ('numClonesAllowed', models.IntegerField(default=18)), + ('metadata', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, null=True)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, size=None)), + ('approved', models.BooleanField(default=True)), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='token_requests', to='dashboard.Profile')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/kudos/migrations/0008_tokenrequest_processed.py b/app/kudos/migrations/0008_tokenrequest_processed.py new file mode 100644 index 00000000000..42b64cbf4d9 --- /dev/null +++ b/app/kudos/migrations/0008_tokenrequest_processed.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-09-26 19:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kudos', '0007_tokenrequest'), + ] + + operations = [ + migrations.AddField( + model_name='tokenrequest', + name='processed', + field=models.BooleanField(default=False), + ), + ] diff --git a/app/kudos/models.py b/app/kudos/models.py index bbb18914fb2..b8a5765b106 100644 --- a/app/kudos/models.py +++ b/app/kudos/models.py @@ -21,7 +21,7 @@ from io import BytesIO from django.conf import settings -from django.contrib.postgres.fields import JSONField +from django.contrib.postgres.fields import ArrayField, JSONField from django.core.files import File from django.db import models from django.db.models import Q @@ -36,6 +36,7 @@ from dashboard.models import SendCryptoAsset from economy.models import SuperModel from eth_utils import to_checksum_address +from gas.utils import recommend_min_gas_price_to_confirm_in_time from pyvips.error import Error as VipsError logger = logging.getLogger(__name__) @@ -547,6 +548,57 @@ def __str__(self): """Return the string representation of a model.""" return f"coupon: {self.coupon} redeemed_by: {self.redeemed_by}" + +class TokenRequest(SuperModel): + """Define the TokenRequest model.""" + + name = models.CharField(max_length=255, db_index=True) + description = models.TextField(max_length=500, default='') + priceFinney = models.IntegerField(default=18) + network = models.CharField(max_length=25, db_index=True) + artist = models.CharField(max_length=255) + platform = models.CharField(max_length=255) + to_address = models.CharField(max_length=255) + artwork_url = models.CharField(max_length=255) + numClonesAllowed = models.IntegerField(default=18) + metadata = JSONField(null=True, default=dict, blank=True) + tags = ArrayField(models.CharField(max_length=200), blank=True, default=list) + approved = models.BooleanField(default=True) + processed = models.BooleanField(default=False) + profile = models.ForeignKey( + 'dashboard.Profile', related_name='token_requests', on_delete=models.CASCADE, + ) + + def __str__(self): + """Define the string representation of a conversion rate.""" + return f"{self.name} on {self.network} on {self.created_on}; approved: {self.approved} " + + + def mint(self): + """Approve / mint this token.""" + from kudos.management.commands.mint_all_kudos import mint_kudos # avoid circular import + from kudos.utils import KudosContract # avoid circular import + account = settings.KUDOS_OWNER_ACCOUNT + private_key = settings.KUDOS_PRIVATE_KEY + kudos = { + 'name': self.name, + 'description': self.description, + 'priceFinney': self.priceFinney, + 'artist': self.artist, + 'platform': self.name, + 'platform': self.platform, + 'numClonesAllowed': self.numClonesAllowed, + 'tags': self.tags, + 'artwork_url': self.artwork_url, + } + kudos_contract = KudosContract(network=self.network) + gas_price_gwei = recommend_min_gas_price_to_confirm_in_time(5) + mint_kudos(kudos_contract, kudos, account, private_key, gas_price_gwei, mint_to=None, live=True) + self.processed = True + self.approved = True + self.save() + + class TransferEnabledFor(SuperModel): """Model that represents the ability to send a Kudos, i f token.send_enabled_for_non_gitcoin_admins is true. diff --git a/app/kudos/views.py b/app/kudos/views.py index 917fce5baac..5d3bd5a3b74 100644 --- a/app/kudos/views.py +++ b/app/kudos/views.py @@ -23,6 +23,7 @@ import random import re import urllib.parse +import uuid from django.conf import settings from django.contrib import messages @@ -38,6 +39,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt +import boto3 from dashboard.models import Activity, Profile, SearchHistory from dashboard.notifications import maybe_market_kudos_to_email, maybe_market_kudos_to_github from dashboard.utils import get_nonce, get_web3 @@ -45,13 +47,14 @@ from gas.utils import recommend_min_gas_price_to_confirm_in_time from git.utils import get_emails_by_category, get_emails_master, get_github_primary_email from kudos.utils import kudos_abi +from marketing.mails import new_kudos_request from ratelimit.decorators import ratelimit from retail.helpers import get_ip from web3 import Web3 from .forms import KudosSearchForm from .helpers import get_token -from .models import BulkTransferCoupon, BulkTransferRedemption, KudosTransfer, Token, TransferEnabledFor +from .models import BulkTransferCoupon, BulkTransferRedemption, KudosTransfer, Token, TokenRequest, TransferEnabledFor logger = logging.getLogger(__name__) @@ -756,3 +759,59 @@ def receive_bulk(request, secret): 'tweet_url': coupon.token.url if not request.GET.get('tweet_url') else request.GET.get('tweet_url'), } return TemplateResponse(request, 'transaction/receive_bulk.html', params) + + +def newkudos(request): + context = { + 'active': 'newkudos', + 'msg': None, + } + + if not request.user.is_authenticated: + login_redirect = redirect('/login/github?next=' + request.get_full_path()) + return login_redirect + + if request.POST: + required_fields = ['name', 'description', 'priceFinney', 'artist', 'platform', 'numClonesAllowed', 'tags', 'to_address'] + validation_passed = True + for key in required_fields: + if not request.POST.get(key): + context['msg'] = str(_('You must provide the following fields: ')) + key + validation_passed = False + if validation_passed: + #upload to s3 + img = request.FILES.get('photo') + session = boto3.Session( + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + + s3 = session.resource('s3') + key = f'media/uploads/{uuid.uuid4()}_{img.name}' + response = s3.Bucket(settings.MEDIA_BUCKET).put_object(Key=key, Body=img, ACL='public-read', ContentEncoding='image/svg+xml') + artwork_url = f'https://{settings.MEDIA_BUCKET}.s3-us-west-2.amazonaws.com/{key}' + + # save / send email + obj = TokenRequest.objects.create( + profile=request.user.profile, + name=request.POST['name'], + description=request.POST['description'], + priceFinney=request.POST['priceFinney'], + artist=request.POST['artist'], + platform=request.POST['platform'], + numClonesAllowed=request.POST['numClonesAllowed'], + tags=",".split(request.POST['tags']), + to_address=request.POST['to_address'], + artwork_url=artwork_url, + network='mainnet', + approved=False, + metadata={ + 'ip': get_ip(request), + 'email': request.POST.get('email'), + } + ) + new_kudos_request(obj) + + context['msg'] = str(_('Your Kudos has been submitted and will be listed within 2 business days if it is accepted.')) + + return TemplateResponse(request, 'newkudos.html', context) diff --git a/app/marketing/mails.py b/app/marketing/mails.py index 8073cd4d367..40fcfbc270b 100644 --- a/app/marketing/mails.py +++ b/app/marketing/mails.py @@ -428,6 +428,28 @@ def new_token_request(obj): translation.activate(cur_language) +def new_kudos_request(obj): + to_email = 'founders@gitcoin.co' + from_email = obj.profile.email + cur_language = translation.get_language() + try: + setup_lang(to_email) + subject = _("New Kudos Request") + body_str = _("A new kudos request was completed. You may approve the kudos request here") + body = f"{body_str}: https://gitcoin.co/{obj.admin_url} \n\n {obj.profile.email}" + if not should_suppress_notification_email(to_email, 'faucet'): + send_mail( + from_email, + to_email, + subject, + body, + from_name=_("No Reply from Gitcoin.co"), + categories=['admin', func_name()], + ) + finally: + translation.activate(cur_language) + + def warn_account_out_of_eth(account, balance, denomination): to_email = settings.PERSONAL_CONTACT_EMAIL from_email = settings.SERVER_EMAIL diff --git a/app/retail/templates/admin/change_form.html b/app/retail/templates/admin/change_form.html index 7f932dd922a..e7c59163eaa 100644 --- a/app/retail/templates/admin/change_form.html +++ b/app/retail/templates/admin/change_form.html @@ -3,7 +3,12 @@ {% block submit_buttons_bottom %} {{ block.super }}
- + {% if 'dashboard/profile' in request.build_absolute_uri %} + + {% endif %} + {% if 'kudos/tokenrequest' in request.build_absolute_uri %} + + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/app/retail/templates/newkudos.html b/app/retail/templates/newkudos.html new file mode 100644 index 00000000000..c2872f57239 --- /dev/null +++ b/app/retail/templates/newkudos.html @@ -0,0 +1,113 @@ +{% extends 'base.html' %} +{% comment %} + Copyright (C) 2018 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 %} +{% block body %} + +
+
+
+
+ +
+
+
+
+

+ {% trans "New Kudos Submission Form" %} +

+ {% if msg %} +

+ {{ msg | safe }} +

+ {% endif %} + {% if success %} +
+ +
+ {% endif %} +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
+ + +
+
+ + {% csrf_token %} + +
+ {% endblock %} +
+
+
+
+{% block scripts %} + + + +{% endblock %} diff --git a/app/retail/templates/newtoken.html b/app/retail/templates/newtoken.html index ff01600e131..b42a0b2440e 100644 --- a/app/retail/templates/newtoken.html +++ b/app/retail/templates/newtoken.html @@ -17,32 +17,8 @@ {% endcomment %} {% load i18n static %} {% block body %} -