diff --git a/app/app/urls.py b/app/app/urls.py
index c2d33c8a6bd..ca13331fc47 100644
--- a/app/app/urls.py
+++ b/app/app/urls.py
@@ -100,6 +100,11 @@
dashboard.views.profile_job_opportunity,
name='profile_job_opportunity'
),
+ url(
+ r'^api/v0.1/bountydocument',
+ dashboard.views.bounty_upload_nda,
+ name='bounty_upload_nda'
+ ),
url(r'^api/v0.1/faucet/save/?', faucet.views.save_faucet, name='save_faucet'),
url(r'^api/v0.1/', include(dbrouter.urls)),
url(r'^api/v0.1/', include(kdrouter.urls)),
diff --git a/app/app/utils.py b/app/app/utils.py
index f74f14ed319..9bc6743cf4c 100644
--- a/app/app/utils.py
+++ b/app/app/utils.py
@@ -1,9 +1,11 @@
import email
import imaplib
import logging
+import os
import re
import time
from hashlib import sha1
+from secrets import token_hex
from django.conf import settings
from django.contrib.auth.models import User
@@ -17,7 +19,6 @@
import requests
from avatar.models import SocialAvatar
from avatar.utils import get_svg_templates, get_user_github_avatar_image
-from dashboard.models import Profile
from geoip2.errors import AddressNotFoundError
from git.utils import _AUTH, HEADERS, get_user
from ipware.ip import get_real_ip
@@ -161,6 +162,7 @@ def setup_lang(request, user):
DoesNotExist: The exception is raised if no profile is found for the specified handle.
"""
+ from dashboard.models import Profile
profile = None
if user.is_authenticated and hasattr(user, 'profile'):
profile = user.profile
@@ -174,7 +176,14 @@ def setup_lang(request, user):
request.session.modified = True
+def get_upload_filename(instance, filename):
+ salt = token_hex(16)
+ file_path = os.path.basename(filename)
+ return f"docs/{getattr(instance, '_path', '')}/{salt}/{file_path}"
+
+
def sync_profile(handle, user=None, hide_profile=True):
+ from dashboard.models import Profile
handle = handle.strip().replace('@', '').lower()
data = get_user(handle)
email = ''
diff --git a/app/assets/v2/css/base.css b/app/assets/v2/css/base.css
index bd2520c93c7..fd2978a90a4 100644
--- a/app/assets/v2/css/base.css
+++ b/app/assets/v2/css/base.css
@@ -1654,3 +1654,27 @@ div.busyOverlay {
border: 0.25em solid currentColor;
border-radius: 100px;
}
+
+.cta-blue {
+ background: #0D0764;
+ color: white;
+ padding: 1.8rem;
+ border-radius: 0.2rem;
+}
+
+.privaterepo-instructions {
+ background: #F8F8F8;
+ padding: 1rem;
+}
+
+@media (min-width: 768px) {
+ .privaterepo-instructions {
+ width: 75%;
+ }
+
+}
+
+.g-modal .modal-header,
+.g-modal .modal-footer {
+ border: none;
+}
diff --git a/app/assets/v2/css/forms/checkbox.css b/app/assets/v2/css/forms/checkbox.css
index 0278371deea..ed4a7a0bb4e 100644
--- a/app/assets/v2/css/forms/checkbox.css
+++ b/app/assets/v2/css/forms/checkbox.css
@@ -16,7 +16,7 @@
background: #fff;
border: 1px solid #dbdbdb;
border-radius: 2px;
- content: url('/static/v2/images/check.svg');
+ content: '';
display: inline-flex;
justify-content: center;
height: 18px;
@@ -30,6 +30,12 @@
color: #D50000;
}
+.form__checkbox input:disabled ~ .form__label::before {
+ background-color: #d0d0d0 !important;
+ border-color: #d0d0d0 !important;
+ cursor: not-allowed;
+}
+
.form__checkbox label.error {
display: none !important;
}
@@ -37,4 +43,5 @@
.form__checkbox input:checked ~ .form__label::before {
background-color: #0D0764;
border-color: #0D0764;
+ content: url('/static/v2/images/check.svg');
}
diff --git a/app/assets/v2/css/forms/select.css b/app/assets/v2/css/forms/select.css
index 4db09369b23..2438daed591 100644
--- a/app/assets/v2/css/forms/select.css
+++ b/app/assets/v2/css/forms/select.css
@@ -52,7 +52,7 @@
.form__select2 .select2-container {
display: inline-block !important;
- width: 100% !important;
+ min-width: 100%;
height: 100%;
font-size: 14px;
}
@@ -161,4 +161,4 @@
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #ffffff;
-}
\ No newline at end of file
+}
diff --git a/app/assets/v2/images/emails/private-repo-1.png b/app/assets/v2/images/emails/private-repo-1.png
new file mode 100644
index 00000000000..72a2bd9fc94
Binary files /dev/null and b/app/assets/v2/images/emails/private-repo-1.png differ
diff --git a/app/assets/v2/images/emails/private-repo-2.png b/app/assets/v2/images/emails/private-repo-2.png
new file mode 100644
index 00000000000..142e89d8ca4
Binary files /dev/null and b/app/assets/v2/images/emails/private-repo-2.png differ
diff --git a/app/assets/v2/images/emails/private-repo-3.png b/app/assets/v2/images/emails/private-repo-3.png
new file mode 100644
index 00000000000..02ebbaab1ca
Binary files /dev/null and b/app/assets/v2/images/emails/private-repo-3.png differ
diff --git a/app/assets/v2/images/emails/private-repo-4.png b/app/assets/v2/images/emails/private-repo-4.png
new file mode 100644
index 00000000000..52085a0c1ef
Binary files /dev/null and b/app/assets/v2/images/emails/private-repo-4.png differ
diff --git a/app/assets/v2/images/repo-instructions.png b/app/assets/v2/images/repo-instructions.png
new file mode 100644
index 00000000000..4de1b7b6e90
Binary files /dev/null and b/app/assets/v2/images/repo-instructions.png differ
diff --git a/app/assets/v2/images/repo-settings.png b/app/assets/v2/images/repo-settings.png
new file mode 100644
index 00000000000..b959095f08b
Binary files /dev/null and b/app/assets/v2/images/repo-settings.png differ
diff --git a/app/assets/v2/images/robots-party.svg b/app/assets/v2/images/robots-party.svg
new file mode 100644
index 00000000000..f4c3ef473a6
--- /dev/null
+++ b/app/assets/v2/images/robots-party.svg
@@ -0,0 +1,537 @@
+
+
\ No newline at end of file
diff --git a/app/assets/v2/js/ajax-helper.js b/app/assets/v2/js/ajax-helper.js
index 102b537ba33..ecdea817a6f 100644
--- a/app/assets/v2/js/ajax-helper.js
+++ b/app/assets/v2/js/ajax-helper.js
@@ -2,14 +2,14 @@
* Generic function to make an AJAX call avoid DRY
*
* ex:
- * var getdata = fetchData ('/api/v0.1/data/','GET')
- * $.when( getdata ).then( function ( response ){ return response })
+ * var getdata = fetchData('/api/v0.1/data/','GET')
+ * $.when(getdata).then(function(response){ return response })
*
- * var sendForm = fetchData ( e.currentTarget.action,
+ * var sendForm = fetchData(e.currentTarget.action,
* e.currentTarget.method,
* $("#form-wallets").serialize()
* )
- * $.when( sendForm ).then( function ( payback ){ return payback })
+ * $.when(sendForm).then(function(payback){ return payback })
*
*/
diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js
index 8ac9dfd813b..bb94c2f6561 100644
--- a/app/assets/v2/js/pages/bounty_details.js
+++ b/app/assets/v2/js/pages/bounty_details.js
@@ -633,51 +633,132 @@ var attach_override_status = function() {
});
};
-
var show_interest_modal = function() {
var self = this;
+ let modals = $('#modalInterest');
+ let modalBody = $('#modalInterest .modal-content');
+ let modalUrl = `/interest/modal?redirect=${window.location.pathname}&pk=${document.result['pk']}`;
+
+ modals.on('show.bs.modal', function() {
+ modalBody.load(modalUrl, ()=> {
+ if (document.result['repo_type'] === 'private') {
+ $('#nda-upload').show();
+ $('#issueNDA').prop('required', true);
+ document.result.unsigned_nda ? $('.nda-download-link').attr('href', document.result.unsigned_nda.doc) : $('#nda-upload').hide();
+ }
- setTimeout(function() {
- var url = '/interest/modal?redirect=' + window.location.pathname + '&pk=' + document.result['pk'];
-
- $.get(url, function(newHTML) {
- var modal = $(newHTML).appendTo('body').modal({
- modalClass: 'modal add-interest-modal'
- });
-
- var actionPlanForm = modal.find('form#action_plan');
- var issueMessage = actionPlanForm.find('#issue_message');
+ let actionPlanForm = $('#action_plan');
+ let issueMessage = $('#issue_message');
issueMessage.attr('placeholder', gettext('What steps will you take to complete this task? (min 30 chars)'));
- modal.on('submit', function(event) {
+ actionPlanForm.on('submit', function(event) {
event.preventDefault();
- var msg = issueMessage.val().trim();
+ let msg = issueMessage.val().trim();
+ let issueNDA = $('#issueNDA')[0].files;
if (!msg || msg.length < 30) {
_alert({message: gettext('Please provide an action plan for this ticket. (min 30 chars)')}, 'error');
return false;
}
+ if (typeof issueNDA[0] !== 'undefined') {
+ const formData = new FormData();
+
+ formData.append('docs', issueNDA[0]);
+ formData.append('doc_type', 'signed_nda');
+ const ndaSend = {
+ url: '/api/v0.1/bountydocument',
+ method: 'POST',
+ data: formData,
+ processData: false,
+ dataType: 'json',
+ contentType: false
+ };
+
+ $.ajax(ndaSend).done(function(response) {
+ _alert(response.message, 'info');
+ add_interest(document.result['pk'], {
+ issue_message: msg,
+ signed_nda: response.bounty_doc_id
+ }).then(success => {
+ if (success) {
+ $(self).attr('href', '/uninterested');
+ $(self).find('span').text(gettext('Stop Work'));
+ $(self).parent().attr('title', '
' + gettext('Notify the funder that you will not be working on this project') + '
');
+ $.modal.close();
+ }
+ }).catch((error) => {
+ if (error.responseJSON.error === 'You may only work on max of 3 issues at once.')
+ return;
+ throw error;
+ });
+ }).fail(function(error) {
+ _alert(error, 'error');
+ });
+ } else {
+ add_interest(document.result['pk'], {
+ issue_message: msg
+ }).then(success => {
+ if (success) {
+ $(self).attr('href', '/uninterested');
+ $(self).find('span').text(gettext('Stop Work'));
+ $(self).parent().attr('title', '' + gettext('Notify the funder that you will not be working on this project') + '
');
+ $.modal.close();
+ }
+ }).catch((error) => {
+ if (error.responseJSON.error === 'You may only work on max of 3 issues at once.')
+ return;
+ throw error;
+ });
+ }
- add_interest(document.result['pk'], {
- issue_message: msg
- }).then(success => {
- if (success) {
- $(self).attr('href', '/uninterested');
- $(self).find('span').text(gettext('Stop Work'));
- $(self).parent().attr('title', '' + gettext('Notify the funder that you will not be working on this project') + '
');
- $.modal.close();
- }
- }).catch((error) => {
- if (error.responseJSON.error === 'You may only work on max of 3 issues at once.')
- return;
- throw error;
- });
});
+
});
});
+ modals.bootstrapModal('show');
+};
+
+const repoInstructions = () => {
+ let linkToSettings = `https://github.com/${document.result.github_org_name}/${document.result.github_repo_name}/settings/collaboration`;
+
+
+ let modalTmp = `
+
+
+
+
+
+
+ You have successfully approved the contributor to work on your bounty!
+
+
+
+
+
Now you need to invite the contributor to your private repo on GitHub You can find it under GitHub repository > Settings > Collaborators
+
+
+
+
+
+
+
+
`;
+
+ $(modalTmp).bootstrapModal('show');
+
+ $(document, modalTmp).on('hidden.bs.modal', function(e) {
+ $('#exampleModalCenter').remove();
+ $(modalTmp).bootstrapModal('dispose');
+ });
};
var set_extended_time_html = function(extendedDuration, currentExpires) {
@@ -1055,7 +1136,8 @@ var do_actions = function(result) {
const _entry = {
enabled: true,
href: github_url,
- text: gettext('View On Github') +
+ text: (result['repo_type'] === 'private' ? ' ' +
+ gettext('Private Repo') : gettext('View On Github')) +
(result['is_issue_closed'] ? gettext(' (Issue is closed)') : ''),
parent: 'right_actions',
title: gettext('View issue details and comments on Github'),
@@ -1266,6 +1348,10 @@ var pull_bounty_from_api = function() {
render_activity(result, results);
document.result = result;
+
+ if (typeof promptPrivateInstructions !== 'undefined' && result.repo_type === 'private') {
+ repoInstructions();
+ }
return;
}
}
@@ -1324,6 +1410,12 @@ const process_activities = function(result, bounty_activities) {
const fulfillment = meta.fulfillment || {};
const new_bounty = meta.new_bounty || {};
const old_bounty = meta.old_bounty || {};
+ const has_signed_nda = result.interested.map(interest => {
+ if (interest.profile.handle === _activity.profile.handle && interest.signed_nda) {
+ return interest.signed_nda.doc;
+ }
+ return false;
+ });
const has_pending_interest = !!result.interested.find(interest =>
interest.profile.handle === _activity.profile.handle && interest.pending);
const has_interest = !!result.interested.find(interest =>
@@ -1357,6 +1449,7 @@ const process_activities = function(result, bounty_activities) {
age: timeDifference(now, new Date(_activity.created)),
activity_type: _activity.activity_type,
status: _activity.activity_type === 'work_started' ? 'started' : 'stopped',
+ signed_nda: has_signed_nda,
uninterest_possible: uninterest_possible,
slash_possible: slash_possible,
approve_worker_url: meta.approve_worker_url,
diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js
index 56fd6d41847..42fbd065c21 100644
--- a/app/assets/v2/js/pages/new_bounty.js
+++ b/app/assets/v2/js/pages/new_bounty.js
@@ -52,6 +52,16 @@ $('#sync-issue').on('click', function(event) {
});
$('#issueURL').focusout(function() {
+ if (isPrivateRepo) {
+ setPrivateForm();
+ if ($('input[name=issueURL]').val() == '' || !validURL($('input[name=issueURL]').val())) {
+ $('.js-submit').addClass('disabled');
+ } else {
+ $('.js-submit').removeClass('disabled');
+ }
+ return;
+ }
+
setInterval(function() {
$('#last-synced span').html(timeDifference(new Date(), new_bounty.last_sync));
}, 6000);
@@ -101,7 +111,7 @@ $(document).ready(function() {
$('input[name=hours]').blur(setUsdAmount);
$('select[name=denomination]').change(setUsdAmount);
$('select[name=denomination]').change(promptForAuth);
- $('input[name=issueURL]').blur(retrieveIssueDetails);
+
setTimeout(setUsdAmount, 1000);
waitforWeb3(function() {
promptForAuth();
@@ -144,10 +154,9 @@ $(document).ready(function() {
$('input[name=revisions]').val(revision);
});
- if ($('input[name=issueURL]').val() != '') {
+ if ($('input[name=issueURL]').val() != '' && !isPrivateRepo) {
retrieveIssueDetails();
}
- $('input[name=issueURL]').focus();
// all js select 2 fields
$('.js-select2').each(function() {
@@ -166,6 +175,7 @@ $(document).ready(function() {
if ($('input[name=amount]').val().trim().length > 0) {
setUsdAmount();
}
+
var open_hiring_panel = function(do_focus) {
setTimeout(function() {
var hiringRightNow = $('#hiringRightNow').is(':checked');
@@ -222,6 +232,10 @@ $(document).ready(function() {
data[this.name] = this.value;
});
+ if (data.repo_type == 'private' && data.project_type != 'traditional' && data.permission_type != 'approval') {
+ _alert(gettext('The project type and/or permission type of bounty does not validate for a private repo'));
+ }
+
disabled.attr('disabled', 'disabled');
// setup
@@ -251,6 +265,7 @@ $(document).ready(function() {
estimatedHours: data.hours,
fundingOrganisation: data.fundingOrganisation,
is_featured: data.featuredBounty,
+ repo_type: data.repo_type,
featuring_date: data.featuredBounty && ((new Date().getTime() / 1000) | 0) || 0,
reservedFor: reservedFor ? reservedFor.text : '',
tokenName
@@ -290,6 +305,7 @@ $(document).ready(function() {
},
funding_organisation: metadata.fundingOrganisation,
is_featured: metadata.is_featured,
+ repo_type: metadata.repo_type,
featuring_date: metadata.featuring_date,
privacy_preferences: privacy_preferences,
funders: [],
@@ -400,7 +416,6 @@ $(document).ready(function() {
issuePackage['txid'] = result;
localStorage[issueURL] = JSON.stringify(issuePackage);
- // sync db
syncDb();
}
@@ -449,9 +464,29 @@ $(document).ready(function() {
}
var do_bounty = function(callback) {
- // Add data to IPFS and kick off all the callbacks.
- ipfsBounty.payload.issuer.address = account;
- ipfs.addJson(ipfsBounty, newIpfsCallback);
+ const formData = new FormData();
+
+ formData.append('docs', $('#issueNDA')[0].files[0]);
+ formData.append('doc_type', 'unsigned_nda');
+ const settings = {
+ url: '/api/v0.1/bountydocument',
+ method: 'POST',
+ processData: false,
+ dataType: 'json',
+ contentType: false,
+ data: formData
+ };
+
+ $.ajax(settings).done(function(response) {
+ _alert(response.message, 'info');
+ // sync db
+ // Add data to IPFS and kick off all the callbacks.
+ ipfsBounty.payload.issuer.address = account;
+ ipfsBounty.payload.unsigned_nda = response.bounty_doc_id;
+ ipfs.addJson(ipfsBounty, newIpfsCallback);
+ }).fail(function(error) {
+ _alert(error, 'error');
+ });
};
const payFeaturedBounty = function() {
@@ -483,6 +518,21 @@ $(document).ready(function() {
});
});
+$(window).on('load', function() {
+ if (params.has('type')) {
+ let checked = params.get('type');
+
+ toggleCtaPlan(checked);
+ $(`input[name=repo_type][value=${checked}]`).prop('checked', 'true');
+ } else {
+ params.append('type', 'public');
+ window.history.replaceState({}, '', location.pathname + '?' + params);
+ }
+ $('input[name=repo_type]').change(function() {
+ toggleCtaPlan($(this).val());
+ });
+});
+
var check_balance_and_alert_user_if_not_enough = function(tokenAddress, amount) {
var token_contract = web3.eth.contract(token_abi).at(tokenAddress);
var from = web3.eth.coinbase;
@@ -510,3 +560,69 @@ getAmountEstimate(usdFeaturedPrice, 'ETH', (amountEstimate) => {
ethFeaturedPrice = amountEstimate['value'];
$('.featured-price-eth').text(`+${amountEstimate['value']} ETH`);
});
+
+
+let isPrivateRepo = false;
+let params = (new URL(document.location)).searchParams;
+
+const setPrivateForm = () => {
+ $('#title').removeClass('hidden');
+ $('#description, #title').prop('readonly', false);
+ $('#description, #title').prop('required', true);
+ $('#no-issue-banner').hide();
+ $('#issue-details, #issue-details-edit').show();
+ $('#sync-issue').removeClass('disabled');
+ // $('.js-submit').removeClass('disabled');
+ $('#last-synced, #edit-issue, #sync-issue, #title--text').hide();
+
+ $('#admin_override_suspend_auto_approval').prop('checked', false);
+ $('#admin_override_suspend_auto_approval').attr('disabled', true);
+ $('#show_email_publicly').attr('disabled', true);
+ $('#cta-subscription, #private-repo-instructions').removeClass('d-md-none');
+ $('#nda-upload').show();
+ $('#issueNDA').prop('required', true);
+
+ $('#project_type').select2().val('traditional');
+ $('#permission_type').select2().val('approval');
+ $('#project_type, #permission_type').select2().prop('disabled', true).trigger('change');
+ $('#keywords').select2({
+ placeholder: 'Select tags',
+ tags: 'true',
+ allowClear: true
+ });
+};
+
+const setPublicForm = () => {
+ $('#title').addClass('hidden');
+ $('#description, #title').prop('readonly', true);
+ $('#no-issue-banner').show();
+ $('#issue-details, #issue-details-edit').hide();
+ $('#sync-issue').addClass('disabled');
+ $('.js-submit').addClass('disabled');
+ $('#last-synced, #edit-issue , #sync-issue, #title--text').show();
+
+ $('#admin_override_suspend_auto_approval').prop('checked', true);
+ $('#admin_override_suspend_auto_approval').attr('disabled', false);
+ $('#show_email_publicly').attr('disabled', false);
+ $('#cta-subscription, #private-repo-instructions').addClass('d-md-none');
+ $('#nda-upload').hide();
+ $('#issueNDA').prop('required', false);
+
+ $('#project_type, #permission_type').select2().prop('disabled', false).trigger('change');
+ retrieveIssueDetails();
+};
+
+const toggleCtaPlan = (value) => {
+ if (value === 'private') {
+
+ params.set('type', 'private');
+ isPrivateRepo = true;
+ setPrivateForm();
+ } else {
+
+ params.set('type', 'public');
+ isPrivateRepo = false;
+ setPublicForm();
+ }
+ window.history.replaceState({}, '', location.pathname + '?' + params);
+};
diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py
index 7ec3866ef99..703006b6e54 100644
--- a/app/dashboard/helpers.py
+++ b/app/dashboard/helpers.py
@@ -30,7 +30,7 @@
from django.utils import timezone
from app.utils import get_semaphore, sync_profile
-from dashboard.models import Activity, Bounty, BountyFulfillment, BountySyncRequest, UserAction
+from dashboard.models import Activity, Bounty, BountyDocuments, BountyFulfillment, BountySyncRequest, UserAction
from dashboard.notifications import (
maybe_market_to_email, maybe_market_to_github, maybe_market_to_slack, maybe_market_to_twitter,
maybe_market_to_user_discord, maybe_market_to_user_slack,
@@ -364,6 +364,13 @@ def create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id):
url = normalize_url(url)
else:
raise UnsupportedSchemaException('No webReferenceURL found. Cannot continue!')
+
+ # check conditions for private repos
+ if metadata.get('repo_type', None) == 'private' and \
+ bounty_payload.get('schemes', {}).get('permission_type', 'permissionless') != 'approval' and \
+ bounty_payload.get('schemes', {}).get('project_type', 'traditional') != 'traditional':
+ raise UnsupportedSchemaException('The project type or permission does not match for private repo')
+
# Check if we have any fulfillments. If so, check if they are accepted.
# If there are no fulfillments, accepted is automatically False.
@@ -403,6 +410,11 @@ def create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id):
}
if not latest_old_bounty:
schemes = bounty_payload.get('schemes', {})
+ unsigned_nda = None
+ if bounty_payload.get('unsigned_nda', None):
+ unsigned_nda = BountyDocuments.objects.filter(
+ pk=bounty_payload.get('unsigned_nda')
+ ).first()
bounty_kwargs.update({
# info to xfr over from latest_old_bounty as override fields (this is because sometimes
# ppl dont login when they first submit issue and it needs to be overridden)
@@ -433,6 +445,8 @@ def create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id):
'featuring_date': timezone.make_aware(
timezone.datetime.fromtimestamp(metadata.get('featuring_date', 0)),
timezone=UTC),
+ 'repo_type': metadata.get('repo_type', None),
+ 'unsigned_nda': unsigned_nda,
'bounty_owner_github_username': bounty_issuer.get('githubUsername', ''),
'bounty_owner_address': bounty_issuer.get('address', ''),
'bounty_owner_email': bounty_issuer.get('email', ''),
@@ -448,7 +462,8 @@ def create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id):
'bounty_owner_github_username', 'bounty_owner_address', 'bounty_owner_email', 'bounty_owner_name',
'github_comments', 'override_status', 'last_comment_date', 'snooze_warnings_for_days',
'admin_override_and_hide', 'admin_override_suspend_auto_approval', 'admin_mark_as_remarket_ready',
- 'funding_organisation', 'bounty_reserved_for_user', 'is_featured', 'featuring_date',
+ 'funding_organisation', 'bounty_reserved_for_user', 'is_featured', 'featuring_date', 'repo_type',
+ 'unsigned_nda'
],
)
if latest_old_bounty_dict['bounty_reserved_for_user']:
diff --git a/app/dashboard/migrations/0021_auto_20190319_1502.py b/app/dashboard/migrations/0021_auto_20190319_1502.py
new file mode 100644
index 00000000000..bd6c476d931
--- /dev/null
+++ b/app/dashboard/migrations/0021_auto_20190319_1502.py
@@ -0,0 +1,49 @@
+# Generated by Django 2.1.7 on 2019-03-19 15:02
+
+import app.utils
+from django.db import migrations, models
+import django.db.models.deletion
+import economy.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0020_auto_20190312_0739'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BountyDocuments',
+ 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)),
+ ('doc', models.FileField(blank=True, help_text='Bounty documents.', null=True, upload_to=app.utils.get_upload_filename)),
+ ('doc_type', models.CharField(max_length=50)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='bounty',
+ name='repo_type',
+ field=models.CharField(choices=[('public', 'public'), ('private', 'private')], default='public', max_length=50),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='resume',
+ field=models.FileField(blank=True, help_text='The profile resume.', null=True, upload_to=app.utils.get_upload_filename),
+ ),
+ migrations.AddField(
+ model_name='bounty',
+ name='unsigned_nda',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unsignednda', to='dashboard.BountyDocuments'),
+ ),
+ migrations.AddField(
+ model_name='interest',
+ name='signed_nda',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='signednda', to='dashboard.BountyDocuments'),
+ ),
+ ]
diff --git a/app/dashboard/migrations/0022_merge_20190325_1402.py b/app/dashboard/migrations/0022_merge_20190325_1402.py
new file mode 100644
index 00000000000..4045004127a
--- /dev/null
+++ b/app/dashboard/migrations/0022_merge_20190325_1402.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.1.7 on 2019-03-25 14:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0021_auto_20190319_1502'),
+ ('dashboard', '0021_profile_custom_tagline'),
+ ]
+
+ operations = [
+ ]
diff --git a/app/dashboard/migrations/0023_auto_20190325_1404.py b/app/dashboard/migrations/0023_auto_20190325_1404.py
new file mode 100644
index 00000000000..f27d292c609
--- /dev/null
+++ b/app/dashboard/migrations/0023_auto_20190325_1404.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.1.7 on 2019-03-25 14:04
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0022_merge_20190325_1402'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='bounty',
+ name='unsigned_nda',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bounty', to='dashboard.BountyDocuments'),
+ ),
+ migrations.AlterField(
+ model_name='bountyinvites',
+ name='bounty',
+ field=models.ManyToManyField(blank=True, related_name='bountyinvites', to='dashboard.Bounty'),
+ ),
+ migrations.AlterField(
+ model_name='interest',
+ name='signed_nda',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interest', to='dashboard.BountyDocuments'),
+ ),
+ ]
diff --git a/app/dashboard/models.py b/app/dashboard/models.py
index 2d60b4aafba..13f73df0f99 100644
--- a/app/dashboard/models.py
+++ b/app/dashboard/models.py
@@ -42,7 +42,7 @@
import pytz
import requests
-from avatar.utils import get_upload_filename
+from app.utils import get_upload_filename
from dashboard.tokens import addr_to_token
from economy.models import ConversionRate, SuperModel
from economy.utils import ConversionRateNotFoundError, convert_amount, convert_token_to_usdt
@@ -177,6 +177,10 @@ class Bounty(SuperModel):
('permissionless', 'permissionless'),
('approval', 'approval'),
]
+ REPO_TYPES = [
+ ('public', 'public'),
+ ('private', 'private'),
+ ]
PROJECT_TYPES = [
('traditional', 'traditional'),
('contest', 'contest'),
@@ -271,10 +275,12 @@ 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')
permission_type = models.CharField(max_length=50, choices=PERMISSION_TYPES, default='permissionless')
+ repo_type = models.CharField(max_length=50, choices=REPO_TYPES, default='public')
snooze_warnings_for_days = models.IntegerField(default=0)
is_featured = models.BooleanField(
default=False, help_text=_('Whether this bounty is featured'))
featuring_date = models.DateTimeField(blank=True, null=True)
+ unsigned_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='bounty', on_delete=models.SET_NULL)
token_value_time_peg = models.DateTimeField(blank=True, null=True)
token_value_in_usdt = models.DecimalField(default=0, decimal_places=2, max_digits=50, blank=True, null=True)
@@ -1102,6 +1108,12 @@ def __str__(self):
return f"{self.email} {self.created_on}"
+class BountyDocuments(SuperModel):
+
+ doc = models.FileField(upload_to=get_upload_filename, null=True, blank=True, help_text=_('Bounty documents.'))
+ doc_type = models.CharField(max_length=50)
+
+
class SendCryptoAssetQuerySet(models.QuerySet):
"""Handle the manager queryset for SendCryptoAsset."""
@@ -1457,6 +1469,7 @@ class Interest(SuperModel):
max_length=7,
help_text=_('Whether or not the interest requires review'),
verbose_name=_('Needs Review'))
+ signed_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='interest', on_delete=models.SET_NULL)
# Interest QuerySet Manager
objects = InterestQuerySet.as_manager()
@@ -1745,7 +1758,7 @@ class BountyInvites(SuperModel):
('completed', 'completed'),
]
- bounty = models.ManyToManyField('dashboard.Bounty', related_name='bounty', blank=True)
+ bounty = models.ManyToManyField('dashboard.Bounty', related_name='bountyinvites', blank=True)
inviter = models.ManyToManyField(User, related_name='inviter', blank=True)
invitee = models.ManyToManyField(User, related_name='invitee', blank=True)
status = models.CharField(max_length=20, choices=INVITE_STATUS, blank=True)
@@ -1838,7 +1851,7 @@ class Profile(SuperModel):
job_salary = models.DecimalField(default=1, decimal_places=2, max_digits=50)
job_location = JSONField(default=dict, blank=True)
linkedin_url = models.CharField(max_length=255, default='', blank=True, null=True)
- resume = models.FileField(upload_to=get_upload_filename, null=True, blank=True, help_text=_('The avatar SVG.'))
+ resume = models.FileField(upload_to=get_upload_filename, null=True, blank=True, help_text=_('The profile resume.'))
objects = ProfileQuerySet.as_manager()
diff --git a/app/dashboard/router.py b/app/dashboard/router.py
index 25b1d86d399..f8a1c44447d 100644
--- a/app/dashboard/router.py
+++ b/app/dashboard/router.py
@@ -25,7 +25,9 @@
from rest_framework import routers, serializers, viewsets
from retail.helpers import get_ip
-from .models import Activity, Bounty, BountyFulfillment, BountyInvites, Interest, ProfileSerializer, SearchHistory
+from .models import (
+ Activity, Bounty, BountyDocuments, BountyFulfillment, BountyInvites, Interest, ProfileSerializer, SearchHistory,
+)
class BountyFulfillmentSerializer(serializers.ModelSerializer):
@@ -40,16 +42,26 @@ class Meta:
'fulfillment_id', 'accepted', 'profile', 'created_on', 'accepted_on', 'fulfiller_github_url')
-class InterestSerializer(serializers.ModelSerializer):
- """Handle serializing the Interest object."""
+class ActivitySerializer(serializers.ModelSerializer):
+ """Handle serializing the Activity object."""
profile = ProfileSerializer()
class Meta:
- """Define the Interest serializer metadata."""
+ """Define the activity serializer metadata."""
- model = Interest
- fields = ('profile', 'created', 'pending')
+ model = Activity
+ fields = ('activity_type', 'created', 'profile', 'metadata', 'bounty', 'tip')
+
+
+class BountyDocumentsSerializer(serializers.ModelSerializer):
+ """Handle serializing the Activity object."""
+
+ class Meta:
+ """Define the activity serializer metadata."""
+
+ model = BountyDocuments
+ fields = ('doc', 'doc_type')
class KudosSerializer(serializers.ModelSerializer):
@@ -76,6 +88,18 @@ class Meta:
fields = ('activity_type', 'created', 'profile', 'metadata', 'bounty', 'tip', 'kudos')
+class InterestSerializer(serializers.ModelSerializer):
+ """Handle serializing the Interest object."""
+
+ profile = ProfileSerializer()
+ signed_nda = BountyDocumentsSerializer()
+
+ class Meta:
+ """Define the Interest serializer metadata."""
+ model = Interest
+ fields = ('profile', 'created', 'pending', 'signed_nda')
+
+
# Serializers define the API representation.
class BountySerializer(serializers.HyperlinkedModelSerializer):
"""Handle serializing the Bounty object."""
@@ -83,6 +107,7 @@ class BountySerializer(serializers.HyperlinkedModelSerializer):
fulfillments = BountyFulfillmentSerializer(many=True)
interested = InterestSerializer(many=True)
activities = ActivitySerializer(many=True)
+ unsigned_nda = BountyDocumentsSerializer(many=False)
bounty_owner_email = serializers.SerializerMethodField('override_bounty_owner_email')
bounty_owner_name = serializers.SerializerMethodField('override_bounty_owner_name')
@@ -112,8 +137,8 @@ class Meta:
'fulfillment_submitted_on', 'fulfillment_started_on', 'canceled_on', 'canceled_bounty_reason',
'action_urls', 'project_type', 'permission_type', 'attached_job_description', 'needs_review',
'github_issue_state', 'is_issue_closed', 'additional_funding_summary', 'funding_organisation', 'paid',
- 'admin_override_suspend_auto_approval', 'reserved_for_user_handle', 'is_featured', 'featuring_date',
- 'funder_last_messaged_on',
+ 'admin_override_suspend_auto_approval', 'reserved_for_user_handle', 'is_featured', 'featuring_date', 'repo_type',
+ 'unsigned_nda', 'funder_last_messaged_on',
)
def create(self, validated_data):
@@ -149,7 +174,7 @@ def update(self, validated_data):
class BountyViewSet(viewsets.ModelViewSet):
"""Handle the Bounty view behavior."""
- queryset = Bounty.objects.prefetch_related('fulfillments', 'interested', 'interested__profile', 'activities') \
+ queryset = Bounty.objects.prefetch_related('fulfillments', 'interested', 'interested__profile', 'activities', 'unsigned_nda') \
.all().order_by('-web3_created')
serializer_class = BountySerializer
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
@@ -163,7 +188,7 @@ def get_queryset(self):
"""
param_keys = self.request.query_params.keys()
queryset = Bounty.objects.prefetch_related(
- 'fulfillments', 'interested', 'interested__profile', 'activities')
+ 'fulfillments', 'interested', 'interested__profile', 'activities', 'unsigned_nda')
if 'not_current' not in param_keys:
queryset = queryset.current()
@@ -286,6 +311,11 @@ def get_queryset(self):
is_open=True,
)
+ if 'repo_type' in param_keys:
+ queryset = queryset.filter(
+ repo_type=self.request.query_params.get('repo_type'),
+ )
+
# order
order_by = self.request.query_params.get('order_by')
if order_by and order_by != 'null':
diff --git a/app/dashboard/templates/addinterest.html b/app/dashboard/templates/addinterest.html
index b73f8443484..f229e468623 100644
--- a/app/dashboard/templates/addinterest.html
+++ b/app/dashboard/templates/addinterest.html
@@ -15,10 +15,10 @@
along with this program. If not, see .
{% endcomment %}
{% load i18n static %}
-
+
-
-
+
+
{% trans "Submit a Plan" %}
@@ -26,6 +26,18 @@
{% trans "Submit a Plan" %}
{% if user_logged_in %}