diff --git a/app/assets/v2/css/dashboard.css b/app/assets/v2/css/dashboard.css index 68f24d31c82..b06e4073608 100644 --- a/app/assets/v2/css/dashboard.css +++ b/app/assets/v2/css/dashboard.css @@ -245,6 +245,10 @@ body { color: #00A652; } +.status-reserved { + color: #d58512; +} + .status-started { color: #0D0764; } diff --git a/app/assets/v2/css/submit_bounty.css b/app/assets/v2/css/submit_bounty.css index 9b667d17bf7..e05744e1105 100644 --- a/app/assets/v2/css/submit_bounty.css +++ b/app/assets/v2/css/submit_bounty.css @@ -227,3 +227,13 @@ input:read-only { .letter-spacing { letter-spacing: 0.1em; } + +.release-after-form-group { + display: none; + margin-top: 0.75em; +} + +.release-after-form-group-required { + border: 1px solid red; + padding: 5px; +} diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js index face40e526b..c1f6e5022c1 100644 --- a/app/assets/v2/js/pages/bounty_details.js +++ b/app/assets/v2/js/pages/bounty_details.js @@ -448,10 +448,7 @@ var callbacks = { }, 'reserved_for_user_handle': function(key, val, bounty) { if (val) { - const reservedForHoursLeft = 72 - Math.abs(new Date() - new Date(bounty['created_on'])) / 3600000; - - // check if 24 hours have passed before setting the issue as reserved - if (Math.round(reservedForHoursLeft) > 0) { + if (bounty['is_reserved'] && bounty['status'] === 'reserved') { const reservedForHtmlLink = '' + val + ''; const reservedForAvatar = ''; @@ -475,10 +472,7 @@ const isAvailableIfReserved = function(bounty) { return true; } - const reservedForHoursLeft = 72 - Math.abs(new Date() - new Date(bounty['created_on'])) / 3600000; - - // check if 24 hours have passed before setting the issue as reserved - if (Math.round(reservedForHoursLeft) > 0) { + if (bounty['is_reserved']) { return false; } } @@ -1037,7 +1031,7 @@ var do_actions = function(result) { var is_status_done = result['status'] == 'done'; var is_status_cancelled = result['status'] == 'cancelled'; var can_submit_after_expiration_date = result['can_submit_after_expiration_date']; - var is_still_on_happy_path = result['status'] == 'open' || result['status'] == 'started' || result['status'] == 'submitted' || (can_submit_after_expiration_date && result['status'] == 'expired'); + var is_still_on_happy_path = result['status'] == 'reserved' || result['status'] == 'open' || result['status'] == 'started' || result['status'] == 'submitted' || (can_submit_after_expiration_date && result['status'] == 'expired'); var needs_review = result['needs_review']; const is_open = result['is_open']; let bounty_path = result['network'] + '/' + result['standard_bounties_id']; @@ -1132,7 +1126,7 @@ var do_actions = function(result) { let text; if (result['permission_type'] === 'approval') - text = is_interested ? gettext('Stop') : gettext('Express Interest'); + text = is_interested ? gettext('Stop Work') : gettext('Express Interest'); else text = is_interested ? gettext('Stop Work') : gettext('Start Work'); @@ -1149,6 +1143,21 @@ var do_actions = function(result) { actions.push(interest_entry); } + if (result['is_reserved'] && result['status'] === 'reserved' && + caseInsensitiveCompare(result['reserved_for_user_handle'], document.contxt['github_handle'])) { + const connector_char = result['url'].indexOf('?') == -1 ? '?' : '&'; + + const release_to_public_entry = { + enabled: true, + href: result['url'] + connector_char + 'release_to_public=1', + text: gettext('Release Bounty'), + parent: 'bounty_actions', + title: gettext('Release this reserved bounty to the public') + }; + + actions.push(release_to_public_entry); + } + if (show_kill_bounty) { const enabled = isBountyOwner(result); const _entry = { diff --git a/app/assets/v2/js/pages/dashboard.js b/app/assets/v2/js/pages/dashboard.js index 2dc243a1c98..35bc5e0402d 100644 --- a/app/assets/v2/js/pages/dashboard.js +++ b/app/assets/v2/js/pages/dashboard.js @@ -363,6 +363,9 @@ var get_search_URI = function(offset, order) { } else if (_value === 'fulfilledByMe') { _key = 'fulfiller_github_username'; _value = document.contxt.github_handle; + } else if (_value === 'reservedForMe') { + _key = 'reserved_for_user_handle'; + _value = document.contxt.github_handle; } if (_value !== 'any') { diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index f6f7e8a1314..f639755d6f4 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -421,8 +421,20 @@ $(function() { }); -$('#reservedFor').on('select2:select', function(e) { +$('#reservedFor').on('select2:select', (e) => { $('#permissionless').click(); + $('#releaseAfterFormGroup').show(); + $('#releaseAfter').attr('required', true); +}); + +$('#reservedFor').on('select2:unselect', (e) => { + $('#releaseAfterFormGroup').hide(); + $('#releaseAfter').attr('required', false); + $('#releaseAfterFormGroup').addClass('releaseAfterFormGroupRequired'); +}); + +$('#releaseAfter').on('change', () => { + $('#releaseAfterFormGroup').removeClass('releaseAfterFormGroupRequired'); }); $('#sync-issue').on('click', function(event) { @@ -566,6 +578,7 @@ $('#submitBounty').validate({ var decimalDivisor = Math.pow(10, decimals); var expirationTimeDelta = $('#expirationTimeDelta').data('daterangepicker').endDate.utc().unix(); let reservedFor = $('.username-search').select2('data')[0]; + let releaseAfter = $('#releaseAfter').children('option:selected').val(); let inviteContributors = $('#invite-contributors.js-select2').select2('data').map((user) => { return user.profile__id; }); @@ -587,6 +600,7 @@ $('#submitBounty').validate({ repo_type: data.repo_type, featuring_date: data.featuredBounty && ((new Date().getTime() / 1000) | 0) || 0, reservedFor: reservedFor ? reservedFor.text : '', + releaseAfter: releaseAfter !== 'Release To Public After' ? releaseAfter : '', tokenName, invite: inviteContributors, bounty_categories: data.bounty_category diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index d721b594b76..ab324053063 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -601,12 +601,18 @@ def merge_bounty(latest_old_bounty, new_bounty, metadata, bounty_details, verbos for activity in latest_old_bounty.activities.all().nocache(): new_bounty.activities.add(activity) - bounty_reserved_for_user = metadata.get('reservedFor', '') - if bounty_reserved_for_user: + release_to_public_after = metadata.get('releaseAfter', '') + if bounty_reserved_for_user and release_to_public_after: + new_bounty.reserved_for_user_from = timezone.now() + if release_to_public_after == "3-days": + new_bounty.reserved_for_user_expiration = new_bounty.reserved_for_user_from + timezone.timedelta(days=3) + elif release_to_public_after == "1-week": + new_bounty.reserved_for_user_expiration = new_bounty.reserved_for_user_from + timezone.timedelta(weeks=1) + new_bounty.reserved_for_user_handle = bounty_reserved_for_user new_bounty.save() - if new_bounty.bounty_reserved_for_user: + if new_bounty.bounty_reserved_for_user and new_bounty.status == 'reserved': # notify a user that a bounty has been reserved for them new_reserved_issue('founders@gitcoin.co', new_bounty.bounty_reserved_for_user, new_bounty) diff --git a/app/dashboard/migrations/0052_auto_20190919_1445.py b/app/dashboard/migrations/0052_auto_20190919_1445.py new file mode 100644 index 00000000000..2064e509f82 --- /dev/null +++ b/app/dashboard/migrations/0052_auto_20190919_1445.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.4 on 2019-09-19 14:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0051_profile_gitcoin_discord_username'), + ] + + operations = [ + migrations.AddField( + model_name='bounty', + name='reserved_for_user_expiration', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='bounty', + name='reserved_for_user_from', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='bounty', + name='idx_status', + field=models.CharField(choices=[('cancelled', 'cancelled'), ('done', 'done'), ('expired', 'expired'), ('reserved', 'reserved'), ('open', 'open'), ('started', 'started'), ('submitted', 'submitted'), ('unknown', 'unknown')], db_index=True, default='open', max_length=9), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index ae3dfb90601..fa7b84f8c6c 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -247,15 +247,16 @@ class Bounty(SuperModel): ('cancelled', 'cancelled'), ('done', 'done'), ('expired', 'expired'), + ('reserved', 'reserved'), ('open', 'open'), ('started', 'started'), ('submitted', 'submitted'), ('unknown', 'unknown'), ) - FUNDED_STATUSES = ['open', 'started', 'submitted', 'done'] - OPEN_STATUSES = ['open', 'started', 'submitted'] + FUNDED_STATUSES = ['reserved', 'open', 'started', 'submitted', 'done'] + OPEN_STATUSES = ['reserved', 'open', 'started', 'submitted'] CLOSED_STATUSES = ['expired', 'unknown', 'cancelled', 'done'] - WORK_IN_PROGRESS_STATUSES = ['open', 'started', 'submitted'] + WORK_IN_PROGRESS_STATUSES = ['reserved', 'open', 'started', 'submitted'] TERMINAL_STATUSES = ['done', 'expired', 'cancelled'] web3_type = models.CharField(max_length=50, default='bounties_network') @@ -281,6 +282,8 @@ class Bounty(SuperModel): bounty_reserved_for_user = models.ForeignKey( 'dashboard.Profile', null=True, on_delete=models.SET_NULL, related_name='reserved_bounties', blank=True ) + reserved_for_user_from = models.DateTimeField(blank=True, null=True) + reserved_for_user_expiration = models.DateTimeField(blank=True, null=True) is_open = models.BooleanField(help_text=_('Whether the bounty is still open for fulfillments.')) expires_date = models.DateTimeField() raw_data = JSONField() @@ -727,6 +730,8 @@ def status(self): if self.num_fulfillments == 0: if self.pk and self.interested.filter(pending=False).exists(): return 'started' + elif self.is_reserved: + return 'reserved' return 'open' return 'submitted' except Exception as e: @@ -1158,8 +1163,8 @@ def can_remarket(self): result = False if self.last_remarketed: - one_hour_after_remarketing = self.last_remarketed + timezone.timedelta(hours=1) - if timezone.now() < one_hour_after_remarketing: + minimum_wait_after_remarketing = self.last_remarketed + timezone.timedelta(minutes=settings.MINUTES_BETWEEN_RE_MARKETING) + if timezone.now() < minimum_wait_after_remarketing: result = False if self.interested.count() > 0: @@ -1167,6 +1172,50 @@ def can_remarket(self): return result + @property + def is_reserved(self): + if self.bounty_reserved_for_user and self.reserved_for_user_from: + if timezone.now() < self.reserved_for_user_from: + return False + + if self.reserved_for_user_expiration and timezone.now() > self.reserved_for_user_expiration: + return False + + return True + + @property + def total_reserved_length_label(self): + if self.bounty_reserved_for_user and self.reserved_for_user_from: + if self.reserved_for_user_expiration is None: + return 'indefinitely' + + if self.reserved_for_user_from == self.reserved_for_user_expiration: + return '' + + delta = self.reserved_for_user_expiration - self.reserved_for_user_from + days = delta.days + + if days > 0: + if days % 7 == 0: + if days == 7: + return '1 week' + else: + weeks = int(days / 7) + return f'{weeks} weeks' + + if days == 1: + return '1 day' + else: + return f'{days} days' + else: + hours = int(int(delta.total_seconds()) / 3600) + if hours == 1: + return '1 hour' + else: + return f'{hours} hours' + else: + return '' + class BountyFulfillmentQuerySet(models.QuerySet): """Handle the manager queryset for BountyFulfillments.""" diff --git a/app/dashboard/router.py b/app/dashboard/router.py index 71dd9359309..5feff114f44 100644 --- a/app/dashboard/router.py +++ b/app/dashboard/router.py @@ -29,8 +29,8 @@ from retail.helpers import get_ip from .models import ( - Activity, Bounty, BountyDocuments, BountyFulfillment, BountyInvites, HackathonEvent, Interest, ProfileSerializer, - SearchHistory, + Activity, Bounty, BountyDocuments, BountyFulfillment, BountyInvites, HackathonEvent, Interest, Profile, + ProfileSerializer, SearchHistory, ) logger = logging.getLogger(__name__) @@ -157,7 +157,7 @@ class Meta: 'attached_job_description', 'needs_review', 'github_issue_state', 'is_issue_closed', 'additional_funding_summary', 'funding_organisation', 'paid', 'event', 'admin_override_suspend_auto_approval', 'reserved_for_user_handle', 'is_featured', - 'featuring_date', 'repo_type', 'unsigned_nda', 'funder_last_messaged_on', 'can_remarket' + 'featuring_date', 'repo_type', 'unsigned_nda', 'funder_last_messaged_on', 'can_remarket', 'is_reserved' ) def create(self, validated_data): @@ -269,6 +269,15 @@ def get_queryset(self): _queryset = _queryset | queryset.filter(**args) queryset = _queryset + if 'reserved_for_user_handle' in param_keys: + handle = self.request.query_params.get('reserved_for_user_handle', '') + if handle: + try: + profile = Profile.objects.filter(handle__iexact=handle).first() + queryset = queryset.filter(bounty_reserved_for_user=profile) + except: + logger.warning(f'reserved_for_user_handle: Unknown handle: ${handle}') + # filter by PK if 'pk__gt' in param_keys: queryset = queryset.filter(pk__gt=self.request.query_params.get('pk__gt')) diff --git a/app/dashboard/templates/dashboard/sidebar_search.html b/app/dashboard/templates/dashboard/sidebar_search.html index b92acd11763..95baeb23cf7 100644 --- a/app/dashboard/templates/dashboard/sidebar_search.html +++ b/app/dashboard/templates/dashboard/sidebar_search.html @@ -166,6 +166,67 @@ +
+
+ {% trans "Status" %} + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ {% trans "Applicants" %} +
+
+ +
+
+
{% trans "Advanced Filters" %} @@ -187,6 +248,10 @@
+
+ + +
diff --git a/app/dashboard/templates/shared/reserved.html b/app/dashboard/templates/shared/reserved.html index 4c550953312..f5d0ec92695 100644 --- a/app/dashboard/templates/shared/reserved.html +++ b/app/dashboard/templates/shared/reserved.html @@ -20,6 +20,18 @@
+ +
+ +
+ +
+
diff --git a/app/dashboard/tests/test_dashboard_models.py b/app/dashboard/tests/test_dashboard_models.py index a7f1a0b4adc..3fdc25275b2 100644 --- a/app/dashboard/tests/test_dashboard_models.py +++ b/app/dashboard/tests/test_dashboard_models.py @@ -663,3 +663,191 @@ def get_all_tokens_sum(): assert query[1]['token_name'] == 'ETH' assert query[1]['value_in_token'] == 6 + + @staticmethod + def test_total_reserved_length_label_empty_when_bounty_not_reserved(): + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + raw_data={} + ) + + assert bounty.total_reserved_length_label == '' + + @staticmethod + def test_total_reserved_length_label_is_indefinite_when_no_expiration_set(): + dummy_profile = Profile( + handle='foo' + ) + + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=timezone.now(), + raw_data={} + ) + + assert bounty.total_reserved_length_label == 'indefinitely' + + @staticmethod + def test_total_reserved_length_label_empty_when_from_and_exp_date_the_same(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now, + raw_data={} + ) + + assert bounty.total_reserved_length_label == '' + + @staticmethod + def test_total_reserved_length_label_when_reservation_is_one_week(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(weeks=1), + raw_data={} + ) + + assert bounty.total_reserved_length_label == '1 week' + + @staticmethod + def test_total_reserved_length_label_when_reservation_is_two_weeks(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(weeks=2), + raw_data={} + ) + + assert bounty.total_reserved_length_label == '2 weeks' + + @staticmethod + def test_total_reserved_length_label_when_reservation_is_one_day(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(days=1), + raw_data={} + ) + + assert bounty.total_reserved_length_label == '1 day' + + @staticmethod + def test_total_reserved_length_label_when_reservation_is_three_days(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(days=3), + raw_data={} + ) + + assert bounty.total_reserved_length_label == '3 days' + + @staticmethod + def test_total_reserved_length_label_when_reservation_is_one_hour(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(hours=1), + raw_data={} + ) + + assert bounty.total_reserved_length_label == '1 hour' + + @staticmethod + def test_total_reserved_length_label_when_reservation_is_three_hours(): + dummy_profile = Profile( + handle='foo' + ) + + now = timezone.now() + bounty = Bounty( + title='ReservedLengthLabelTest', + idx_status=0, + is_open=True, + web3_created=datetime(2008, 10, 31, tzinfo=pytz.UTC), + expires_date=datetime(2008, 11, 30, tzinfo=pytz.UTC), + github_url='https://github.com/gitcoinco/web/issues/12345678', + bounty_reserved_for_user=dummy_profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(hours=3), + raw_data={} + ) + + assert bounty.total_reserved_length_label == '3 hours' diff --git a/app/dashboard/tests/test_dashboard_utils.py b/app/dashboard/tests/test_dashboard_utils.py index 4e6a3aa6497..7de5928d97a 100644 --- a/app/dashboard/tests/test_dashboard_utils.py +++ b/app/dashboard/tests/test_dashboard_utils.py @@ -26,10 +26,11 @@ import ipfshttpclient import pytest -from dashboard.models import Bounty +from dashboard.models import Bounty, Profile from dashboard.utils import ( IPFSCantConnectException, apply_new_bounty_deadline, clean_bounty_url, create_user_action, get_bounty, get_ipfs, get_ordinal_repr, get_web3, getBountyContract, humanize_event_name, ipfs_cat_ipfsapi, re_market_bounty, + release_bounty_to_the_public, ) from pytz import UTC from test_plus.test import TestCase @@ -263,3 +264,36 @@ def test_apply_new_bounty_deadline_is_successful_with_re_market(): timezone=UTC ) assert bounty.expires_date == deadline_as_date_time + + @staticmethod + def test_release_bounty_to_public_fails_when_bounty_is_none(): + assert release_bounty_to_the_public(None) is False + + @staticmethod + def test_release_bounty_to_public_is_successful(): + now = timezone.now() + profile = Profile( + handle='foo', + ) + bounty = Bounty( + title='ReleaseToPublicTrueTest', + idx_status='reserved', + is_open=True, + web3_created=now, + expires_date=now + timezone.timedelta(minutes=2), + bounty_reserved_for_user=profile, + reserved_for_user_from=now, + reserved_for_user_expiration=now + timezone.timedelta(minutes=2), + github_url='https://github.com/gitcoinco/web/issues/12345678', + raw_data={} + ) + + assert bounty.bounty_reserved_for_user is not None + assert bounty.reserved_for_user_from is not None + assert bounty.reserved_for_user_expiration is not None + + assert release_bounty_to_the_public(bounty, False) is True + + assert bounty.bounty_reserved_for_user is None + assert bounty.reserved_for_user_from is None + assert bounty.reserved_for_user_expiration is None diff --git a/app/dashboard/utils.py b/app/dashboard/utils.py index a5b0e0bb54f..4e992e7c8b1 100644 --- a/app/dashboard/utils.py +++ b/app/dashboard/utils.py @@ -882,3 +882,17 @@ def apply_new_bounty_deadline(bounty, deadline, auto_save = True): result['msg'] = base_result_msg + " " + result['msg'] return result + + +def release_bounty_to_the_public(bounty, auto_save = True): + if bounty: + bounty.reserved_for_user_handle = None + bounty.reserved_for_user_from = None + bounty.reserved_for_user_expiration = None + + if auto_save: + bounty.save() + + return True + else: + return False diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 68d3ac7d44d..5bf0faefaad 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -84,7 +84,7 @@ ) from .utils import ( apply_new_bounty_deadline, get_bounty, get_bounty_id, get_context, get_unrated_bounties_count, get_web3, - has_tx_mined, re_market_bounty, record_user_action_on_interest, web3_process_bounty, + has_tx_mined, re_market_bounty, record_user_action_on_interest, release_bounty_to_the_public, web3_process_bounty, ) logger = logging.getLogger(__name__) @@ -1691,6 +1691,26 @@ def helper_handle_remarket_trigger(request, bounty): messages.warning(request, _('Only staff or the funder of this bounty may do this.')) +def helper_handle_release_bounty_to_public(request, bounty): + release_to_public = request.GET.get('release_to_public', False) + if release_to_public: + is_bounty_status_reserved = bounty.status == 'reserved' + if is_bounty_status_reserved: + is_staff = request.user.is_staff + is_bounty_reserved_for_user = bounty.reserved_for_user_handle == request.user.username.lower() + if is_staff or is_bounty_reserved_for_user: + success = release_bounty_to_the_public(bounty) + if success: + messages.success(request, _('You have successfully released this bounty to the public')) + else: + messages.warning(request, _('An error has occurred whilst trying to release. Please try again later')) + else: + messages.warning(request, _('Only staff or the user that has been reserved can release this bounty')) + else: + messages.warning(request, _('This functionality is only for reserved bounties')) + + + @login_required def bounty_invite_url(request, invitecode): """Decode the bounty details and redirect to correct bounty @@ -1809,6 +1829,7 @@ def bounty_details(request, ghuser='', ghrepo='', ghissue=0, stdbounties_id=None helper_handle_suspend_auto_approval(request, bounty) helper_handle_mark_as_remarket_ready(request, bounty) helper_handle_remarket_trigger(request, bounty) + helper_handle_release_bounty_to_public(request, bounty) helper_handle_admin_contact_funder(request, bounty) helper_handle_override_status(request, bounty) except Bounty.DoesNotExist: diff --git a/app/retail/templates/emails/reserved_issue.html b/app/retail/templates/emails/reserved_issue.html index 5b612510d89..9f5646482b3 100644 --- a/app/retail/templates/emails/reserved_issue.html +++ b/app/retail/templates/emails/reserved_issue.html @@ -23,8 +23,14 @@

{% trans "Reserved Issue" %}

{% trans "Hello" %} @{{ user.handle }},

- {% trans "Good news! An "%} {% trans "issue" %}. {% trans " has been reserved for you on gitcoin. "%} - {% trans "Please start working on the issue, in the next 72 hours, before it is opened up for other bounty hunters to try as well." %} + {% trans "Good news! An "%} {% trans "issue" %} {% trans " has been reserved for you on gitcoin. "%} + {% if bounty.total_reserved_length_label == "indefinitely" %} + {% trans "Please start working on the issue and release it to the public if you cannot commit to the work." %} + {% else %} + {% blocktrans with total_reserved_length_label=bounty.total_reserved_length_label %} + Please start working on the issue, within the next {{ total_reserved_length_label }}, before it is opened up for other bounty hunters to try as well. + {% endblocktrans %} + {% endif %}


{% include 'emails/bounty.html' with bounty=bounty %} diff --git a/app/retail/templates/emails/reserved_issue.txt b/app/retail/templates/emails/reserved_issue.txt index 9c02168ff64..333ba1539bd 100644 --- a/app/retail/templates/emails/reserved_issue.txt +++ b/app/retail/templates/emails/reserved_issue.txt @@ -1,4 +1,11 @@ {% load i18n %} -{% trans "Good news! An issue has been reserved for you on gitcoin. Please start working on the issue, in the next 72 hours, before it is opened up for other bounty hunters to try as well. " %} +{% trans "Good news! An issue has been reserved for you on gitcoin. " %} +{% if bounty.total_reserved_length_label == "indefinitely" %} + {% trans "Please start working on the issue and release it to the public if you cannot commit to the work." %} +{% else %} + {% blocktrans with total_reserved_length_label=bounty.total_reserved_length_label %} + Please start working on the issue, within the next {{ total_reserved_length_label }}, before it is opened up for other bounty hunters to try as well. + {% endblocktrans %} +{% endif %} {% include 'emails/bounty.txt' with bounty=bounty %} {% trans "As always, if you have questions, please reach out to the project owner!" %} diff --git a/app/retail/templates/shared/result.html b/app/retail/templates/shared/result.html index 61e368e0d46..e5d8d6dcc7e 100644 --- a/app/retail/templates/shared/result.html +++ b/app/retail/templates/shared/result.html @@ -28,6 +28,8 @@ [[if status === "open"]] {% trans "Ready to work" %} + [[else status === "reserved"]] + {% trans "Reserved" %} [[else status === "started"]] {% trans "Work Started" %} [[else status === "submitted"]]