prettify refund page (bug 704961, bug 705871)
This commit is contained in:
Родитель
68d6c10475
Коммит
a02cb78d3e
|
@ -1,36 +1,49 @@
|
||||||
{% extends "devhub/base.html" %}
|
{% extends 'devhub/base_impala.html' %}
|
||||||
|
|
||||||
{% block title %}{{ dev_page_title(_('Issue Refund')) }}{% endblock %}
|
{% set title = _('Issue Refund') %}
|
||||||
|
{% block title %}{{ dev_page_title(title) }}{% endblock %}
|
||||||
|
|
||||||
|
{# TODO(apps): Finalize copy. #}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<header>
|
<header>
|
||||||
<h1>{{ _('Issue Refund') }}</h1>
|
{{ dev_breadcrumbs(addon) }}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
</header>
|
</header>
|
||||||
<form method="post" id="issue-refund" class="item" action="">
|
<form method="post" action="" id="issue-refund" class="primary island full c">
|
||||||
{% if transaction_id %}
|
{% if transaction_id %}
|
||||||
{{ csrf() }}
|
{{ csrf() }}
|
||||||
<p>
|
<p>
|
||||||
{% trans %}
|
{% with user=contribution.user.display_name,
|
||||||
A refund was requested by {{ user }} for {{ addon_name }}.
|
user_url=contribution.user.get_url_path(),
|
||||||
{% endtrans %}
|
addon_url=addon.get_url_path(),
|
||||||
</p><p>
|
addon_name=addon.name %}
|
||||||
{% trans %}
|
A refund was requested by
|
||||||
Price: {{ price }}
|
<a href="{{ user_url }}" target="_blank">{{ user }}</a> for
|
||||||
{% endtrans %}
|
<a href="{{ addon_url }}" target="_blank">{{ addon_name }}</a>.
|
||||||
</p><p>
|
{% endwith %}
|
||||||
{% trans %}
|
</p>
|
||||||
Purchase date: {{ purchase_date }}
|
<p>
|
||||||
{% endtrans %}
|
{% with price=contribution.get_amount_locale() %}
|
||||||
</p><p>
|
<b>Price:</b> {{ price }}
|
||||||
<button type="submit" name="issue">
|
{% endwith %}
|
||||||
{{ _('Issue Refund') }}
|
</p>
|
||||||
</button>
|
<p>
|
||||||
<button type="submit" name="decline">
|
{% with purchase_date=contribution.created|datetime %}
|
||||||
{{ _('Decline Refund') }}
|
<b>Purchase date:</b> {{ purchase_date }}
|
||||||
</button>
|
{% endwith %}
|
||||||
<input type="hidden" name="transaction_id" value="{{ transaction_id }}">
|
</p>
|
||||||
{% else %}
|
<p>
|
||||||
<p>{{ _('No refundable transaction found.') }}</p>
|
<button type="submit" class="good" name="issue">
|
||||||
{% endif %}
|
{{ _('Issue Refund') }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="bad" name="decline">
|
||||||
|
{{ _('Decline Refund') }}
|
||||||
|
</button>
|
||||||
|
<input type="hidden" name="transaction_id" value="{{ transaction_id }}">
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ loc('No refundable transaction found.') }}</p>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock %}
|
||||||
|
|
|
@ -1260,60 +1260,71 @@ class TestIssueRefund(amo.tests.TestCase):
|
||||||
self.paykey = u'fake-paykey'
|
self.paykey = u'fake-paykey'
|
||||||
self.client.login(username='del@icio.us', password='password')
|
self.client.login(username='del@icio.us', password='password')
|
||||||
self.user = UserProfile.objects.get(username='clouserw')
|
self.user = UserProfile.objects.get(username='clouserw')
|
||||||
self.url = reverse('devhub.issue_refund', args=[self.addon.slug])
|
self.url = self.addon.get_dev_url('issue_refund')
|
||||||
|
|
||||||
def makePurchase(self, uuid='123456', type=amo.CONTRIB_PURCHASE):
|
def make_purchase(self, uuid='123456', type=amo.CONTRIB_PURCHASE):
|
||||||
return Contribution.objects.create(uuid=uuid, addon=self.addon,
|
return Contribution.objects.create(uuid=uuid, addon=self.addon,
|
||||||
transaction_id=self.transaction_id,
|
transaction_id=self.transaction_id,
|
||||||
user=self.user, paykey=self.paykey,
|
user=self.user, paykey=self.paykey,
|
||||||
amount=Decimal('10'), type=type)
|
amount=Decimal('10'), type=type)
|
||||||
|
|
||||||
def test_request_issue(self):
|
def test_request_issue(self):
|
||||||
c = self.makePurchase()
|
c = self.make_purchase()
|
||||||
r = self.client.get(self.url,
|
r = self.client.get(self.url, {'transaction_id': c.transaction_id})
|
||||||
data={'transaction_id': c.transaction_id})
|
|
||||||
doc = pq(r.content)
|
doc = pq(r.content)
|
||||||
eq_(doc('#issue-refund button')[0].text.strip(), 'Issue Refund')
|
eq_(doc('#issue-refund button').length, 2)
|
||||||
eq_(doc('#issue-refund button')[1].text.strip(), 'Decline Refund')
|
|
||||||
eq_(doc('#issue-refund input[name=transaction_id]').val(),
|
eq_(doc('#issue-refund input[name=transaction_id]').val(),
|
||||||
self.transaction_id)
|
self.transaction_id)
|
||||||
|
|
||||||
def test_nonexistent_txn(self):
|
def test_nonexistent_txn(self):
|
||||||
r = self.client.get(self.url, data={'transaction_id': 'none'})
|
r = self.client.get(self.url, {'transaction_id': 'none'})
|
||||||
eq_(r.status_code, 404)
|
eq_(r.status_code, 404)
|
||||||
|
|
||||||
def test_nonexistent_txn_no_really(self):
|
def test_nonexistent_txn_no_really(self):
|
||||||
r = self.client.get(self.url)
|
r = self.client.get(self.url)
|
||||||
eq_(r.status_code, 404)
|
eq_(r.status_code, 404)
|
||||||
|
|
||||||
@mock.patch('paypal.refund')
|
def _test_issue(self, refund, destination):
|
||||||
def test_issue(self, refund):
|
c = self.make_purchase()
|
||||||
c = self.makePurchase()
|
r = self.client.post(self.url, {'transaction_id': c.transaction_id,
|
||||||
r = self.client.post(self.url,
|
'issue': '1'})
|
||||||
data={'transaction_id': c.transaction_id,
|
self.assertRedirects(r, reverse(destination), 302)
|
||||||
'issue': '1'})
|
|
||||||
eq_(r.status_code, 302)
|
|
||||||
refund.assert_called_with(self.transaction_id, self.paykey)
|
refund.assert_called_with(self.transaction_id, self.paykey)
|
||||||
eq_(len(mail.outbox), 1)
|
eq_(len(mail.outbox), 1)
|
||||||
assert 'approved' in mail.outbox[0].subject
|
assert 'approved' in mail.outbox[0].subject
|
||||||
|
|
||||||
@mock.patch('paypal.refund')
|
@mock.patch('paypal.refund')
|
||||||
def test_decline(self, refund):
|
def test_addons_issue(self, refund):
|
||||||
c = self.makePurchase()
|
self._test_issue(refund, 'devhub.addons')
|
||||||
r = self.client.post(self.url,
|
|
||||||
data={'transaction_id': c.transaction_id,
|
@mock.patch('paypal.refund')
|
||||||
'decline': ''})
|
def test_apps_issue(self, refund):
|
||||||
eq_(r.status_code, 302)
|
self.addon.update(type=amo.ADDON_WEBAPP)
|
||||||
|
self._test_issue(refund, 'devhub.apps')
|
||||||
|
|
||||||
|
def _test_decline(self, refund, destination):
|
||||||
|
c = self.make_purchase()
|
||||||
|
r = self.client.post(self.url, {'transaction_id': c.transaction_id,
|
||||||
|
'decline': ''})
|
||||||
|
self.assertRedirects(r, reverse(destination), 302)
|
||||||
assert not refund.called
|
assert not refund.called
|
||||||
eq_(len(mail.outbox), 1)
|
eq_(len(mail.outbox), 1)
|
||||||
assert 'declined' in mail.outbox[0].subject
|
assert 'declined' in mail.outbox[0].subject
|
||||||
|
|
||||||
|
@mock.patch('paypal.refund')
|
||||||
|
def test_addons_decline(self, refund):
|
||||||
|
self._test_decline(refund, 'devhub.addons')
|
||||||
|
|
||||||
|
@mock.patch('paypal.refund')
|
||||||
|
def test_apps_decline(self, refund):
|
||||||
|
self.addon.update(type=amo.ADDON_WEBAPP)
|
||||||
|
self._test_decline(refund, 'devhub.apps')
|
||||||
|
|
||||||
@mock.patch('paypal.refund')
|
@mock.patch('paypal.refund')
|
||||||
def test_non_refundable_txn(self, refund):
|
def test_non_refundable_txn(self, refund):
|
||||||
c = self.makePurchase('56789', amo.CONTRIB_VOLUNTARY)
|
c = self.make_purchase('56789', amo.CONTRIB_VOLUNTARY)
|
||||||
r = self.client.post(self.url,
|
r = self.client.post(self.url, {'transaction_id': c.transaction_id,
|
||||||
data={'transaction_id': c.transaction_id,
|
'issue': ''})
|
||||||
'issue': ''})
|
|
||||||
eq_(r.status_code, 404)
|
eq_(r.status_code, 404)
|
||||||
assert not refund.called
|
assert not refund.called
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ app_detail_patterns = patterns('',
|
||||||
url('^profile$', views.profile, name='devhub.apps.profile'),
|
url('^profile$', views.profile, name='devhub.apps.profile'),
|
||||||
url('^profile/remove$', views.remove_profile,
|
url('^profile/remove$', views.remove_profile,
|
||||||
name='devhub.apps.profile.remove'),
|
name='devhub.apps.profile.remove'),
|
||||||
|
url('^issue_refund$', views.issue_refund, name='devhub.apps.issue_refund'),
|
||||||
)
|
)
|
||||||
|
|
||||||
# These will all start with /addon/<addon_id>/
|
# These will all start with /addon/<addon_id>/
|
||||||
|
@ -81,7 +82,8 @@ detail_patterns = patterns('',
|
||||||
url('^payments/permission/refund$', views.acquire_refund_permission,
|
url('^payments/permission/refund$', views.acquire_refund_permission,
|
||||||
name='devhub.addons.acquire_refund_permission'),
|
name='devhub.addons.acquire_refund_permission'),
|
||||||
url('^payments/', include(marketplace_patterns('addons'))),
|
url('^payments/', include(marketplace_patterns('addons'))),
|
||||||
url('^issue_refund$', views.issue_refund, name='devhub.issue_refund'),
|
url('^issue_refund$', views.issue_refund,
|
||||||
|
name='devhub.addons.issue_refund'),
|
||||||
url('^profile$', views.profile, name='devhub.addons.profile'),
|
url('^profile$', views.profile, name='devhub.addons.profile'),
|
||||||
url('^profile/remove$', views.remove_profile,
|
url('^profile/remove$', views.remove_profile,
|
||||||
name='devhub.addons.profile.remove'),
|
name='devhub.addons.profile.remove'),
|
||||||
|
|
|
@ -492,23 +492,19 @@ def issue_refund(request, addon_id, addon, webapp=False):
|
||||||
if 'issue' in request.POST:
|
if 'issue' in request.POST:
|
||||||
paypal.refund(txn_id, contribution.paykey)
|
paypal.refund(txn_id, contribution.paykey)
|
||||||
contribution.mail_approved()
|
contribution.mail_approved()
|
||||||
paypal_log.error('Refund issued for transaction %r' % (txn_id,))
|
paypal_log.error('Refund issued for transaction %r' % txn_id)
|
||||||
messages.success(request, 'Refund issued.')
|
messages.success(request, 'Refund issued.')
|
||||||
return redirect('devhub.addons')
|
|
||||||
else:
|
else:
|
||||||
contribution.mail_declined()
|
contribution.mail_declined()
|
||||||
paypal_log.error('Refund declined for transaction %r' % (txn_id,))
|
paypal_log.error('Refund declined for transaction %r' % txn_id)
|
||||||
messages.success(request, 'Refund declined.')
|
messages.success(request, 'Refund declined.')
|
||||||
return redirect('devhub.addons')
|
return redirect('devhub.%s' % ('apps' if webapp else 'addons'))
|
||||||
else:
|
else:
|
||||||
return jingo.render(request, 'devhub/payments/issue-refund.html',
|
return jingo.render(request, 'devhub/payments/issue-refund.html',
|
||||||
{'refund_issued': False,
|
{'contribution': contribution,
|
||||||
'user': contribution.user.display_name,
|
'addon': addon,
|
||||||
'addon_name': addon.name,
|
|
||||||
'webapp': webapp,
|
'webapp': webapp,
|
||||||
'price': contribution.amount,
|
'transaction_id': txn_id})
|
||||||
'transaction_id': txn_id,
|
|
||||||
'purchase_date': contribution.created})
|
|
||||||
|
|
||||||
|
|
||||||
@dev_required
|
@dev_required
|
||||||
|
@ -1028,7 +1024,6 @@ def addons_section(request, addon_id, addon, section, editable=False,
|
||||||
else:
|
else:
|
||||||
form = False
|
form = False
|
||||||
|
|
||||||
#import pdb; pdb.set_trace()
|
|
||||||
data = {'addon': addon,
|
data = {'addon': addon,
|
||||||
'webapp': webapp,
|
'webapp': webapp,
|
||||||
'form': form,
|
'form': form,
|
||||||
|
|
|
@ -467,7 +467,6 @@ def profile(request, user_id):
|
||||||
.filter(following__user=user)
|
.filter(following__user=user)
|
||||||
.order_by('-following__created'))[:10]
|
.order_by('-following__created'))[:10]
|
||||||
|
|
||||||
|
|
||||||
edit_any_user = acl.action_allowed(request, 'Admin', 'EditAnyUser')
|
edit_any_user = acl.action_allowed(request, 'Admin', 'EditAnyUser')
|
||||||
own_profile = (request.user.is_authenticated() and
|
own_profile = (request.user.is_authenticated() and
|
||||||
request.amo_user.id == user.id)
|
request.amo_user.id == user.id)
|
||||||
|
@ -760,11 +759,8 @@ def support_mozilla(request, contribution, wizard):
|
||||||
def refund_request(request, contribution, wizard):
|
def refund_request(request, contribution, wizard):
|
||||||
addon = contribution.addon
|
addon = contribution.addon
|
||||||
form = forms.RemoveForm(request.POST or None)
|
form = forms.RemoveForm(request.POST or None)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST' and form.is_valid():
|
||||||
if form.is_valid():
|
return redirect('users.support', contribution.pk, 'reason')
|
||||||
return redirect(reverse('users.support',
|
|
||||||
args=[contribution.pk, 'reason']))
|
|
||||||
|
|
||||||
return wizard.render(request, wizard.tpl('request.html'),
|
return wizard.render(request, wizard.tpl('request.html'),
|
||||||
{'addon': addon, 'webapp': addon.is_webapp(),
|
{'addon': addon, 'webapp': addon.is_webapp(),
|
||||||
'form': form, 'contribution': contribution})
|
'form': form, 'contribution': contribution})
|
||||||
|
@ -773,35 +769,30 @@ def refund_request(request, contribution, wizard):
|
||||||
def refund_reason(request, contribution, wizard):
|
def refund_reason(request, contribution, wizard):
|
||||||
addon = contribution.addon
|
addon = contribution.addon
|
||||||
if not 'request' in wizard.get_progress():
|
if not 'request' in wizard.get_progress():
|
||||||
return redirect(reverse('users.support',
|
return redirect('users.support', contribution.pk, 'request')
|
||||||
args=[contribution.pk, 'request']))
|
|
||||||
|
|
||||||
form = forms.ContactForm(request.POST or None)
|
form = forms.ContactForm(request.POST or None)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST' and form.is_valid():
|
||||||
if form.is_valid():
|
# TODO(ashort): Reject refund if purchase was more than 30 minutes ago.
|
||||||
# if under 30 minutes, refund
|
url = absolutify(urlparams(addon.get_dev_url('issue_refund'),
|
||||||
# TODO(ashort): add in the logic for under 30 minutes
|
transaction_id=contribution.transaction_id))
|
||||||
refund_url = absolutify(urlparams(
|
template = jingo.render_to_string(request,
|
||||||
reverse('devhub.issue_refund', args=[addon.slug]),
|
wizard.tpl('emails/refund-request.txt'),
|
||||||
transaction_id=contribution.transaction_id))
|
context={'addon': addon,
|
||||||
|
'form': form,
|
||||||
|
'user': request.amo_user,
|
||||||
|
'contribution': contribution,
|
||||||
|
'refund_url': url})
|
||||||
|
log.info('Refund request sent by user: %s for addon: %s' %
|
||||||
|
(request.amo_user.pk, addon.pk))
|
||||||
|
# L10n: %s is the addon name.
|
||||||
|
send_mail(_(u'New Refund Request for %s' % addon.name),
|
||||||
|
template, request.amo_user.email,
|
||||||
|
[smart_str(addon.support_email)])
|
||||||
|
return redirect(reverse('users.support',
|
||||||
|
args=[contribution.pk, 'refund-sent']))
|
||||||
|
|
||||||
template = jingo.render_to_string(request,
|
return wizard.render(request, wizard.tpl('refund.html'), {'form': form})
|
||||||
wizard.tpl('emails/refund-request.txt'),
|
|
||||||
context={'addon': addon, 'form': form,
|
|
||||||
'user': request.amo_user,
|
|
||||||
'contribution': contribution,
|
|
||||||
'refund_url': refund_url})
|
|
||||||
log.info('Refund request sent by user: %s for addon: %s' %
|
|
||||||
(request.amo_user.pk, addon.pk))
|
|
||||||
# L10n: %s is the addon name.
|
|
||||||
send_mail(_(u'New Refund Request for %s' % addon.name),
|
|
||||||
template, request.amo_user.email,
|
|
||||||
[smart_str(addon.support_email)])
|
|
||||||
return redirect(reverse('users.support',
|
|
||||||
args=[contribution.pk, 'refund-sent']))
|
|
||||||
|
|
||||||
return wizard.render(request, wizard.tpl('refund.html'),
|
|
||||||
{'contribut': addon, 'form': form})
|
|
||||||
|
|
||||||
|
|
||||||
class SupportWizard(Wizard):
|
class SupportWizard(Wizard):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import 'lib';
|
@import '../impala/lib';
|
||||||
|
|
||||||
.devhub-form .tip,
|
.devhub-form .tip,
|
||||||
.addon-submission-process .tip,
|
.addon-submission-process .tip,
|
||||||
|
@ -92,3 +92,7 @@ a.remove:hover {
|
||||||
.html-rtl .undo {
|
.html-rtl .undo {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#issue-refund {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
|
@ -15,6 +15,17 @@
|
||||||
.box-shadow(0 1px rgba(0, 0, 0, 0.1), 0 -2px rgba(0, 0, 0, 0.1) inset);
|
.box-shadow(0 1px rgba(0, 0, 0, 0.1), 0 -2px rgba(0, 0, 0, 0.1) inset);
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.good, .button.add { // Green
|
||||||
|
background: #489615;
|
||||||
|
.gradient-two-color(#84C63C, #489615);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.bad, .button.developer, .button.scary { // Red
|
||||||
|
background: #bc2b1a;
|
||||||
|
.gradient-two-color(#f84b4e, #bc2b1a);
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
&.prominent {
|
&.prominent {
|
||||||
|
@ -23,9 +34,6 @@
|
||||||
.box-shadow(0 3px rgba(0, 0, 0, 0.1), 0 -4px rgba(0, 0, 0, 0.1) inset);
|
.box-shadow(0 3px rgba(0, 0, 0, 0.1), 0 -4px rgba(0, 0, 0, 0.1) inset);
|
||||||
}
|
}
|
||||||
&.add { // Green
|
&.add { // Green
|
||||||
background: #489615;
|
|
||||||
.gradient-two-color(#84C63C, #489615);
|
|
||||||
color: #fff;
|
|
||||||
span {
|
span {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
background: url(../../img/impala/button-icons.png) no-repeat 0 3px;
|
background: url(../../img/impala/button-icons.png) no-repeat 0 3px;
|
||||||
|
@ -93,9 +101,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.developer, &.scary { // Red
|
&.developer, &.scary { // Red
|
||||||
background: #bc2b1a;
|
|
||||||
.gradient-two-color(#f84b4e, #bc2b1a);
|
|
||||||
color: #fff;
|
|
||||||
span {
|
span {
|
||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
|
@ -106,7 +111,6 @@
|
||||||
&.watch:not(.watching) { // Orange
|
&.watch:not(.watching) { // Orange
|
||||||
background: #ea0;
|
background: #ea0;
|
||||||
.gradient-two-color(#ea0, darken(#ea0, 10%));
|
.gradient-two-color(#ea0, darken(#ea0, 10%));
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
&.platform {
|
&.platform {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -32,6 +32,11 @@ p.req {
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CSRF token */
|
||||||
|
form div[style]:first-child + p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.optional {
|
.optional {
|
||||||
color: @note-gray;
|
color: @note-gray;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
@ -380,4 +385,3 @@ button.loading-submit:after {
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -506,7 +506,7 @@ MINIFY_BUNDLES = {
|
||||||
'css/impala/devhub-popups.less',
|
'css/impala/devhub-popups.less',
|
||||||
'css/impala/devhub-compat.less',
|
'css/impala/devhub-compat.less',
|
||||||
'css/impala/formset.less',
|
'css/impala/formset.less',
|
||||||
'css/impala/devhub-forms.less',
|
'css/devhub/forms.less',
|
||||||
),
|
),
|
||||||
'zamboni/devhub_impala': (
|
'zamboni/devhub_impala': (
|
||||||
'css/impala/developers.less',
|
'css/impala/developers.less',
|
||||||
|
@ -514,7 +514,7 @@ MINIFY_BUNDLES = {
|
||||||
'css/impala/devhub-popups.less',
|
'css/impala/devhub-popups.less',
|
||||||
'css/impala/devhub-compat.less',
|
'css/impala/devhub-compat.less',
|
||||||
'css/impala/devhub-dashboard.less',
|
'css/impala/devhub-dashboard.less',
|
||||||
'css/impala/devhub-forms.less',
|
'css/devhub/forms.less',
|
||||||
),
|
),
|
||||||
'zamboni/editors': (
|
'zamboni/editors': (
|
||||||
'css/zamboni/editors.css',
|
'css/zamboni/editors.css',
|
||||||
|
|
Загрузка…
Ссылка в новой задаче