diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 09ddbec07b1..c3ed4f52def 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: 🐛 Bug report about: Create a report to help us improve --- diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md index fecabfb735a..bb30f5e52a8 100644 --- a/.github/ISSUE_TEMPLATE/custom.md +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -1,5 +1,5 @@ --- -name: Custom +name: đŸ€” Custom about: Want to report an issue that doesn't fall under any other category? Use this template. diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md index 49f1ba92d2c..012e4cbfba7 100644 --- a/.github/ISSUE_TEMPLATE/discussion.md +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -1,5 +1,5 @@ --- -name: Discussion +name: ❓Discussion about: Want to start a discussion? Use this template. --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b90147dd6f4..8c586460889 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature request +name: ✋ Feature request about: Suggest an idea for this project --- diff --git a/.github/ISSUE_TEMPLATE/thank_you.md b/.github/ISSUE_TEMPLATE/thank_you.md new file mode 100644 index 00000000000..239bcddcc18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/thank_you.md @@ -0,0 +1,21 @@ +--- +name: ❀ Say thank you +about: Tell us how you use nock & support our efforts towards our mission (Grow/Sustain Open Source) +--- + +# ❀ I'm using Gitcoin + +If you (or your company) are using Gitcoin - please let us know. We'd love to hear from you! + +If you would like to help Gitcoin - any of the following is greatly appreciated. + +- [ ] Give the repository a star ⭐ +- [ ] Help out with issues +- [ ] Review pull requests +- [ ] Blog about Gitcoin +- [ ] Give talks +- [ ] Do a bounty +- [ ] Support us on [Gitcoin Grants](https://gitcoin.co/grants/86/gitcoin-sustainability-fund) + +Thank you! +The Gitcoin team \ No newline at end of file diff --git a/app/app/tests/test_app_urls.py b/app/app/tests/test_app_urls.py index a10db8154b3..319561786dd 100644 --- a/app/app/tests/test_app_urls.py +++ b/app/app/tests/test_app_urls.py @@ -89,15 +89,6 @@ def test_faucet_resolve(self): self.assertEqual(resolve('/faucet').view_name, 'faucet') self.assertEqual(resolve('/faucet/').view_name, 'faucet') - def test_dashboard_reverse(self): - """Test the dashboard url and check the reverse.""" - self.assertEqual(reverse('dashboard'), '/dashboard') - - def test_dashboard_resolve(self): - """Test the dashboard url and check the resolution.""" - self.assertEqual(resolve('/dashboard').view_name, 'dashboard') - self.assertEqual(resolve('/dashboard/').view_name, 'dashboard') - def test_explorer_reverse(self): """Test the explorer url and check the reverse.""" self.assertEqual(reverse('explorer'), '/explorer') diff --git a/app/app/urls.py b/app/app/urls.py index 2a7d50c07eb..076e18550d8 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -64,6 +64,9 @@ # inbox path('inbox/', include('inbox.urls', namespace='inbox')), + # board + path('dashboard/', dashboard.views.board, name='dashboard'), + # kudos path('kudos/', kudos.views.about, name='kudos_main'), path('kudos/about/', kudos.views.about, name='kudos_about'), @@ -132,9 +135,21 @@ re_path(r'^onboard/(?P\w+)/$', dashboard.views.onboard, name='onboard'), re_path(r'^onboard/contributor/avatar/?$', dashboard.views.onboard_avatar, name='onboard_avatar'), url(r'^postcomment/', dashboard.views.post_comment, name='post_comment'), - url(r'^dashboard/?', dashboard.views.dashboard, name='dashboard'), url(r'^explorer/?', dashboard.views.dashboard, name='explorer'), + # Funder dashboard + path('funder_dashboard//', dashboard.views.funder_dashboard, name='funder_dashboard'), + path( + 'funder_dashboard/bounties//', + dashboard.views.funder_dashboard_bounty_info, + name='funder_dashboard_bounty_info' + ), + + # Contributor dashboard + path( + 'contributor_dashboard//', dashboard.views.contributor_dashboard, name='contributor_dashboard' + ), + # Hackathon static page url(r'^hackathon/ethhack2019', dashboard.views.ethhack, name='ethhack_2019'), url(r'^hackathon/beyondblocks', dashboard.views.beyond_blocks_2019, name='beyond_blocks_2019'), diff --git a/app/assets/v2/css/base.css b/app/assets/v2/css/base.css index 6a32b314d6d..6c9b3a6f949 100644 --- a/app/assets/v2/css/base.css +++ b/app/assets/v2/css/base.css @@ -1581,3 +1581,7 @@ div.busyOverlay { .g-modal .modal-footer { border: none; } + +.inner-tooltip { + pointer-events: none; +} diff --git a/app/assets/v2/css/board.css b/app/assets/v2/css/board.css new file mode 100644 index 00000000000..73a0c531c72 --- /dev/null +++ b/app/assets/v2/css/board.css @@ -0,0 +1,95 @@ +.line-deco:after { + content: ''; + position: absolute; + top: 4em; + border: 1px solid #F5F6F6; + bottom: 2em; + left: 2.25em; +} + +.list-bounty { + max-height: 100vh; + overflow-y: auto; + padding: 0; + position: relative; + z-index: 1; + background: #fff no-repeat; + background-image: -webkit-radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), -webkit-radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)); + background-image: -moz-radial-gradient(50% 0, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), -moz-radial-gradient(50% 100%, farthest-side, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)); + background-image: radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)); + background-position: 0 0, 0 100%; + background-size: 100% 14px; +} + +.list-bounty:before, +.list-bounty:after { + content: ""; + position: relative; + z-index: -1; + display: block; + height: 30px; + margin: 0 0 -30px; + background: -webkit-linear-gradient(top, #fff, #fff 30%, rgba(255, 255, 255, 0)); + background: -moz-linear-gradient(top, #fff, #fff 30%, rgba(255, 255, 255, 0)); + background: linear-gradient(to bottom, #fff, #fff 30%, rgba(255, 255, 255, 0)); +} + +.list-bounty:after { + margin: -30px 0 0; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0), #fff 70%, #fff); + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0), #fff 70%, #fff); + background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 70%, #fff); +} + +.list-bounty-item + .list-bounty-item { + border-top: 1px solid #dee2e6; +} + +.comment-plan { + color: #666666; + white-space: pre-line; + max-height: 100px; + overflow: hidden; + overflow-y: auto; + background: #F5F6F6; + padding: 1em; + margin-top: 1em; + border-radius: 5px; +} + +.head-number { + text-align: center; +} +.head-number_title { + font-size: 12px; + color: #666666; + letter-spacing: 1.5px; +} + +.head-number_value { + color: var(--gc-purple); + font-weight: 700; + display: block; + font-size: 2rem; +} + +.light-blue { + color: #008EFF; +} + +.list-bounty-item_content { + width: 100%; +} + +@media (min-width: 768px) { + .list-bounty-item_logo { + width: 40px; + height: 40px; + } +} + +@media (min-width: 992px) { + .list-bounty-item_content { + width: 75%; + } +} diff --git a/app/assets/v2/css/featured-bounties.css b/app/assets/v2/css/featured-bounties.css index fa938f10cc6..3044bd0530e 100644 --- a/app/assets/v2/css/featured-bounties.css +++ b/app/assets/v2/css/featured-bounties.css @@ -2,6 +2,36 @@ color: #fff; background-color: #0d003c; padding: 0.7rem 1.5rem; + position: relative; +} + +.featured-bounties__arrow { + position: absolute; + top: 0; + bottom: 0; + opacity: 0; + z-index: 1; + color: #10ce7c; + background: transparent; + border: none; +} + +.featured-bounties__arrow:hover, +.featured-bounties__arrow:focus { + color: #57e6a8; + opacity: 1; +} + +.featured-bounties:hover .featured-bounties__arrow { + opacity: 1; +} + +.featured-bounties__arrow-left { + left: 0; +} + +.featured-bounties__arrow-right { + right: 10px; } .featured-bounties__title { diff --git a/app/assets/v2/css/grants/detail.css b/app/assets/v2/css/grants/detail.css index 2cb0946c33e..a8aadc3441b 100644 --- a/app/assets/v2/css/grants/detail.css +++ b/app/assets/v2/css/grants/detail.css @@ -3,7 +3,7 @@ max-width: 85vw } - #editor.ql-bubble .ql-editor { + #editor.ql-bubble .ql-editor[contenteditable='false'] { padding: 0; } diff --git a/app/assets/v2/css/hackathons/explorer.css b/app/assets/v2/css/hackathons/explorer.css index f0eed76dbcd..b8a62848d42 100644 --- a/app/assets/v2/css/hackathons/explorer.css +++ b/app/assets/v2/css/hackathons/explorer.css @@ -26,12 +26,12 @@ } .banner .sponsors-gold img { - width: 11rem; + height: 6rem; margin: 1em; } .banner .sponsors-silver img { - width: 7.5rem; + height: 3.5rem; margin: 0.8em; } @@ -48,4 +48,13 @@ /* Beyond Blockchain 2019 */ #beyondblockchain_2019 { background: #121315; +} + +/* Grow Ethereum 2019 */ +#grow-ethereum-2019 { + border-bottom: solid 0.5px #eee; +} + +#grow-ethereum-2019 .text-white { + color: black !important; /* TODO: REMOVE */ } \ No newline at end of file diff --git a/app/assets/v2/css/vue-loader.css b/app/assets/v2/css/vue-loader.css new file mode 100644 index 00000000000..830558cae03 --- /dev/null +++ b/app/assets/v2/css/vue-loader.css @@ -0,0 +1,130 @@ +/* loading */ +.rhombus-spinner { + height: 65px; + width: 65px; + position: relative; + transform: rotate(45deg); + margin: auto; + color: #25E899; +} + +.rhombus-spinner .rhombus { + height: calc(65px / 7.5); + width: calc(65px / 7.5); + animation-duration: 2000ms; + top: calc(65px / 2.3077); + left: calc(65px / 2.3077); + border-radius: 30px; + background-color: currentColor; + position: absolute; + animation-iteration-count: infinite; +} + +.rhombus-spinner .rhombus:nth-child(2n+0) { + margin-right: 0; +} + +.rhombus-spinner .rhombus.child-1 { + animation-name: rhombus-spinner-animation-child-1; + animation-delay: calc(100ms * 1); +} + +.rhombus-spinner .rhombus.child-2 { + animation-name: rhombus-spinner-animation-child-2; + animation-delay: calc(100ms * 2); +} + +.rhombus-spinner .rhombus.child-3 { + animation-name: rhombus-spinner-animation-child-3; + animation-delay: calc(100ms * 3); +} + +.rhombus-spinner .rhombus.child-4 { + animation-name: rhombus-spinner-animation-child-4; + animation-delay: calc(100ms * 4); +} + +.rhombus-spinner .rhombus.child-5 { + animation-name: rhombus-spinner-animation-child-5; + animation-delay: calc(100ms * 5); +} + +.rhombus-spinner .rhombus.child-6 { + animation-name: rhombus-spinner-animation-child-6; + animation-delay: calc(100ms * 6); +} + +.rhombus-spinner .rhombus.child-7 { + animation-name: rhombus-spinner-animation-child-7; + animation-delay: calc(100ms * 7); +} + +.rhombus-spinner .rhombus.child-8 { + animation-name: rhombus-spinner-animation-child-8; + animation-delay: calc(100ms * 8); +} + +.rhombus-spinner .rhombus.big { + height: calc(65px / 3); + width: calc(65px / 3); + top: calc(65px / 3); + left: calc(65px / 3); + background-color: #3E00FF; + animation: rhombus-spinner-animation-child-big 2s infinite; + animation-duration: 2000ms; + animation-delay: 0.5s; +} + +@keyframes rhombus-spinner-animation-child-1 { + 50% { + transform: translate(-325%, -325%); + } +} + +@keyframes rhombus-spinner-animation-child-2 { + 50% { + transform: translate(0, -325%); + } +} + +@keyframes rhombus-spinner-animation-child-3 { + 50% { + transform: translate(325%, -325%); + } +} + +@keyframes rhombus-spinner-animation-child-4 { + 50% { + transform: translate(325%, 0); + } +} + +@keyframes rhombus-spinner-animation-child-5 { + 50% { + transform: translate(325%, 325%); + } +} + +@keyframes rhombus-spinner-animation-child-6 { + 50% { + transform: translate(0, 325%); + } +} + +@keyframes rhombus-spinner-animation-child-7 { + 50% { + transform: translate(-325%, 325%); + } +} + +@keyframes rhombus-spinner-animation-child-8 { + 50% { + transform: translate(-325%, 0); + } +} + +@keyframes rhombus-spinner-animation-child-big { + 50% { + transform: scale(0.5); + } +} diff --git a/app/assets/v2/images/zero-bounties.svg b/app/assets/v2/images/zero-bounties.svg new file mode 100644 index 00000000000..a0d06e64ce7 --- /dev/null +++ b/app/assets/v2/images/zero-bounties.svg @@ -0,0 +1 @@ + diff --git a/app/assets/v2/js/board.js b/app/assets/v2/js/board.js new file mode 100644 index 00000000000..1d1b2655e2e --- /dev/null +++ b/app/assets/v2/js/board.js @@ -0,0 +1,168 @@ +let contributorBounties = {}; +let bounties = {}; +let authProfile = document.contxt.profile_id; + +Vue.mixin({ + methods: { + fetchBounties: function(type) { + let vm = this; + let apiUrlbounties = `/funder_dashboard/${type}/`; + let getbounties = fetchData (apiUrlbounties, 'GET'); + + $.when(getbounties).then(function(response) { + vm.$set(vm.bounties, type, response); + vm.isLoading[type] = false; + }); + }, + fetchApplicants: function(id, key, type) { + let vm = this; + let apiUrlApplicants = `/funder_dashboard/bounties/${id}/`; + + if (vm.bounties[type][key].contributors) { + return; + } + let getApplicants = fetchData (apiUrlApplicants, 'GET'); + + $.when(getApplicants).then(function(response) { + vm.$set(vm.bounties[type][key], 'contributors', response.profiles); + vm.isLoading[`${type}Contrib`] = false; + }); + }, + fetchContributorBounties: function(type) { + let vm = this; + let apiUrlbounties = `/contributor_dashboard/${type}/`; + let getbounties = fetchData (apiUrlbounties, 'GET'); + + $.when(getbounties).then(function(response) { + vm.$set(vm.contributorBounties, type, response); + vm.isLoading[type] = false; + + }); + }, + 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); + } + }, + startWork(key, bountyPk, profileId) { + let vm = this; + let url = `/actions/bounty/${bountyPk}/interest/${profileId}/interested/`; + let postStartpWork = fetchData (url, 'POST'); + + vm.disabledBtn = key; + + $.when(postStartpWork).then(response => { + vm.contributors.splice(key, 1); + vm.disabledBtn = ''; + _alert({ message: gettext('Contributor removed from bounty.') }, 'success'); + }, error => { + vm.disabledBtn = ''; + let msg = error.responseJSON.error || 'got an error. please try again, or contact support@gitcoin.co'; + + console.log(error.responseJSON.error); + _alert({ message: gettext(msg) }, 'error'); + }); + }, + stopWork(key, bountyPk, profileId, obj, section) { + let vm = this; + // let url = `/actions/bounty/${bountyPk}/interest/${profileId}/uninterested/`; + let url = `/actions/bounty/${bountyPk}/interest/remove/`; + + vm.disabledBtn = key; + if (window.confirm('Do you want to stop working on this bounty?')) { + let postStartpWork = fetchData (url, 'POST'); + + $.when(postStartpWork).then(response => { + vm[obj][section].splice(key, 1); + vm.disabledBtn = ''; + _alert({ message: gettext('Contributor removed from bounty.') }, 'success'); + }, error => { + vm.disabledBtn = ''; + let msg = error.responseJSON.error || 'got an error. please try again, or contact support@gitcoin.co'; + + _alert({ message: gettext(msg) }, 'error'); + }); + } else { + vm.disabledBtn = ''; + } + }, + checkData(persona) { + let vm = this; + + if (!Object.keys(vm.bounties).length && persona === 'funder') { + vm.fetchBounties('open'); + vm.fetchBounties('submitted'); + vm.fetchBounties('expired'); + } + + if (!Object.keys(vm.contributorBounties).length && persona === 'contributor') { + vm.fetchContributorBounties('work_in_progress'); + vm.fetchContributorBounties('work_submitted'); + vm.fetchContributorBounties('interested'); + } + }, + tabOnLoad() { + let vm = this; + + if (document.contxt.persona_is_hunter) { + vm.checkData('contributor'); + $('#contributor-tab').tab('show'); + } else { + vm.checkData('funder'); + $('#funder-tab').tab('show'); + } + } + } +}); + +if (document.getElementById('gc-board')) { + var app = new Vue({ + delimiters: [ '[[', ']]' ], + el: '#gc-board', + data: { + bounties: bounties, + openBounties: [], + submittedBounties: [], + expiredBounties: [], + contributors: [], + contributorBounties: contributorBounties, + expandedGroup: {'submitted': [], 'open': []}, + disabledBtn: false, + authProfile: authProfile, + isLoading: { + 'open': true, + 'openContrib': true, + 'submitted': true, + 'submittedContrib': true, + 'expired': true, + 'work_in_progress': true, + 'interested': true, + 'work_submitted': true + } + }, + mounted() { + this.tabOnLoad(); + } + }); +} + +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); +}); + +Vue.filter('moment', (date) => { + moment.locale('en'); + return moment.utc(date).fromNow(); +}); diff --git a/app/assets/v2/js/grants/detail.js b/app/assets/v2/js/grants/detail.js index b98d4160e68..5f5dc720ae5 100644 --- a/app/assets/v2/js/grants/detail.js +++ b/app/assets/v2/js/grants/detail.js @@ -17,7 +17,7 @@ $(document).ready(function() { $('#grant_contract_owner_address').text(), '#cancel_grant', 'Looks like your grant has been created with ' + - $('#grant_contract_owner_address').text() + '. Switch to take action on your grant.' + $('#grant_contract_owner_address').text() + '. Switch to take action on your grant.' ); if ($('#cancel_grant').attr('disabled')) { @@ -43,8 +43,6 @@ $(document).ready(function() { } }, 1000); - let _text = grant_description.getContents(); - userSearch('#grant-admin', false, undefined, false, false, true); userSearch('#grant-members', false, undefined, false, false, true); $('.select2-selection__rendered').removeAttr('title'); @@ -54,10 +52,11 @@ $(document).ready(function() { $('#edit-details').on('click', (event) => { event.preventDefault(); - if (grant_description) { + if (grant_description !== undefined) { grant_description.enable(true); grant_description.getContents(); } + $('#edit-details').addClass('hidden'); $('#save-details').removeClass('hidden'); $('#cancel-details').removeClass('hidden'); @@ -70,14 +69,9 @@ $(document).ready(function() { editableFields.forEach(field => { makeEditable(field); }); - }); - $('#save-details').on('click', (event) => { - if (grant_description) { - grant_description.enable(false); - } - + $('#save-details').on('click', event => { $('#edit-details').removeClass('hidden'); $('#save-details').addClass('hidden'); $('#cancel-details').addClass('hidden'); @@ -87,23 +81,32 @@ $(document).ready(function() { let edit_title = $('#form--input__title').val(); let edit_reference_url = $('#form--input__reference-url').val(); let edit_admin_profile = $('#grant-admin option').last().text(); - let edit_description = grant_description.getText(); - let edit_description_rich = JSON.stringify(grant_description.getContents()); let edit_amount_goal = $('#amount_goal').val(); let edit_grant_members = $('#grant-members').val(); + let data = { + 'edit-title': edit_title, + 'edit-reference_url': edit_reference_url, + 'edit-admin_profile': edit_admin_profile, + 'edit-amount_goal': edit_amount_goal, + 'edit-grant_members[]': edit_grant_members + }; + + if (grant_description !== undefined) { + const edit_description = grant_description.getText(); + const edit_description_rich = JSON.stringify(grant_description.getContents()); + + grant_description.enable(false); + data = Object.assign({}, data, { + 'edit-description': edit_description, + 'edit-description_rich': edit_description_rich + }); + } + $.ajax({ type: 'post', url: '', - data: { - 'edit-title': edit_title, - 'edit-reference_url': edit_reference_url, - 'edit-admin_profile': edit_admin_profile, - 'edit-description': edit_description, - 'edit-description_rich': edit_description_rich, - 'edit-amount_goal': edit_amount_goal, - 'edit-grant_members[]': edit_grant_members - }, + data, success: function(json) { window.location.reload(false); }, @@ -115,10 +118,10 @@ $(document).ready(function() { editableFields.forEach(field => disableEdit(field)); }); - $('#cancel-details').on('click', (event) => { - if (grant_description) { + $('#cancel-details').on('click', event => { + if (grant_description !== undefined) { grant_description.enable(false); - grant_description.setContents(_text); + grant_description.setContents(grant_description.getContents()); } $('#edit-details').removeClass('hidden'); $('#save-details').addClass('hidden'); @@ -129,7 +132,6 @@ $(document).ready(function() { }); $('#cancel_grant').on('click', function(e) { - $('.modal-cancel-grants').on('click', function(e) { let contract_address = $('#contract_address').val(); let grant_cancel_tx_id; @@ -206,6 +208,9 @@ $(document).ready(function() { }); }); + $('#grant-profile-tabs button').click(function() { + document.location = $(this).attr('href'); + }); }); const makeEditable = (input) => { @@ -249,9 +254,3 @@ const copyDuplicateDetails = () => { }); }); }; - -$(document).ready(() => { - $('#grant-profile-tabs button').click(function() { - document.location = $(this).attr('href'); - }); -}); \ No newline at end of file diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js index d5e96e2d8a0..95f76333cff 100644 --- a/app/assets/v2/js/pages/bounty_details.js +++ b/app/assets/v2/js/pages/bounty_details.js @@ -112,7 +112,11 @@ var heads = { var callbacks = { 'github_url': link_ize, 'value_in_token': function(key, val, result) { - return [ 'amount', token_value_to_display(val) + ' ' + result['token_name'] ]; + const title = token_value_to_display(val) + ' ' + result['token_name']; + const title_expand = title + ' in funding from original funder.'; + + $('#value_in_token').parents('.token').attr('title', title_expand); + return [ 'amount', title ]; }, 'avatar_url': function(key, val, result) { return [ 'avatar', '' ]; @@ -132,7 +136,7 @@ var callbacks = { let can_submit = result['can_submit_after_expiration_date']; - if (!isBountyOwner() && can_submit && is_bounty_expired(result)) { + if (!isBountyOwner(result) && can_submit && is_bounty_expired(result)) { ui_status += '

' + gettext('This issue is past its expiration date, but it is still active.') + '
' + @@ -149,7 +153,7 @@ var callbacks = { ui_status = '' + gettext('cancelled') + ''; } - if (isBountyOwner() && is_bounty_expired(result) && + if (isBountyOwner(result) && is_bounty_expired(result) && ui_status_raw !== 'done' && ui_status_raw !== 'cancelled') { ui_status += '

' + @@ -233,9 +237,8 @@ var callbacks = { if (val === null) { return [ null, null ]; } - var rates_estimate = get_rates_estimate(val); - $('#value_in_usdt_wrapper').attr('title', '

' + rates_estimate + '
'); + $('#value_in_usdt_wrapper').attr('title', '
The funding in this bounty adds up to $' + val + ' USD
'); return [ 'Amount_usd', val ]; }, @@ -292,11 +295,11 @@ var callbacks = { const ratio = obj['ratio']; const amount = obj['amount']; const usd = amount * ratio; - const funding = normalizeAmount(amount, tokenDecimals); - const tokenValue = normalizeAmount(1.0 * ratio, dollarDecimals); + const funding = round(amount, 2); + const tokenValue = Math.round(1.0 * ratio); const timestamp = new Date(obj['timestamp']); const timePeg = timeDifference(dateNow, timestamp > dateNow ? dateNow : timestamp, false, 60 * 60); - const tooltip = `$ ${normalizeAmount(usd, dollarDecimals)} USD in crowdfunding`; + const tooltip = `+ ${funding} ${tokenName} in crowdfunding`; leftHtml += '

+ ' + funding + ' ' + tokenName + '

'; rightHtml += '

@ $' + tokenValue + ' ' + tokenName + ' as of ' + timePeg + '

'; @@ -307,9 +310,10 @@ var callbacks = { newTokenTag(funding, tokenName, tooltip, true), usdTagElement ); + } - $('#value_in_usdt').html(normalizeAmount(totalUSDValue, dollarDecimals)); + $('#value_in_usdt').html(Math.round(totalUSDValue)); $('#value_in_usdt_wrapper').attr('title', '
' + @@ -1450,6 +1454,13 @@ const process_activities = function(result, bounty_activities) { const fulfillment = meta.fulfillment || {}; const new_bounty = meta.new_bounty || {}; const old_bounty = meta.old_bounty || {}; + const issue_message = result.interested.length ? + result.interested.find(interest => { + if (interest.profile.handle === _activity.profile.handle && interest.issue_message) { + return interest.issue_message; + } + return false; + }) : false; const has_signed_nda = result.interested.length ? result.interested.find(interest => { if (interest.profile.handle === _activity.profile.handle && interest.signed_nda) { @@ -1491,6 +1502,7 @@ const process_activities = function(result, bounty_activities) { activity_type: _activity.activity_type, status: _activity.activity_type === 'work_started' ? 'started' : 'stopped', signed_nda: has_signed_nda, + issue_message: issue_message, uninterest_possible: uninterest_possible, slash_possible: slash_possible, approve_worker_url: meta.approve_worker_url, diff --git a/app/assets/v2/js/pages/bounty_share.js b/app/assets/v2/js/pages/bounty_share.js index fa7a47099e8..37151e1bf5d 100644 --- a/app/assets/v2/js/pages/bounty_share.js +++ b/app/assets/v2/js/pages/bounty_share.js @@ -30,7 +30,7 @@ const sendInvites = (users) => { var sendEmail = fetchData( '/api/v0.1/social_contribution_email/', 'POST', - {usersId, msg, bountyId, invite_url}, + {usersId, msg, bountyId}, {'X-CSRFToken': csrftoken} ); diff --git a/app/assets/v2/js/pages/bulk_payout.js b/app/assets/v2/js/pages/bulk_payout.js index 0a7f7decfb6..52f9706b66a 100644 --- a/app/assets/v2/js/pages/bulk_payout.js +++ b/app/assets/v2/js/pages/bulk_payout.js @@ -1,8 +1,3 @@ -const round = function(num, decimals) { - return Math.round(num * 10 ** decimals) / 10 ** decimals; -}; - - const rateUser = (elem) => { let userSelected = $(elem).select2('data')[0].text; diff --git a/app/assets/v2/js/pages/dashboard.js b/app/assets/v2/js/pages/dashboard.js index 4dc79bc8069..03acadf2ae8 100644 --- a/app/assets/v2/js/pages/dashboard.js +++ b/app/assets/v2/js/pages/dashboard.js @@ -60,6 +60,20 @@ var paint_search_tabs = function() { target.html(html); }; +function scrollSlider(element, cardSize) { + const arrowLeft = $('#arrowLeft'); + const arrowRight = $('#arrowRight'); + + arrowLeft.on('click', function() { + element[0].scrollBy({left: -cardSize, behavior: 'smooth'}); + }); + arrowRight.on('click', function() { + element[0].scrollBy({left: cardSize, behavior: 'smooth'}); + }); + +} +scrollSlider($('#featured-card-container'), 288); + function debounce(func, wait, immediate) { var timeout; diff --git a/app/assets/v2/js/pages/increase_bounty.js b/app/assets/v2/js/pages/increase_bounty.js index f081dacb961..9b99128de73 100644 --- a/app/assets/v2/js/pages/increase_bounty.js +++ b/app/assets/v2/js/pages/increase_bounty.js @@ -22,6 +22,7 @@ $(document).ready(function() { waitforWeb3(function() { if (!is_funder()) { $('input, select').removeAttr('disabled'); + $('#increase_funding_explainer').html("Your transaction is secured by the Gitcoin's crowdfunding technology on the Ethereum blockchain. Learn more here."); } }); diff --git a/app/assets/v2/js/pages/process_bounty.js b/app/assets/v2/js/pages/process_bounty.js index 641a630fa34..59e954e9e13 100644 --- a/app/assets/v2/js/pages/process_bounty.js +++ b/app/assets/v2/js/pages/process_bounty.js @@ -159,6 +159,8 @@ window.onload = function() { var issueURL = $('input[name=issueURL]').val(); var fulfillmentId = getSelectedFulfillment().getAttribute('value'); + sessionStorage['bountyId'] = getURLParams('pk'); + var isError = false; if ($('#terms:checked').length == 0) { diff --git a/app/assets/v2/js/shared.js b/app/assets/v2/js/shared.js index b4d5280a003..03ae492cc33 100644 --- a/app/assets/v2/js/shared.js +++ b/app/assets/v2/js/shared.js @@ -1347,6 +1347,10 @@ function normalizeAmount(amount, decimals) { return Math.round((parseInt(amount) / Math.pow(10, decimals)) * 1000) / 1000; } +function round(amount, decimals) { + return Math.round(((amount) * Math.pow(10, decimals))) / Math.pow(10, decimals); +} + function newTokenTag(amount, tokenName, tooltipInfo, isCrowdfunded) { const ele = document.createElement('div'); const p = document.createElement('p'); @@ -1356,14 +1360,11 @@ function newTokenTag(amount, tokenName, tooltipInfo, isCrowdfunded) { span.innerHTML = amount + ' ' + tokenName + (isCrowdfunded ? '' : ''); + p.className = 'inner-tooltip'; p.appendChild(span); ele.appendChild(p); - if (tooltipInfo) { - ele.title = - '
' + - tooltipInfo + - '
'; + ele.title = tooltipInfo; } return ele; diff --git a/app/assets/v2/js/users.js b/app/assets/v2/js/users.js index 1b612ec4206..ed20bee7a1b 100644 --- a/app/assets/v2/js/users.js +++ b/app/assets/v2/js/users.js @@ -135,9 +135,11 @@ Vue.mixin({ let getUsers = fetchData (api, 'GET'); $.when(getUsers).then(function(response) { - if (response && response.data) { + if (response && response.data.length) { vm.openBounties(response.data[0]); $('#userModal').bootstrapModal('show'); + } else { + _alert('The user was not found. Please try using the search box.', 'error'); } }); } diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index 61e1dcb5860..d2b6d5f2c03 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -540,14 +540,18 @@ def merge_bounty(latest_old_bounty, new_bounty, metadata, bounty_details, verbos except Exception as e: logger.error(e) - event_tag = metadata.get('eventTag', '') - if event_tag: - try: - evt = HackathonEvent.objects.filter(name__iexact=event_tag).latest('id') - new_bounty.event = evt - new_bounty.save() - except Exception as e: - logger.error(e) + if latest_old_bounty and latest_old_bounty.event: + new_bounty.event = latest_old_bounty.event; + new_bounty.save() + else: + event_tag = metadata.get('eventTag', '') + if event_tag: + try: + evt = HackathonEvent.objects.filter(name__iexact=event_tag).latest('id') + new_bounty.event = evt + new_bounty.save() + except Exception as e: + logger.error(e) bounty_invitees = metadata.get('invite', '') if bounty_invitees and not latest_old_bounty: diff --git a/app/dashboard/management/commands/cleanup_db_space.py b/app/dashboard/management/commands/cleanup_db_space.py index 05fd1599210..feb5becacfc 100644 --- a/app/dashboard/management/commands/cleanup_db_space.py +++ b/app/dashboard/management/commands/cleanup_db_space.py @@ -59,5 +59,5 @@ def handle(self, *args, **options): result = LeaderboardRank.objects.filter( created_on__lt=self.get_then(14), - ).delete() + ).exclude(created_on__week_day=2).delete() print(f'LeaderboardRank: {result}') diff --git a/app/dashboard/migrations/0045_auto_20190803_1827.py b/app/dashboard/migrations/0045_auto_20190803_1827.py new file mode 100644 index 00000000000..f09f667a092 --- /dev/null +++ b/app/dashboard/migrations/0045_auto_20190803_1827.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-08-03 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0044_auto_20190729_1817'), + ] + + operations = [ + migrations.AlterField( + model_name='bounty', + name='title', + field=models.CharField(max_length=1000), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 45893f82f0a..df2635295c4 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -251,7 +251,7 @@ class Bounty(SuperModel): TERMINAL_STATUSES = ['done', 'expired', 'cancelled'] web3_type = models.CharField(max_length=50, default='bounties_network') - title = models.CharField(max_length=255) + title = models.CharField(max_length=1000) web3_created = models.DateTimeField(db_index=True) value_in_token = models.DecimalField(default=1, decimal_places=2, max_digits=50) token_name = models.CharField(max_length=50) @@ -304,7 +304,7 @@ class Bounty(SuperModel): canceled_bounty_reason = models.TextField(default='', blank=True, verbose_name=_('Cancelation reason')) project_type = models.CharField(max_length=50, choices=PROJECT_TYPES, default='traditional', db_index=True) permission_type = models.CharField(max_length=50, choices=PERMISSION_TYPES, default='permissionless', db_index=True) - bounty_categories = ArrayField(models.CharField(max_length=50, choices=BOUNTY_CATEGORIES), default=list) + bounty_categories = ArrayField(models.CharField(max_length=50, choices=BOUNTY_CATEGORIES), default=list, blank=True) repo_type = models.CharField(max_length=50, choices=REPO_TYPES, default='public') snooze_warnings_for_days = models.IntegerField(default=0) is_featured = models.BooleanField( @@ -2695,7 +2695,7 @@ def get_eth_sum(self, sum_type='collected', network='mainnet', bounties=None): try: if bounties.exists(): - eth_sum = sum([amount for amount in bounty.values_list("value_in_eth", flat=True)]) + eth_sum = sum([amount for amount in bounties.values_list("value_true", flat=True)]) except Exception: pass @@ -3011,8 +3011,8 @@ def to_representation(self, instance): 'keywords': instance.keywords, 'url': instance.get_relative_url(), 'position': instance.get_contributor_leaderboard_index(), - 'organizations': instance.get_who_works_with(), - 'total_earned': instance.get_eth_sum() + 'organizations': instance.get_who_works_with(network=None), + 'total_earned': instance.get_eth_sum(network=None) } diff --git a/app/dashboard/router.py b/app/dashboard/router.py index 089ef6aaf73..82d7d5b5659 100644 --- a/app/dashboard/router.py +++ b/app/dashboard/router.py @@ -103,7 +103,7 @@ class InterestSerializer(serializers.ModelSerializer): class Meta: """Define the Interest serializer metadata.""" model = Interest - fields = ('profile', 'created', 'pending', 'signed_nda') + fields = ('profile', 'created', 'pending', 'signed_nda', 'issue_message') # Serializers define the API representation. diff --git a/app/dashboard/templates/board.html b/app/dashboard/templates/board.html new file mode 100644 index 00000000000..472b4bd97bc --- /dev/null +++ b/app/dashboard/templates/board.html @@ -0,0 +1,465 @@ +{% comment %} + Copyright (C) 2019 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 add_url_schema avatar_tags %} + + + + + {% include 'shared/head.html' %} + {% include 'shared/cards.html' %} + + + + + + + {% include 'shared/top_nav.html' with class='d-md-flex' %} + {% include 'shared/nav.html' %} +
+ +
+ +
+ + +
+ {% include 'shared/footer.html' %} + {% include 'shared/analytics.html' %} + {% include 'shared/footer_scripts.html' with slim=1 %} + + + + + + diff --git a/app/dashboard/templates/bounty/details.html b/app/dashboard/templates/bounty/details.html index 05f6e87d674..cfb627832e0 100644 --- a/app/dashboard/templates/bounty/details.html +++ b/app/dashboard/templates/bounty/details.html @@ -56,12 +56,12 @@

-

+

-

+

USD

@@ -241,7 +241,7 @@
{% trans "Funder" %}
@@ -321,7 +321,7 @@
{% trans "Funder" %}
Worker - + [[:worker_handle]] Approved @@ -331,7 +331,7 @@
{% trans "Funder" %}
Worker - + [[:worker_handle]] Rejected @@ -399,6 +399,9 @@
{% trans "Funder" %}
[[:age]]
+ [[if issue_message && activity_type == 'worker_applied']] +

Work Plan: [[:issue_message.issue_message]]

+ [[/if]]
[[/if]] diff --git a/app/dashboard/templates/bounty/increase.html b/app/dashboard/templates/bounty/increase.html index 49659ceb561..e94c8072d59 100644 --- a/app/dashboard/templates/bounty/increase.html +++ b/app/dashboard/templates/bounty/increase.html @@ -148,7 +148,7 @@
{% trans "Total"%}
- + Your transaction is secured by the audited StandardBounties contract on the Ethereum blockchain.
Learn more here.
diff --git a/app/dashboard/templates/dashboard/featured_bounties.html b/app/dashboard/templates/dashboard/featured_bounties.html index ed81fa629ff..0f7e68027ec 100644 --- a/app/dashboard/templates/dashboard/featured_bounties.html +++ b/app/dashboard/templates/dashboard/featured_bounties.html @@ -9,5 +9,7 @@
+ +
diff --git a/app/dashboard/templates/shared/hackathon_sponsors.html b/app/dashboard/templates/shared/hackathon_sponsors.html index 78065ab8217..a961f4cb10d 100644 --- a/app/dashboard/templates/shared/hackathon_sponsors.html +++ b/app/dashboard/templates/shared/hackathon_sponsors.html @@ -15,12 +15,12 @@ along with this program. If not, see . {% endcomment %} {% load i18n static %} -
-

SUPPORTED BY

+
{% if sponsors %} +

SUPPORTED BY

{% if sponsors.sponsors_gold %} -
+
{% for sponsor in sponsors.sponsors_gold %}
{{sponsor.name}} @@ -30,7 +30,7 @@ {% endif %} {% if sponsors.sponsors_silver %} -
+
{% for sponsor in sponsors.sponsors_silver %}
{{sponsor.name}} diff --git a/app/dashboard/templates/shared/nav_auth.html b/app/dashboard/templates/shared/nav_auth.html index ef67e36b9c9..e7bdda10344 100644 --- a/app/dashboard/templates/shared/nav_auth.html +++ b/app/dashboard/templates/shared/nav_auth.html @@ -25,6 +25,10 @@
diff --git a/app/dashboard/templates/social_contribution_modal.html b/app/dashboard/templates/social_contribution_modal.html index 0822e490bb5..687ed3b6237 100644 --- a/app/dashboard/templates/social_contribution_modal.html +++ b/app/dashboard/templates/social_contribution_modal.html @@ -81,5 +81,4 @@

{% trans "Click to instantly share on the following networks" %}

{% csrf_token %} diff --git a/app/dashboard/tests/test_dashboard_utils.py b/app/dashboard/tests/test_dashboard_utils.py index b0e86198bfe..9b83e12dd96 100644 --- a/app/dashboard/tests/test_dashboard_utils.py +++ b/app/dashboard/tests/test_dashboard_utils.py @@ -22,9 +22,11 @@ from django.conf import settings from django.test.client import RequestFactory +import ipfshttpclient +import pytest from dashboard.utils import ( - clean_bounty_url, create_user_action, get_bounty, get_ordinal_repr, get_web3, getBountyContract, - humanize_event_name, + IPFSCantConnectException, clean_bounty_url, create_user_action, get_bounty, get_ipfs, get_ordinal_repr, get_web3, + getBountyContract, humanize_event_name, ipfs_cat_ipfsapi, ) from test_plus.test import TestCase from web3.main import Web3 @@ -114,3 +116,20 @@ def test_create_user_action_with_partial_cookie(mockUserAction): create_user_action(None, 'Login', request) mockUserAction.create.assert_called_once_with(action='Login', metadata={}, user=None, utm={'utm_campaign': 'test campaign'}) + + @staticmethod + def test_get_ipfs(): + """Test that IPFS connectivity to gateway defined in settings succeeds.""" + ipfs = get_ipfs() + assert type(ipfs) is ipfshttpclient.client.Client + + @staticmethod + def test_get_ipfs_with_bad_host(): + """Test that IPFS connectivity to gateway fails when bad host is passed.""" + with pytest.raises(IPFSCantConnectException): + assert get_ipfs('nohost.com') + + @staticmethod + def test_ipfs_cat_ipfsapi(): + """Test that ipfs_cat_ipfsapi method returns IPFS object.""" + assert "security-notes" in str(ipfs_cat_ipfsapi('/ipfs/QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv/readme')) diff --git a/app/dashboard/utils.py b/app/dashboard/utils.py index c76258232a4..9a8d9055dc1 100644 --- a/app/dashboard/utils.py +++ b/app/dashboard/utils.py @@ -24,7 +24,7 @@ from django.conf import settings -import ipfsapi +import ipfshttpclient import requests from app.utils import sync_profile from dashboard.helpers import UnsupportedSchemaException, normalize_url, process_bounty_changes, process_bounty_details @@ -32,7 +32,7 @@ from eth_utils import to_checksum_address from gas.utils import conf_time_spread, eth_usd_conv_rate, gas_advisories, recommend_min_gas_price_to_confirm_in_time from hexbytes import HexBytes -from ipfsapi.exceptions import CommunicationError +from ipfshttpclient.exceptions import CommunicationError from web3 import HTTPProvider, Web3, WebsocketProvider from web3.exceptions import BadFunctionCallOutput from web3.middleware import geth_poa_middleware @@ -187,7 +187,8 @@ def get_ipfs(host=None, port=settings.IPFS_API_PORT): Args: host (str): The IPFS host to connect to. - Defaults to environment variable: IPFS_HOST. + Defaults to environment variable: IPFS_HOST. The host name should be of the form 'ipfs.infura.io' and not + include 'https://'. port (int): The IPFS port to connect to. Defaults to environment variable: env IPFS_API_PORT. @@ -196,14 +197,15 @@ def get_ipfs(host=None, port=settings.IPFS_API_PORT): communication error with IPFS. Returns: - ipfsapi.client.Client: The IPFS connection client. + ipfshttpclient.client.Client: The IPFS connection client. """ if host is None: - host = f'https://{settings.IPFS_HOST}' - + clientConnectString = f'/dns/{settings.IPFS_HOST}/tcp/{settings.IPFS_API_PORT}/{settings.IPFS_API_SCHEME}' + else: + clientConnectString = f'/dns/{host}/tcp/{settings.IPFS_API_PORT}/https' try: - return ipfsapi.connect(host, port) + return ipfshttpclient.connect(clientConnectString) except CommunicationError as e: logger.exception(e) raise IPFSCantConnectException('Failed while attempt to connect to IPFS') diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 6a0ca8b1bf9..e0cb01ea1b8 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -23,6 +23,7 @@ import os import time from datetime import datetime +from decimal import Decimal from django.conf import settings from django.contrib import messages @@ -1058,7 +1059,7 @@ def invoice(request): params['total'] = bounty._val_usd_db if params['accepted_fulfillments'] else 0 for tip in params['tips']: if tip.value_in_usdt: - params['total'] += tip.value_in_usdt + params['total'] += Decimal(tip.value_in_usdt) return TemplateResponse(request, 'bounty/invoice.html', params) @@ -1136,11 +1137,9 @@ def social_contribution_email(request): from .utils import get_bounty_invite_url emails = [] - user_ids = request.POST.getlist('usersId[]', []) - invite_url = request.POST.get('invite_url', '') bounty_id = request.POST.get('bountyId') - if not invite_url: - invite_url = f'{settings.BASE_URL}issue/{get_bounty_invite_url(request.user.username, bounty_id)}' + user_ids = request.POST.getlist('usersId[]', []) + invite_url = f'{settings.BASE_URL}issue/{get_bounty_invite_url(request.user.username, bounty_id)}' inviter = request.user if request.user.is_authenticated else None bounty = Bounty.objects.current().get(id=int(bounty_id)) @@ -1814,7 +1813,7 @@ def profile_job_opportunity(request, handle): uploaded_file = request.FILES.get('job_cv') error_response = invalid_file_response(uploaded_file, supported=['application/pdf']) # 400 is ok because file upload is optional here - if error_response and error_response['status'] != '400': + if error_response and error_response['status'] != 400: return JsonResponse(error_response) try: profile = profile_helper(handle, True) @@ -1829,7 +1828,7 @@ def profile_job_opportunity(request, handle): profile.job_salary = float(request.POST.get('job_salary', '0').replace(',', '')) profile.job_location = json.loads(request.POST.get('locations')) profile.linkedin_url = request.POST.get('linkedin_url', None) - profile.resume = request.FILES.get('job_cv', None) + profile.resume = request.FILES.get('job_cv', profile.resume) profile.save() except (ProfileNotFoundException, ProfileHiddenException): raise Http404 @@ -2826,6 +2825,9 @@ def hackathon(request, hackathon=''): 'sponsors_silver': sponsors_silver } + if hackathon_event.identifier == 'grow-ethereum-2019': + params['card_desc'] = "The ‘Grow Ethereum’ Hackathon runs from Jul 29, 2019 - Aug 15, 2019 and features over $10,000 in bounties" + elif hackathon_event.identifier == 'beyondblockchain_2019': from dashboard.context.hackathon_explorer import beyondblockchain_2019 params['sponsors'] = beyondblockchain_2019 @@ -2853,6 +2855,201 @@ def get_hackathons(request): return TemplateResponse(request, 'dashboard/hackathons.html', params) +@login_required +def board(request): + """Handle the board view.""" + + context = { + 'is_outside': True, + 'active': 'dashboard', + 'title': 'Dashboard', + 'card_title': _('Dashboard'), + 'card_desc': _('Manage all your activity.'), + 'avatar_url': static('v2/images/helmet.png'), + } + return TemplateResponse(request, 'board.html', context) + + +def funder_dashboard_bounty_info(request, bounty_id): + """Per-bounty JSON data for the user dashboard""" + + user = request.user if request.user.is_authenticated else None + if not user: + return JsonResponse( + {'error': _('You must be authenticated via github to use this feature!')}, + status=401) + + bounty = Bounty.objects.get(id=bounty_id) + + if bounty.status == 'open': + interests = Interest.objects.prefetch_related('profile').filter(status='okay', bounty=bounty).all() + profiles = [ + {'interest': {'id': i.id, + 'issue_message': i.issue_message}, + 'handle': i.profile.handle, + 'avatar_url': i.profile.avatar_url, + 'star_rating': i.profile.get_average_star_rating['overall'], + 'total_rating': i.profile.get_average_star_rating['total_rating'], + 'fulfilled_bounties': len( + [b for b in i.profile.get_fulfilled_bounties()]), + 'leaderboard_rank': i.profile.get_contributor_leaderboard_index(), + 'id': i.profile.id} for i in interests] + elif bounty.status == 'submitted': + fulfillments = bounty.fulfillments.prefetch_related('profile').all() + profiles = [] + for f in fulfillments: + profile = {'fulfiller_metadata': f.fulfiller_metadata, 'created_on': f.created_on} + if f.profile: + profile.update( + {'handle': f.profile.handle, + 'avatar_url': f.profile.avatar_url, + 'preferred_payout_address': f.profile.preferred_payout_address, + 'id': f.profile.id}) + profiles.append(profile) + else: + profiles = [] + + return JsonResponse({ + 'id': bounty.id, + 'profiles': profiles}) + + +def serialize_funder_dashboard_open_rows(bounties, interests): + return [{'users_count': len([i for i in interests if b.pk in [i_b.pk for i_b in i.bounties]]), + 'title': b.title, + 'id': b.id, + 'standard_bounties_id': b.standard_bounties_id, + 'token_name': b.token_name, + 'value_in_token': b.value_in_token, + 'value_true': b.value_true, + 'value_in_usd': b.get_value_in_usdt, + 'github_url': b.github_url, + 'absolute_url': b.absolute_url, + 'avatar_url': b.avatar_url, + 'project_type': b.project_type, + 'expires_date': b.expires_date, + 'interested_comment': b.interested_comment, + 'bounty_owner_github_username': b.bounty_owner_github_username, + 'submissions_comment': b.submissions_comment} for b in bounties] + + +def serialize_funder_dashboard_submitted_rows(bounties): + return [{'users_count': b.fulfillments.count(), + 'title': b.title, + 'id': b.id, + 'token_name': b.token_name, + 'value_in_token': b.value_in_token, + 'value_true': b.value_true, + 'value_in_usd': b.get_value_in_usdt, + 'github_url': b.github_url, + 'absolute_url': b.absolute_url, + 'avatar_url': b.avatar_url, + 'project_type': b.project_type, + 'expires_date': b.expires_date, + 'interested_comment': b.interested_comment, + 'bounty_owner_github_username': b.bounty_owner_github_username, + 'submissions_comment': b.submissions_comment} for b in bounties] + + +def funder_dashboard(request, bounty_type): + """JSON data for the user dashboard""" + + user = request.user if request.user.is_authenticated else None + if not user: + return JsonResponse( + {'error': _('You must be authenticated via github to use this feature!')}, + status=401) + + profile = request.user.profile + + if bounty_type == 'open': + bounties = list(Bounty.objects.filter( + Q(idx_status='open') | Q(override_status='open'), + current_bounty=True, + bounty_owner_github_username=profile.handle, + ).order_by('-interested__created')) + interests = list(Interest.objects.filter( + bounty__pk__in=[b.pk for b in bounties], + status='okay', + pending=True)) + return JsonResponse(serialize_funder_dashboard_open_rows(bounties, interests), safe=False) + + elif bounty_type == 'submitted': + bounties = Bounty.objects.prefetch_related('fulfillments').filter( + Q(idx_status='submitted') | Q(override_status='submitted'), + current_bounty=True, + fulfillments__accepted=False, + bounty_owner_github_username=profile.handle, + ).order_by('-fulfillments__created_on') + return JsonResponse(serialize_funder_dashboard_submitted_rows(bounties), safe=False) + + elif bounty_type == 'expired': + bounties = Bounty.objects.filter( + Q(idx_status='expired') | Q(override_status='expired'), + current_bounty=True, + bounty_owner_github_username=profile.handle, + ).order_by('-expires_date') + + return JsonResponse([{'title': b.title, + 'token_name': b.token_name, + 'value_in_token': b.value_in_token, + 'value_true': b.value_true, + 'value_in_usd': b.get_value_in_usdt, + 'github_url': b.github_url, + 'absolute_url': b.absolute_url, + 'avatar_url': b.avatar_url, + 'project_type': b.project_type, + 'expires_date': b.expires_date, + 'interested_comment': b.interested_comment, + 'submissions_comment': b.submissions_comment} + for b in bounties], safe=False) + + + +def contributor_dashboard(request, bounty_type): + user = request.user if request.user.is_authenticated else None + if not user: + return JsonResponse( + {'error': _('You must be authenticated via github to use this feature!')}, + status=401) + + profile = request.user.profile + if bounty_type == 'work_in_progress': + status = ['open', 'started'] + pending = False + + elif bounty_type == 'interested': + status = ['open'] + pending = True + + elif bounty_type == 'work_submitted': + status = ['submitted'] + pending = False + + if status: + bounties = Bounty.objects.current().filter( + interested__profile=profile, + interested__status='okay', + interested__pending=pending, + idx_status__in=status, + current_bounty=True).order_by('-interested__created') + + return JsonResponse([{'title': b.title, + 'id': b.id, + 'token_name': b.token_name, + 'value_in_token': b.value_in_token, + 'value_true': b.value_true, + 'value_in_usd': b.get_value_in_usdt, + 'github_url': b.github_url, + 'absolute_url': b.absolute_url, + 'avatar_url': b.avatar_url, + 'project_type': b.project_type, + 'expires_date': b.expires_date, + 'interested_comment': b.interested_comment, + 'submissions_comment': b.submissions_comment} + for b in bounties], safe=False) + + @require_POST @login_required def change_user_profile_banner(request): diff --git a/app/economy/management/commands/refresh_bounties.py b/app/economy/management/commands/refresh_bounties.py index 411803876b3..66285c522be 100644 --- a/app/economy/management/commands/refresh_bounties.py +++ b/app/economy/management/commands/refresh_bounties.py @@ -48,7 +48,7 @@ def handle(self, *args, **options): Defaults to: `False` unless user passes the remote option. """ - all_bounties = Bounty.objects.filter(current_bounty=True).order('-pk') + all_bounties = Bounty.objects.filter(current_bounty=True).order_by('-pk') fetch_remote = options['remote'] print(f"refreshing {all_bounties.count()} bounties") diff --git a/app/grants/migrations/0026_auto_20190731_1433.py b/app/grants/migrations/0026_auto_20190731_1433.py new file mode 100644 index 00000000000..b7a94904add --- /dev/null +++ b/app/grants/migrations/0026_auto_20190731_1433.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.3 on 2019-07-31 14:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('grants', '0025_donation'), + ] + + operations = [ + migrations.AlterField( + model_name='grant', + name='request_ownership_change', + field=models.ForeignKey(blank=True, help_text="The Grant's potential new administrator profile.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='request_ownership_change', to='dashboard.Profile'), + ), + ] diff --git a/app/grants/models.py b/app/grants/models.py index f3eb7ca9793..61d76be16d2 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -652,7 +652,7 @@ def get_hash_from_web3(self): def get_converted_amount(self): try: if self.token_symbol == "ETH" or self.token_symbol == "WETH": - return Decimal(self.amount_per_period * eth_usd_conv_rate()) + return Decimal(float(self.amount_per_period) * float(eth_usd_conv_rate())) else: value_token_to_eth = Decimal(convert_amount( self.amount_per_period, diff --git a/app/grants/templates/grants/detail/tabs.html b/app/grants/templates/grants/detail/tabs.html index 15d05a68c59..7a26c8b1739 100644 --- a/app/grants/templates/grants/detail/tabs.html +++ b/app/grants/templates/grants/detail/tabs.html @@ -17,7 +17,7 @@ {% load static humanize i18n grants_extra %}
- + @@ -32,12 +32,12 @@ - + - +
@@ -91,26 +91,30 @@

{% trans "No Activity for th

{% endif %} -
\ No newline at end of file + {% with description_tab_url=grant.url|addstr:'?tab=description' %} + let grant_description; + + {% if request.get_full_path == description_tab_url or request.get_full_path == grant.url %} + grant_description = new Quill('#editor', { + theme: 'bubble', + readOnly: true, + }); + + {% if is_admin and not grant_is_inactive %} + grant_description = new Quill('#editor', { + theme: 'snow', + }); + grant_description.enable(false); + {% endif %} + + let desc = JSON.parse(grant_description.getContents().ops[0].insert); + if (desc.ops) { + grant_description.setContents(desc); + } + {% endif %} + {% endwith %} + diff --git a/app/grants/views.py b/app/grants/views.py index ca0f58ca63d..0a840578624 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -180,12 +180,13 @@ def grant_details(request, grant_id, grant_slug): grant.reference_url = request.POST.get('edit-reference_url') form_profile = request.POST.get('edit-admin_profile') admin_profile = Profile.objects.get(handle=form_profile) - grant.description = request.POST.get('edit-description') - grant.description_rich = request.POST.get('edit-description_rich') grant.amount_goal = Decimal(request.POST.get('edit-amount_goal')) team_members = request.POST.getlist('edit-grant_members[]') team_members.append(str(admin_profile.id)) grant.team_members.set(team_members) + if 'edit-description' in request.POST: + grant.description = request.POST.get('edit-description') + grant.description_rich = request.POST.get('edit-description_rich') if grant.admin_profile != admin_profile: grant.request_ownership_change = admin_profile change_grant_owner_request(grant, grant.request_ownership_change) diff --git a/app/healthcheck/healthchecks.py b/app/healthcheck/healthchecks.py index 864a8f4a0b7..145887fbe20 100644 --- a/app/healthcheck/healthchecks.py +++ b/app/healthcheck/healthchecks.py @@ -52,7 +52,7 @@ def check_status(self): """Define the functionality of the health check.""" from dashboard.utils import get_ipfs try: - ipfs_connection = get_ipfs(host='https://ipfs.infura.io', port=5001) + ipfs_connection = get_ipfs(host='ipfs.infura.io', port=5001) except IPFSCantConnectException: ipfs_connection = None diff --git a/app/kudos/utils.py b/app/kudos/utils.py index 87dc2392c99..3872dad1a27 100644 --- a/app/kudos/utils.py +++ b/app/kudos/utils.py @@ -25,7 +25,7 @@ from django.conf import settings -import ipfsapi +import ipfshttpclient from dashboard.utils import get_web3 from eth_utils import to_checksum_address from git.utils import get_emails_master @@ -160,8 +160,8 @@ def __init__(self, network='localhost', sockets=False): self._w3 = get_web3(self.network, sockets=sockets) - host = f'{settings.IPFS_API_SCHEME}://{settings.IPFS_HOST}' - self._ipfs = ipfsapi.connect(host=host, port=settings.IPFS_API_PORT) + ipfsConnectionString = f'/dns/{settings.IPFS_HOST}/tcp/{settings.IPFS_API_PORT}/{settings.IPFS_API_SCHEME}' + self._ipfs = ipfshttpclient.connect(ipfsConnectionString) self._contract = self._get_contract() self.address = self._get_contract_address() diff --git a/app/marketing/migrations/0004_auto_20190801_1303.py b/app/marketing/migrations/0004_auto_20190801_1303.py new file mode 100644 index 00000000000..c094751ef68 --- /dev/null +++ b/app/marketing/migrations/0004_auto_20190801_1303.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-08-01 13:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0003_auto_20190720_1709'), + ] + + operations = [ + migrations.AlterField( + model_name='emailsupressionlist', + name='email', + field=models.TextField(max_length=255), + ), + ] diff --git a/app/marketing/models.py b/app/marketing/models.py index fbed341956c..1be5d4a6f21 100644 --- a/app/marketing/models.py +++ b/app/marketing/models.py @@ -342,7 +342,7 @@ def __str__(self): class EmailSupressionList(SuperModel): - email = models.EmailField(max_length=255) + email = models.TextField(max_length=255) metadata = JSONField(default=dict, blank=True) comments = models.TextField(max_length=5000, blank=True) diff --git a/app/marketing/utils.py b/app/marketing/utils.py index 04ba38dea68..84a6798a77f 100644 --- a/app/marketing/utils.py +++ b/app/marketing/utils.py @@ -27,7 +27,7 @@ import requests from mailchimp3 import MailChimp -from marketing.models import AccountDeletionRequest, LeaderboardRank +from marketing.models import AccountDeletionRequest, EmailSupressionList, LeaderboardRank from slackclient import SlackClient from slackclient.exceptions import SlackClientError @@ -201,6 +201,12 @@ def should_suppress_notification_email(email, email_type): def get_or_save_email_subscriber(email, source, send_slack_invite=True, profile=None): + # Prevent syncing for those who match the suppression list + suppressions = EmailSupressionList.objects.all() + for suppression in suppressions: + if re.match(suppression, email): + return None + from marketing.models import EmailSubscriber defaults = {'source': source, 'email': email} diff --git a/app/retail/emails.py b/app/retail/emails.py index ff129187dc2..bc93e200d94 100644 --- a/app/retail/emails.py +++ b/app/retail/emails.py @@ -932,8 +932,8 @@ def render_start_work_applicant_expired(interest, bounty): def render_new_bounty_roundup(to_email): from dashboard.models import Bounty from django.conf import settings - subject = "The Grow Ethereum Hackathon Draws Nearer" - new_kudos_pks = [4284, 4282, 4281] + subject = "Grow Ethereum Hackathon: Just Days Away" + new_kudos_pks = [153, 66, 4281] new_kudos_size_px = 150 kudos_friday = f''' @@ -948,19 +948,19 @@ def render_new_bounty_roundup(to_email): Hey Gitcoiners,

-The Grow Ethereum Hackathon is right around the corner -- only 11 days remain until the hacking begins❗ We are expecting great sponsors, including confirmation from the Ethereum Foundation itself (!) and UNICEF. Build projects together with top Ethereum companies and enterprises, win crypto, and unleash a new era of decentralized global infrastructure. The link to register is here. đŸŒ± +We are now only days away from the beginning of the Grow Ethereum hackathon, one of the largest hackathons we've planned to date. As we announced last week, we have huge names behind the event - the Ethereum Foundation and MetaCartel to name a couple - with dozens of prizes on the horizon. See the full list of sponsors (including bZx, AdEx, and Arweave) and a checklist to prepare for the July 29th start date on this blog post.

-Working on an Ethereum ecosystem project and need funding? Get ready. Round 3 of the Gitcoin Grants CLR matching is right around the corner. Pick up some funding to help sustain your work and ease your worries: a funding opportunity awaits. Create a Gitcoin Grant & let the community know your progress! +Is it time for a summer refresh on your Gitcoin profile? Yes. Now, you can customize your Gitcoin profile with a groovy header. Check out the new profile customizations and grab yourself a brand new avatar for the summer at the Avatar Creator and Profile Editor.

- +Finally, we're preparing to announce the next round of CLR matching for Gitcoin Grants. If you have a project that needs funding, or are a funder that would like to help grow open source, check out the current Gitcoin Grants homepage. We are happy to answer any questions you might have. Grants live here.

{kudos_friday}

What else is new?

  • - The Gitcoin Livestream is back this week! Join us at 2PM ET this Friday to see some of the top Beyond Blockchain projects present. + The Gitcoin Livestream is back this week! Join us at 2PM ET this Friday.
  • Interested in the future of ads on the internet? Check out this primer on Ethical Advertising from Connor O'Day, part of the Codefund team. @@ -972,45 +972,45 @@ def render_new_bounty_roundup(to_email):

    ''' highlights = [{ - 'who': 'IgorShadurin', + 'who': 'dcd018', 'who_link': True, - 'what': 'Provable Emails, thanks to IgorShadurin and josh-richardson', - 'link': 'https://gitcoin.co/issue/ArweaveTeam/Bounties/5/3164', + 'what': 'Clone form, fixed!', + 'link': 'https://gitcoin.co/issue/gitcoinco/code_fund_ads/572/3247', 'link_copy': 'View more', }, { - 'who': 'sanchaymittal', + 'who': 'enieber', 'who_link': True, - 'what': 'Docs are complete!', - 'link': 'https://gitcoin.co/issue/MrElliwood/audio-router/1/3213', + 'what': 'README: Updated!', + 'link': 'https://gitcoin.co/issue/sigillabs/mobidex/236/3166', 'link_copy': 'View more', }, { - 'who': 'cpurta', + 'who': 'gutsal-arsen', 'who_link': True, - 'what': 'Raiden Hackathon Completion!', - 'link': 'https://gitcoin.co/issue/raiden-network/hackathons/3/3170', + 'what': 'Safe Keyboard, Implemented', + 'link': 'https://gitcoin.co/issue/polkawallet-io/polkawallet-RN/104/2944', 'link_copy': 'View more', }, ] sponsor = { - 'name': 'Solana', - 'title': 'Solana is the most performant blockchain in the world with speeds over 50,000 TPS (while being decentralized and secure).', + 'name': 'Allinfra', + 'title': 'The Future of Infrastructure Finance', 'image_url': 'https://s3.us-west-2.amazonaws.com/gitcoin-static/jDSk7ZTfpY19PWdwwsk8puNd.png', - 'link': 'http://bit.ly/TourDeSOL', - 'cta': 'Sign Up for Tour de SOL', + 'link': 'http://bit.ly/Allinfra', + 'cta': 'Register today', 'body': [ - 'We just announced Tour de SOL, or our incentivized testnet event. Are you a validator? Earn token and race against the best in the world. Deadline is July 21st.' + 'Bringing access, choice, and liquidity to unlisted infrastructure assets using the power of Ethereum' ] } bounties_spec = [{ - 'url': 'https://github.com/RibbonBlockchain/IncentivesMVP/issues/19', - 'primer': 'Create Initial Pilot phase for Ribbon blockchain', + 'url': 'https://github.com/gitcoinco/web/issues/4744', + 'primer': 'Allow Ability to Resurface Bounties to the Top When Expiration Date is Extended', }, { - 'url': 'https://github.com/RibbonBlockchain/IncentivesMVP/issues/8', - 'primer': 'Enable camera on front end for capture passport on patient and practitioner onboarding', + 'url': 'https://github.com/SpeckleOS/speckle-browser-extension/issues/89', + 'primer': 'Global Settings (Change Colour Theme and Log Out)', }, { - 'url': 'https://github.com/centrifuge/precise-proofs/issues/88', - 'primer': 'Optimize the fixed height tree implementation', + 'url': 'https://github.com/ironcoinext/IronCoin/issues/8', + 'primer': 'New Tab Page', }, ] diff --git a/app/retail/templates/gas.html b/app/retail/templates/gas.html index 5721da20b1d..9a709da6c3d 100644 --- a/app/retail/templates/gas.html +++ b/app/retail/templates/gas.html @@ -96,7 +96,7 @@

    {% trans "Live Predicted Confirmation Times (x axis) vs Gas Usage (y axis)" }); var margin = {top: 20, right: 20, bottom: 50, left: 70}, - width = window.innerWidth - 100 - margin.left - margin.right, + width = window.innerWidth - 300 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; var initialWidth = width; diff --git a/app/retail/templates/presskit.html b/app/retail/templates/presskit.html index cdd0719a9c3..33c3c0d8a61 100644 --- a/app/retail/templates/presskit.html +++ b/app/retail/templates/presskit.html @@ -33,7 +33,7 @@

    {% trans "Gitcoin Press Kit" %}

    {% trans "Thank you for your interest in using the Gitcoin brand assets. We created these guidelines to manage and protect the value of the brand. By using any of the Gitcoin brand assets, you expressly consent to respect these guidelines." %}

    -

    {% trans "If your use case is not covered below we’re happy to help answer any questions about the Gitcoin brand or other products within our brand, please contact " %} Gitcoin Core

    +

    {% trans "If your use case is not covered below we’re happy to help answer any questions about the Gitcoin brand or other products within our brand, please contact " %} Gitcoin Core

    .
diff --git a/app/retail/templates/results.html b/app/retail/templates/results.html index 4f535814787..cc5a91c41ac 100644 --- a/app/retail/templates/results.html +++ b/app/retail/templates/results.html @@ -41,7 +41,7 @@

${{ universe_total_usd|floatformat:2|intcomma }} of {% blocktrans %} - Total Platform Value + Gross Marketplace Value {% endblocktrans %} {% if keyword %}({{keyword}}){%endif%}

diff --git a/app/retail/templates/shared/top_nav.html b/app/retail/templates/shared/top_nav.html index 64f0edff3a0..33790da5a3a 100644 --- a/app/retail/templates/shared/top_nav.html +++ b/app/retail/templates/shared/top_nav.html @@ -20,7 +20,7 @@
- {% blocktrans %}The Grow Ethereum Hackathon runs July 29th to August 19th - Check out the Details.{% endblocktrans %} + {% blocktrans %}The Grow Ethereum Hackathon runs July 29th to August 15th - Check out the Details.{% endblocktrans %} × diff --git a/app/retail/utils.py b/app/retail/utils.py index 22c5f8fa629..7ab6431db01 100644 --- a/app/retail/utils.py +++ b/app/retail/utils.py @@ -173,7 +173,9 @@ def get_codefund_history_at_date(date, keyword): if date > timezone.datetime(2019, 6, 9): amount += 38287.22 if date > timezone.datetime(2019, 7, 9): - amount += 17937 + amount += 36230 + if date > timezone.datetime(2019, 8, 9): + amount += 0 return amount diff --git a/docs/README.md b/docs/README.md index b6b7542b8ba..bb9445eb18a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -172,20 +172,17 @@ Chain of Custody Anywhere between 2 and 4 above, Funder may withdraw their funds via 'Cancel Bounty' function for any reason. -### ... of a Tip +We may introduce Arbitration [via Delphi](http://delphi.network/) at some point in the future. Until then, we are lucky that Github users are very protective of their reputation, and therefore very kind to each other, and disputes have not generally arisen. -*ToDo* +### of a Tip ### ... of a Kudos -*ToDo* - -### Notes - +Note: - Crowdfunded bounties + bulk payouts are secured by Tips (at least until Standard Bounties 2.0 is released). - Kudos are also secured by Tips -This is the high level flow of a bounty on Gitcoin: +This is the high level flow of a tip on Gitcoin: diff --git a/requirements/base.txt b/requirements/base.txt index 38f4c5fb56a..ba83890180d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -37,7 +37,7 @@ cytoolz==0.9.0 boto==2.49.0 google-api-python-client django-environ==0.4.5 -ipfsapi +ipfshttpclient eth-utils==1.4.1 jsondiff==1.1.1 social-auth-app-django==2.1.0