the support pages (bug 686808)

This commit is contained in:
Andy McKay 2011-09-16 13:04:22 -07:00
Родитель 613ea4a983
Коммит 11e4158a2e
19 изменённых файлов: 391 добавлений и 25 удалений

Просмотреть файл

@ -147,7 +147,8 @@ def create_addon_purchase(sender, instance, **kw):
if instance.type == amo.CONTRIB_PURCHASE:
log.debug('Creating addon purchase: addon %s, user %s'
% (instance.addon.pk, instance.user.pk))
AddonPurchase.objects.create(addon=instance.addon, user=instance.user)
AddonPurchase.objects.get_or_create(addon=instance.addon,
user=instance.user)
elif instance.type in [amo.CONTRIB_REFUND, amo.CONTRIB_CHARGEBACK]:
purchases = AddonPurchase.objects.filter(addon=instance.addon,

Просмотреть файл

@ -408,3 +408,13 @@ class BlacklistedEmailDomainAddForm(forms.Form):
self._errors['domains'] = ErrorList([msg])
return data
class ContactForm(happyforms.Form):
text = forms.CharField(widget=forms.Textarea(), required=True)
class RemoveForm(happyforms.Form):
remove = forms.BooleanField(required=True,
label=_('I have removed this add-on from '
'all of my devices.'))

Просмотреть файл

@ -15,7 +15,8 @@
{{ impala_addon_listing_header(url_base, filter.opts, sorting, filter.extras) }}
{% endif %}
<div class="items">
{% for addon in purchases.object_list %}
{% for contribution in purchases.object_list %}
{% with addon=contribution.addon %}
<div class="item addon ignore-compatibility">
<div class="info">
<h3>
@ -24,12 +25,13 @@
</h3>
<p class="desc">{{ addon.description|truncate(250)|nl2br }}</p>
<div class="vitals c">
{% trans date=addon.created, amt=prices[addon.pk][1], url='' %}
{% trans date=addon.created, amt=contribution.get_amount_locale(), url=url('users.support', contribution.pk) %}
<div>Purchased {{ date }} for {{ amt }} - <a href="{{ url }}">Request Support</a></div>
{% endtrans %}
</div>
</div>
</div>
{% endwith %}
{% else %}
<p>You have no purchases.</p>
{% endfor %}

Просмотреть файл

@ -0,0 +1,3 @@
<h1>{{ _('Support Request Sent') }}</h1>
<p>{% trans %}You support request has been sent.{% endtrans %}</p>
<p><a href="{{ url('users.purchases') }}" class="button">Close</a></p>

Просмотреть файл

@ -0,0 +1,13 @@
<h1>{{ _('Contact the Author') }}</h1>
<form method="POST">
{{ csrf() }}
<p>{% trans %}For support questions regarding this add-on's functionality,
please contact the author using the form below.{% endtrans %}</p>
<p><strong>{% trans name=addon.name %}To: {{ name }}{% endtrans %}</strong></p>
<p><strong>{% trans %}Please describe the issue:{% endtrans %}</strong></p>
{{ form.text.errors }}
{{ form.text }}
<p>{% trans %}Your email address will be made available to the
add-on author for replies.{% endtrans %}</p>
<button type="submit">{{ _('Send Request') }}</button>
</form>

Просмотреть файл

@ -0,0 +1,9 @@
Your add-on, {{ addon.name }}, has received a new refund request:
User: {{ user.display_name or user.username }}
Purchased on {{ contribution.date|datetime }}
Price: {{ contribution.get_amount_locale() }}
Reason given:
{{ form.cleaned_data.text }}
To approve or deny this request, click here: ...

Просмотреть файл

@ -0,0 +1,12 @@
Add-on, {{ addon.name }}, has received a new support request:
User: {{ user.display_name or user.username }}
Email: {{ user.email }}
Purchased on {{ contribution.date|datetime }}
Details:
{{ form.cleaned_data.text }}
{{ request.HTTP_USER_AGENT }}
Please reply to this email to respond to the users request.

Просмотреть файл

@ -0,0 +1,14 @@
<h1>{{ _('Contact Mozilla') }}</h1>
<form method="POST">
{{ csrf() }}
<p>{% trans %}Please use this form <strong>only for issues with the Mozilla
Marketplace</strong>. For other suport and question, please visit
<a href="{{ sumo }}">support.mozilla.com</a>. We are unable to respond
to misdirected support requests through this form
{% endtrans %}</p>
<p><strong>{% trans %}To: Mozilla Marketplace Support{% endtrans %}</strong></p>
<p><strong>{% trans %}Please describe the issue:{% endtrans %}</strong></p>
{{ form.text.errors }}
{{ form.text }}
<button type="submit">{{ _('Send Request') }}</button>
</form>

Просмотреть файл

@ -0,0 +1,5 @@
<h1>{{ _('Request Refund') }}</h1>
<p>{% trans name=addon.name %}Your request for a refund of {{ name }} has been
submitted. You will receive an email when the author of this add-on has
responded.{% endtrans %}</p>
<p><a href="{{ url('users.purchases') }}" class="button">Close</a></p>

Просмотреть файл

@ -0,0 +1,8 @@
<h1>{{ _('Request Refund') }}</h1>
<form method="POST">
{{ csrf() }}
<p>{% trans %} Please tell us why you are requesting a refund.{% endtrans %}</p>
{{ form.text.errors }}
{{ form.text }}
<button type="submit">{{ _('Request Refund') }}</button>
</form>

Просмотреть файл

@ -0,0 +1,17 @@
<h1>{{ _('Request Refund') }}</h1>
<form method="POST">
{{ csrf() }}
<p>{% trans url=url('users.support', contribution.pk, 'author') %}
If you are unsatisfied with your purchase, you may request
a refund from the developer for up to 60 days. Requests within 30 minutes
after purchase are automatically granted. If you are having difficulty
using the add-on, we encourage you to <a href="{{ url }}">ask the developer for
support</a> before requesting a refund.{% endtrans %}</p>
<p>{% trans name=addon.name %}In order to continue with the refund
process, <b>you must completely
remove the add-on from all of your devices</b>. Open Add-ons Manager,
select Remove next to {{ name }} and restart Firefox.{% endtrans %}</p>
{{ form.remove.errors }}
<p>{{ form.remove }} {{ form.remove.label }}</p>
<button type="submit">{{ _('Continue') }}</button>
</form>

Просмотреть файл

@ -0,0 +1,8 @@
<h1>{{ _('Helpful Resources') }}</h1>
<p>{% trans %}Please use the articles below to find answers to some of the
most common questions and issues encountered in the Marketplace.
{% endtrans %}</p>
<p>{% trans url=url('users.support', contribution.pk, 'mozilla') %}
If you looked through the above resources and still have a
question, please <a href="{{ url }}">contact us</a>
{% endtrans %}.</p>

Просмотреть файл

@ -0,0 +1,10 @@
<h1>{{ _('Add-on Support') }}</h1>
<p>{% trans %}For support questions regarding this add-on's functionality,
please visit the add-on author's support website to search for
a solution:{% endtrans %}</p>
<h2><a href="{{ addon.support_url }}">{{ addon.name }}</a></h2>
<p>{% trans url=url('users.support', contribution.pk, 'author') %}
If you are unable to find the answer to your question at the
above support website, please <a href="{{ url }}">contact the author</a>.
{% endtrans %}</p>

Просмотреть файл

@ -0,0 +1,5 @@
<h1>{{ _('What do you need help with?') }}</h1>
<p><a href="{% with step='site' if addon.support_url else 'author' %}{{ url('users.support', contribution.pk, step) }}{% endwith %}">{{ _("This add-on isn't working as expected &raquo;") }}</a></p>
<p><a href="{{ url('users.support', contribution.pk, 'resources') }}">{{ _("I can't install my purchase &raquo;") }}</a></p>
<p><a href="{{ url('users.support', contribution.pk, 'resources') }}">{{ _('I have billing or payment concerns &raquo;') }}</a></p>
<p><a href="{{ url('users.support', contribution.pk, 'request') }}">{{ _('I want to request a refund &raquo;') }}</a></p>

Просмотреть файл

@ -0,0 +1,9 @@
{% extends "impala/base.html" %}
{% block title %}{{ page_title(wizard.title) }}{% endblock %}
{% block content %}
<section class="primary">
{% include content %}
</section>
{% endblock %}

Просмотреть файл

@ -1,3 +1,4 @@
from datetime import datetime, timedelta
import json
from django.conf import settings
@ -704,9 +705,13 @@ class TestPurchases(amo.tests.TestCase):
for x in range(1, 5):
addon = Addon.objects.create(type=amo.ADDON_EXTENSION,
name='t%s' % x)
Contribution.objects.create(user=self.user.get_profile(),
addon=addon, amount='%s.00' % x,
type=amo.CONTRIB_PURCHASE)
con = Contribution.objects.create(user=self.user.get_profile(),
addon=addon, amount='%s.00' % x,
type=amo.CONTRIB_PURCHASE)
con.created = datetime.now() - timedelta(days=10 - x)
con.save()
self.con = con
self.addon = addon
def test_in_menu(self):
@ -734,7 +739,8 @@ class TestPurchases(amo.tests.TestCase):
def get_order(self, order):
res = self.client.get('%s?sort=%s' % (self.url, order))
return [str(a.name) for a in res.context['purchases'].object_list]
return [str(c.addon.name) for c in
res.context['purchases'].object_list]
def test_ordering(self):
eq_(self.get_order('name'), ['t1', 't2', 't3', 't4'])
@ -774,3 +780,112 @@ class TestPurchases(amo.tests.TestCase):
self.user.get_profile().addonpurchase_set.all().delete()
doc = pq(self.client.get(self.url).content)
eq_(doc('body').attr('data-purchases'), '')
def test_refund_link(self):
doc = pq(self.client.get(self.url).content)
url = reverse('users.support', args=[self.con.pk])
eq_(doc('div.vitals a').eq(0).attr('href'), url)
def get_url(self, *args):
return reverse('users.support', args=[self.con.pk] + list(args))
def test_support_not_logged_in(self):
self.client.logout()
eq_(self.client.get(self.get_url()).status_code, 302)
def test_support_not_mine(self):
self.client.login(username='admin@mozilla.com', password='password')
eq_(self.client.get(self.get_url()).status_code, 404)
def test_support_page(self):
doc = pq(self.client.get(self.get_url()).content)
eq_(doc('section.primary a').attr('href'), self.get_url('author'))
def test_support_page_other(self):
self.addon.support_url = 'http://cbc.ca'
self.addon.save()
doc = pq(self.client.get(self.get_url()).content)
eq_(doc('section.primary a').attr('href'), self.get_url('site'))
def test_support_site(self):
self.addon.support_url = 'http://cbc.ca'
self.addon.save()
doc = pq(self.client.get(self.get_url('site')).content)
eq_(doc('section.primary a').attr('href'), self.addon.support_url)
def test_contact(self):
data = {'text': 'Lorem ipsum dolor sit amet, consectetur'}
res = self.client.post(self.get_url('author'), data)
eq_(res.status_code, 302)
def test_contact_mails(self):
self.addon.support_email = 'a@a.com'
self.addon.save()
data = {'text': 'Lorem ipsum dolor sit amet, consectetur'}
self.client.post(self.get_url('author'), data)
eq_(len(mail.outbox), 1)
email = mail.outbox[0]
eq_(email.to, ['a@a.com'])
eq_(email.from_email, 'regular@mozilla.com')
def test_contact_fails(self):
res = self.client.post(self.get_url('author'), {'b': 'c'})
assert 'text' in res.context['form'].errors
def test_contact_mozilla(self):
data = {'text': 'Lorem ipsum dolor sit amet, consectetur'}
res = self.client.post(self.get_url('mozilla'), data)
eq_(res.status_code, 302)
def test_contact_mozilla_mails(self):
data = {'text': 'Lorem ipsum dolor sit amet, consectetur'}
self.client.post(self.get_url('mozilla'), data)
eq_(len(mail.outbox), 1)
email = mail.outbox[0]
eq_(email.to, [settings.FLIGTAR])
eq_(email.from_email, 'regular@mozilla.com')
assert 'Lorem' in email.body
def test_contact_mozilla_fails(self):
res = self.client.post(self.get_url('mozilla'), {'b': 'c'})
assert 'text' in res.context['form'].errors
def test_refund_remove(self):
res = self.client.post(self.get_url('request'), {'remove': 1})
eq_(res.status_code, 302)
def test_refund_remove_fails(self):
res = self.client.post(self.get_url('request'), {})
eq_(res.status_code, 200)
def test_skip_fails(self):
res = self.client.post(self.get_url('reason'), {})
self.assertRedirects(res, self.get_url('request'))
def test_request(self):
self.client.post(self.get_url('request'), {'remove': 1})
res = self.client.post(self.get_url('reason'), {'text': 'something'})
eq_(res.status_code, 302)
def test_request_mails(self):
self.addon.support_email = 'a@a.com'
self.addon.save()
self.client.post(self.get_url('request'), {'remove': 1})
self.client.post(self.get_url('reason'), {'text': 'something'})
eq_(len(mail.outbox), 1)
email = mail.outbox[0]
eq_(email.to, ['a@a.com'])
eq_(email.from_email, 'regular@mozilla.com')
assert '$4.00' in email.body
def test_request_fails(self):
self.addon.support_email = 'a@a.com'
self.addon.save()
self.client.post(self.get_url('request'), {'remove': 1})
res = self.client.post(self.get_url('reason'), {})
eq_(res.status_code, 200)

Просмотреть файл

@ -60,6 +60,9 @@ users_patterns = patterns('',
url(r'purchases/$', views.purchases, name='users.purchases'),
url(r'purchases/%s$' % ADDON_ID, views.purchases,
name='users.purchases.receipt'),
url(r'support/(?P<contribution_id>\d+)(?:/(?P<step>[\w-]+))?$',
views.SupportWizard.as_view(),
name='users.support')
)
urlpatterns = patterns('',

Просмотреть файл

@ -6,12 +6,16 @@ from django.db import IntegrityError
from django.shortcuts import get_object_or_404, redirect
from django.contrib import auth
from django.template import Context, loader
from django.utils.datastructures import SortedDict
from django.views.decorators.cache import never_cache
from django.utils.decorators import method_decorator
from django.utils.encoding import smart_str
from django.utils.http import base36_to_int
from django.contrib.auth.tokens import default_token_generator
import commonware.log
import jingo
from radagast.wizard import Wizard
from ratelimit.decorators import ratelimit
from tower import ugettext as _, ugettext_lazy as _lazy
from session_csrf import anonymous_csrf, anonymous_csrf_exempt
@ -28,10 +32,10 @@ from amo.utils import send_mail
from abuse.models import send_abuse_report
from addons.models import Addon
from addons.views import BaseFilter
from addons.decorators import addon_view
from access import acl
from bandwagon.models import Collection
from stats.models import Contribution
from translations.query import order_by_translation
from users.models import UserNotification
import users.notifications as notifications
@ -612,20 +616,14 @@ class ContributionsFilter(BaseFilter):
('price', _lazy(u'Price')),
('name', _lazy(u'Name')))
def __init__(self, *args, **kw):
self.prices = kw.pop('prices')
super(ContributionsFilter, self).__init__(*args, **kw)
def filter(self, field):
qs = self.base_queryset
if field == 'date':
return qs.order_by('-created')
elif field == 'price':
# TODO(andym): If someone has a large number of purchases,
# this will get expensive.
return sorted(list(qs), key=lambda x: self.prices[x.pk][0])
return qs.order_by('amount')
elif field == 'name':
return order_by_translation(qs, 'name')
return qs.order_by('addon__name')
@login_required
@ -634,21 +632,144 @@ def purchases(request, addon_id=None):
if not waffle.switch_is_active('marketplace'):
raise http.Http404
# TODO(ashort): this is where we'll need to get cunning about refunds.
cs = Contribution.objects.filter(user=request.amo_user,
type=amo.CONTRIB_PURCHASE)
if addon_id:
cs = cs.filter(addon=addon_id)
prices = dict((p.addon_id, (p.amount, p.get_amount_locale())) for p in cs)
if addon_id and not prices:
filter = ContributionsFilter(request, cs, key='sort', default='date')
purchases = amo.utils.paginate(request, filter.qs)
if addon_id and not purchases.object_list:
# User has requested a receipt for an addon they don't have.
raise http.Http404
base = Addon.objects.filter(id__in=prices.keys())
filter = ContributionsFilter(request, base, key='sort',
default='date', prices=prices)
purchases = amo.utils.paginate(request, filter.qs, count=len(prices))
return jingo.render(request, 'users/purchases.html',
{'purchases': purchases, 'filter': filter,
'url_base': reverse('users.purchases'),
'prices': prices, 'single': bool(addon_id)})
'single': bool(addon_id)})
# Start of the Support wizard all of these are accessed through the
# SupportWizard below.
def plain(request, contribution, wizard):
# Simple view that just shows a template matching the step.
tpl = wizard.tpl('%s.html' % wizard.step)
return wizard.render(request, tpl, {'addon': contribution.addon,
'contribution': contribution})
def support_author(request, contribution, wizard):
addon = contribution.addon
form = forms.ContactForm(request.POST)
if request.method == 'POST':
if form.is_valid():
template = jingo.render_to_string(request,
wizard.tpl('emails/support-request.txt'),
context={'contribution': contribution,
'addon': addon, 'form': form,
'user': request.amo_user})
log.info('Support request to dev. by user: %s for addon: %s' %
(request.amo_user.pk, addon.pk))
# L10n: %s is the addon name.
send_mail(_(u'New Support Request for %s' % addon.name),
template, request.amo_user.email,
[smart_str(addon.support_email)])
return redirect(reverse('users.support',
args=[contribution.pk, 'author-sent']))
return wizard.render(request, wizard.tpl('author.html'),
{'addon': addon, 'form': form})
def support_mozilla(request, contribution, wizard):
addon = contribution.addon
form = forms.ContactForm(request.POST)
if request.method == 'POST':
if form.is_valid():
template = jingo.render_to_string(request,
wizard.tpl('emails/support-request.txt'),
context={'addon': addon, 'form': form,
'contribution': contribution,
'user': request.amo_user})
log.info('Support request to mozilla by user: %s for addon: %s' %
(request.amo_user.pk, addon.pk))
# L10n: %s is the addon name.
send_mail(_(u'New Support Request for %s' % addon.name),
template, request.amo_user.email, [settings.FLIGTAR])
return redirect(reverse('users.support',
args=[contribution.pk, 'mozilla-sent']))
return wizard.render(request, wizard.tpl('mozilla.html'),
{'addon': addon, 'form': form})
def refund_request(request, contribution, wizard):
addon = contribution.addon
form = forms.RemoveForm(request.POST or None)
if request.method == 'POST':
if form.is_valid():
return redirect(reverse('users.support',
args=[contribution.pk, 'reason']))
return wizard.render(request, wizard.tpl('request.html'),
{'addon': addon, 'form': form,
'contribution': contribution})
def refund_reason(request, contribution, wizard):
addon = contribution.addon
if not 'request' in wizard.get_progress():
return redirect(reverse('users.support',
args=[contribution.pk, 'request']))
form = forms.ContactForm(request.POST or None)
if request.method == 'POST':
if form.is_valid():
# if under 30 minutes, refund
# TODO(ashort): add in the logic for under 30 minutes.
template = jingo.render_to_string(request,
wizard.tpl('emails/refund-request.txt'),
context={'addon': addon, 'form': form,
'user': request.amo_user,
'contribution': contribution})
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):
title = _lazy('Support')
steps = SortedDict((('start', plain),
('site', plain),
('resources', plain),
('mozilla', support_mozilla),
('mozilla-sent', plain),
('author', support_author),
('author-sent', plain),
('request', refund_request),
('reason', refund_reason),
('refund-sent', plain)))
def tpl(self, x):
return 'users/support/%s' % x
@property
def wrapper(self):
return self.tpl('wrapper.html')
@method_decorator(login_required)
def dispatch(self, request, contribution_id, step='', *args, **kw):
contribution = get_object_or_404(Contribution, pk=contribution_id)
if contribution.user.pk != request.amo_user.pk:
raise http.Http404
args = [contribution] + list(args)
return super(SupportWizard, self).dispatch(request, step, *args, **kw)

Просмотреть файл

@ -373,6 +373,7 @@ INSTALLED_APPS = (
# Has to load after auth
'django_browserid',
'radagast',
)
# These apps will be removed from INSTALLED_APPS in a production environment.