Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-surface an issue to the top of the issues explorer when its expiration has been extended #5012

Merged
merged 2 commits into from
Aug 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,6 @@
'name': 'Index View',
}]
SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = env.int('SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT', default=10)

RE_MARKET_LIMIT = env.int('RE_MARKET_LIMIT', default=2)
MINUTES_BETWEEN_RE_MARKETING = env.int('MINUTES_BETWEEN_RE_MARKETING', default=60)
133 changes: 131 additions & 2 deletions app/dashboard/tests/test_dashboard_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
from datetime import datetime
from unittest.mock import patch

from django.conf import settings
from django.test.client import RequestFactory
from django.utils import timezone

import ipfshttpclient
import pytest
from dashboard.models import Bounty
from dashboard.utils import (
IPFSCantConnectException, clean_bounty_url, create_user_action, get_bounty, get_ipfs, get_ordinal_repr, get_web3,
getBountyContract, humanize_event_name, ipfs_cat_ipfsapi,
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,
)
from pytz import UTC
from test_plus.test import TestCase
from web3.main import Web3
from web3.providers.rpc import HTTPProvider
Expand Down Expand Up @@ -134,3 +138,128 @@ def test_get_ipfs_with_bad_host():
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'))

@staticmethod
def test_can_successfully_re_market_a_bounty():
bounty = Bounty.objects.create(
title='CanRemarketTrueTest',
idx_status=0,
is_open=True,
web3_created=datetime(2008, 10, 31, tzinfo=UTC),
expires_date=datetime(2008, 11, 30, tzinfo=UTC),
github_url='https://github.com/gitcoinco/web/issues/12345678',
raw_data={}
)

assert bounty.remarketed_count == 0

result = re_market_bounty(bounty, False)

assert result['success'] is True
assert result['msg'] == f"The issue will appear at the top of the issue explorer. You will be able to remarket this bounty {settings.RE_MARKET_LIMIT - 1} more time if a contributor does not pick this up."
assert bounty.remarketed_count == 1
assert bounty.last_remarketed > (timezone.now() - timezone.timedelta(minutes=1))

@staticmethod
def test_can_successfully_re_market_a_bounty_twice():
if settings.RE_MARKET_LIMIT == 2:
bounty = Bounty.objects.create(
title='CanRemarketTrueTest',
idx_status=0,
is_open=True,
web3_created=datetime(2008, 10, 31, tzinfo=UTC),
expires_date=datetime(2008, 11, 30, tzinfo=UTC),
last_remarketed=datetime(2008, 10, 31, tzinfo=UTC),
github_url='https://github.com/gitcoinco/web/issues/12345678',
raw_data={}
)

result = re_market_bounty(bounty, False)
assert result['success'] is True
assert result['msg'] == "The issue will appear at the top of the issue explorer. You will be able to remarket this bounty 1 more time if a contributor does not pick this up."
assert bounty.remarketed_count == 1

bounty.last_remarketed = timezone.now() - timezone.timedelta(hours=2)

result = re_market_bounty(bounty, False)
assert result['success'] is True
assert result['msg'] == "The issue will appear at the top of the issue explorer. Please note this is the last time the issue is able to be remarketed."
assert bounty.remarketed_count == 2

@staticmethod
def test_re_market_fails_after_reaching_re_market_limit():
if settings.RE_MARKET_LIMIT == 2:
bounty = Bounty.objects.create(
title='CanRemarketFalseTest',
idx_status=0,
is_open=True,
web3_created=datetime(2008, 10, 31, tzinfo=UTC),
expires_date=datetime(2008, 11, 30, tzinfo=UTC),
last_remarketed=datetime(2008, 10, 31, tzinfo=UTC),
github_url='https://github.com/gitcoinco/web/issues/12345678',
raw_data={}
)

assert bounty.remarketed_count == 0
result = re_market_bounty(bounty, False)
assert result['success'] is True
assert bounty.remarketed_count == 1

bounty.last_remarketed = timezone.now() - timezone.timedelta(hours=2)

result = re_market_bounty(bounty, False)
assert result['success'] is True
assert bounty.remarketed_count == 2

result = re_market_bounty(bounty, False)
assert result['success'] is False
assert result['msg'] == "The issue was not remarketed due to reaching the remarket limit (2)."

@staticmethod
def test_re_market_fails_after_re_marketing_in_quick_succession():
bounty = Bounty.objects.create(
title='CanRemarketFalseTest',
idx_status=0,
is_open=True,
web3_created=datetime(2008, 10, 31, tzinfo=UTC),
expires_date=datetime(2008, 11, 30, tzinfo=UTC),
github_url='https://github.com/gitcoinco/web/issues/12345678',
raw_data={}
)

assert bounty.remarketed_count == 0
result = re_market_bounty(bounty, False)
assert result['success'] is True
assert bounty.remarketed_count == 1

result = re_market_bounty(bounty, False)
assert result['success'] is False

assert result['msg'] == f'As you recently remarketed this issue, you need to wait {settings.MINUTES_BETWEEN_RE_MARKETING} minutes before remarketing this issue again.'

@staticmethod
def test_apply_new_bounty_deadline_is_successful_with_re_market():
bounty = Bounty.objects.create(
title='CanRemarketFalseTest',
idx_status=0,
is_open=True,
web3_created=datetime(2008, 10, 31, tzinfo=UTC),
expires_date=datetime(2008, 11, 30, tzinfo=UTC),
github_url='https://github.com/gitcoinco/web/issues/12345678',
raw_data={}
)

assert bounty.remarketed_count == 0

import time
deadline = time.time()
re_market_result = apply_new_bounty_deadline(bounty, deadline, False)
assert bounty.remarketed_count == 1
assert re_market_result['success'] is True
assert re_market_result['msg'] == "You've extended expiration of this issue. The issue will appear at the top of the issue explorer. You will be able to remarket this bounty 1 more time if a contributor does not pick this up."

deadline_as_date_time = timezone.make_aware(
timezone.datetime.fromtimestamp(deadline),
timezone=UTC
)
assert bounty.expires_date == deadline_as_date_time
58 changes: 58 additions & 0 deletions app/dashboard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from json.decoder import JSONDecodeError

from django.conf import settings
from django.utils import timezone

import ipfshttpclient
import requests
Expand All @@ -33,10 +34,13 @@
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 ipfshttpclient.exceptions import CommunicationError
from pytz import UTC
from web3 import HTTPProvider, Web3, WebsocketProvider
from web3.exceptions import BadFunctionCallOutput
from web3.middleware import geth_poa_middleware

from .notifications import maybe_market_to_slack

logger = logging.getLogger(__name__)

SEMAPHORE_BOUNTY_SALT = '1'
Expand Down Expand Up @@ -824,3 +828,57 @@ def get_nonce(network, address):
JSONStore.objects.create(key=key, view=view, data=[new_nonce])

return new_nonce


def re_market_bounty(bounty, auto_save = True):
remarketed_count = bounty.remarketed_count
if remarketed_count < settings.RE_MARKET_LIMIT:
now = timezone.now()
minimum_wait_after_remarketing = bounty.last_remarketed + timezone.timedelta(minutes=settings.MINUTES_BETWEEN_RE_MARKETING) if bounty.last_remarketed else now
if now >= minimum_wait_after_remarketing:
bounty.remarketed_count = remarketed_count + 1
bounty.last_remarketed = now

if auto_save:
bounty.save()

maybe_market_to_slack(bounty, 'issue_remarketed')

result_msg = 'The issue will appear at the top of the issue explorer. '
further_permitted_remarket_count = settings.RE_MARKET_LIMIT - bounty.remarketed_count
if further_permitted_remarket_count >= 1:
result_msg += f'You will be able to remarket this bounty {further_permitted_remarket_count} more time if a contributor does not pick this up.'
elif further_permitted_remarket_count == 0:
result_msg += 'Please note this is the last time the issue is able to be remarketed.'

return {
"success": True,
"msg": result_msg
}
else:
time_delta_wait_time = minimum_wait_after_remarketing - now
minutes_to_wait = round(time_delta_wait_time.total_seconds() / 60)
return {
"success": False,
"msg": f'As you recently remarketed this issue, you need to wait {minutes_to_wait} minutes before remarketing this issue again.'
}
else:
return {
"success": False,
"msg": f'The issue was not remarketed due to reaching the remarket limit ({settings.RE_MARKET_LIMIT}).'
}


def apply_new_bounty_deadline(bounty, deadline, auto_save = True):
bounty.expires_date = timezone.make_aware(
timezone.datetime.fromtimestamp(deadline),
timezone=UTC)
result = re_market_bounty(bounty, False)

if auto_save:
bounty.save()

base_result_msg = "You've extended expiration of this issue."
result['msg'] = base_result_msg + " " + result['msg']

return result
43 changes: 9 additions & 34 deletions app/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@
maybe_market_to_user_slack,
)
from .utils import (
get_bounty, get_bounty_id, get_context, get_unrated_bounties_count, get_web3, has_tx_mined,
record_user_action_on_interest, web3_process_bounty,
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,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -551,16 +551,13 @@ def extend_expiration(request, bounty_id):
is_funder = bounty.is_funder(user.username.lower()) if user else False
if is_funder:
deadline = round(int(request.POST.get('deadline')))
bounty.expires_date = timezone.make_aware(
timezone.datetime.fromtimestamp(deadline),
timezone=UTC)
bounty.save()
result = apply_new_bounty_deadline(bounty, deadline)
record_user_action(request.user, 'extend_expiration', bounty)
record_bounty_activity(bounty, request.user, 'extend_expiration')

return JsonResponse({
'success': True,
'msg': _("You've extended expiration of this issue."),
'msg': _(result['msg']),
})

return JsonResponse({
Expand Down Expand Up @@ -1575,34 +1572,12 @@ def helper_handle_remarket_trigger(request, bounty):
is_staff = request.user.is_staff
is_funder = bounty.is_funder(request.user.username.lower())
if is_staff or is_funder:
remarketed_count = bounty.remarketed_count
if remarketed_count < 2:
now = timezone.now()
one_hour_after_remarketing = bounty.last_remarketed + timezone.timedelta(hours=1) if bounty.last_remarketed else now
if now >= one_hour_after_remarketing:
bounty.remarketed_count = remarketed_count + 1
bounty.last_remarketed = now
bounty.save()

maybe_market_to_slack(bounty, 'issue_remarketed')

further_permitted_remarket_count = 2 - bounty.remarketed_count
success_msg = "This issue has been remarketed. The issue will appear at the top of the issue explorer. "
if further_permitted_remarket_count == 1:
success_msg = success_msg + "You will be able to remarket this bounty one more time if a contributor does not pick this up."
elif further_permitted_remarket_count == 0:
success_msg = success_msg + "Please note this is the last time the issue is able to be remarketed."

messages.success(request, _(success_msg))

else:
time_delta_wait_time = one_hour_after_remarketing - now
minutes_to_wait = round(time_delta_wait_time.total_seconds() / 60)
messages.warning(request,
_(f'You need to wait {minutes_to_wait} minutes before remarketing this bounty again'))
result = re_market_bounty(bounty)
if result['success']:
base_result_msg = "This issue has been remarketed."
messages.success(request, _(base_result_msg + " " + result['msg']))
else:
messages.warning(request,
_('You cannot remarket this bounty again due to reaching the remarket limit (2)'))
messages.warning(request, _(result['msg']))
else:
messages.warning(request, _('Only staff or the funder of this bounty may do this.'))

Expand Down