app transaction list page (bug 825262)
This commit is contained in:
Родитель
3ef31ef5e7
Коммит
d08c79d4e8
|
@ -73,6 +73,13 @@ CONTRIB_TYPES = {
|
|||
CONTRIB_VOLUNTARY: _('Voluntary'),
|
||||
}
|
||||
|
||||
MKT_TRANSACTION_CONTRIB_TYPES = {
|
||||
CONTRIB_CHARGEBACK: _('Chargeback'),
|
||||
CONTRIB_INAPP: _('In-app Purchase'),
|
||||
CONTRIB_PURCHASE: _('Purchase'),
|
||||
CONTRIB_REFUND: _('Refund'),
|
||||
}
|
||||
|
||||
CONTRIB_TYPE_DEFAULT = CONTRIB_VOLUNTARY
|
||||
|
||||
INAPP_STATUS_ACTIVE = 0
|
||||
|
|
|
@ -14,7 +14,7 @@ from tower import ugettext as _
|
|||
|
||||
import amo
|
||||
from amo.helpers import absolutify, urlparams
|
||||
from amo.models import ModelBase, SearchMixin
|
||||
from amo.models import SearchMixin
|
||||
from amo.fields import DecimalCharField
|
||||
from amo.utils import send_mail, send_mail_jinja
|
||||
from zadmin.models import DownloadSource
|
||||
|
@ -430,12 +430,12 @@ class ClientData(models.Model):
|
|||
else:
|
||||
lang = translation.get_language()
|
||||
client_data, c = cls.objects.get_or_create(
|
||||
download_source=download_source,
|
||||
device_type=request.POST.get('device_type', ''),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', ''),
|
||||
is_chromeless=request.POST.get('chromeless', False),
|
||||
language=lang,
|
||||
region=region)
|
||||
download_source=download_source,
|
||||
device_type=request.POST.get('device_type', ''),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', ''),
|
||||
is_chromeless=request.POST.get('chromeless', False),
|
||||
language=lang,
|
||||
region=region)
|
||||
return client_data
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -5,7 +5,6 @@ from django.conf import settings
|
|||
from django.db import connection, transaction
|
||||
from django.db.models import Sum, Max
|
||||
|
||||
|
||||
from apiclient.discovery import build
|
||||
import requests
|
||||
import commonware.log
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
@import 'lib';
|
||||
|
||||
th.amount, td.amount {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#tx-filters {
|
||||
margin-bottom: 52px;
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
text-align: right;
|
||||
width: 110px;
|
||||
}
|
||||
.date-to label {
|
||||
width: 50px
|
||||
}
|
||||
button {
|
||||
bottom: 26px;
|
||||
float: right;
|
||||
position: relative;
|
||||
}
|
||||
.form-elem, .errorlist, .errorlist li {
|
||||
display: inline;
|
||||
}
|
||||
.errorlist li {
|
||||
left: 5px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.date-from {
|
||||
float: left;
|
||||
}
|
||||
.form-row {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.results-found {
|
||||
clear: both;
|
||||
display: block;
|
||||
margin-bottom: 13px;
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
@import '../devreg/lib';
|
||||
|
||||
/* data grids */
|
||||
.data-grid-top {
|
||||
border-bottom: 1px solid #A5BFCE;
|
||||
}
|
||||
.data-grid-bottom {
|
||||
border-top: 1px solid #A5BFCE;
|
||||
}
|
||||
.data-grid-content {
|
||||
padding: 5px;
|
||||
|
||||
ol.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
&:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
clear: both;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
table.data-grid {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
thead th {
|
||||
background: @faded-blue;
|
||||
a {
|
||||
&:active, &:hover, &:visited {
|
||||
color: #36b;
|
||||
}
|
||||
}
|
||||
&.ordered {
|
||||
.gradient-two-color(@faded-blue, @border-blue);
|
||||
a {
|
||||
color: #137;
|
||||
&:active, &:hover, &:visited {
|
||||
color: #039;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
border-top: 1px dotted @border-gray;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
&:nth-child(odd) {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background-color: @white;
|
||||
}
|
||||
}
|
||||
.addon-locked {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
.locked .addon-locked {
|
||||
background: url(../../img/mkt/icons/mkt-reviewer-icons.png) no-repeat top left;
|
||||
background-position: -16px -16px;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: @tablet-min) {
|
||||
.data-grid {
|
||||
thead th {
|
||||
&:nth-child(3) {
|
||||
min-width: 7em;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
min-width: 8em;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
min-width: 90px;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
min-width: 7em;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
min-width: 7em;
|
||||
}
|
||||
&:nth-child(8) {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
tr {
|
||||
th, td {
|
||||
padding: 7px 10px;
|
||||
}
|
||||
}
|
||||
th {
|
||||
line-height: 1.3;
|
||||
}
|
||||
.addon-row a {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @landscape-max) {
|
||||
.data-grid {
|
||||
tr {
|
||||
th, td {
|
||||
border: 1px solid @faint-gray;
|
||||
line-height: 12px;
|
||||
font-size: 11px;
|
||||
padding: 4px 2px;
|
||||
text-align: center;
|
||||
}
|
||||
// Don't add borders around table cell for locked icon.
|
||||
th:nth-child(1),
|
||||
td:nth-child(1) {
|
||||
border-right: 0;
|
||||
}
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
thead th {
|
||||
&:nth-child(2) {
|
||||
max-width: 300px;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
min-width: 55px;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
min-width: 45px;
|
||||
}
|
||||
}
|
||||
th {
|
||||
font-weight: 400;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -230,27 +230,6 @@ header {
|
|||
.ed-sprite-sort-desc {
|
||||
background-position: right -435px;
|
||||
}
|
||||
/* data grids on queue pages */
|
||||
.data-grid-top {
|
||||
border-bottom: 1px solid #A5BFCE;
|
||||
}
|
||||
.data-grid-bottom {
|
||||
border-top: 1px solid #A5BFCE;
|
||||
}
|
||||
.data-grid-content {
|
||||
padding: 5px;
|
||||
|
||||
ol.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
&:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
clear: both;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.queue-outer {
|
||||
.border-radius(5px);
|
||||
border: 4px solid #E0EFFD;
|
||||
|
@ -273,34 +252,6 @@ header {
|
|||
|
||||
@media (min-width: @tablet-min) {
|
||||
.data-grid {
|
||||
thead th {
|
||||
&:nth-child(3) {
|
||||
min-width: 7em;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
min-width: 8em;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
min-width: 90px;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
min-width: 7em;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
min-width: 7em;
|
||||
}
|
||||
&:nth-child(8) {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
tr {
|
||||
th, td {
|
||||
padding: 7px 10px;
|
||||
}
|
||||
}
|
||||
th {
|
||||
line-height: 1.3;
|
||||
}
|
||||
.addon-row a {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
@ -311,63 +262,6 @@ header {
|
|||
width: 121px;
|
||||
}
|
||||
|
||||
table.data-grid {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
thead th {
|
||||
background: @faded-blue;
|
||||
&:first-child {
|
||||
width: 0;
|
||||
}
|
||||
a {
|
||||
&:active, &:hover, &:visited {
|
||||
color: #36b;
|
||||
}
|
||||
}
|
||||
&.ordered {
|
||||
.gradient-two-color(@faded-blue, @border-blue);
|
||||
a {
|
||||
color: #137;
|
||||
&:active, &:hover, &:visited {
|
||||
color: #039;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
border-top: 1px dotted @border-gray;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
&:nth-child(odd) {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background-color: @white;
|
||||
}
|
||||
}
|
||||
.addon-locked {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
.locked .addon-locked {
|
||||
background: url(../../img/mkt/icons/mkt-reviewer-icons.png) no-repeat top left;
|
||||
background-position: -16px -16px;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
time {
|
||||
color: @medium-gray;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Version notes */
|
||||
#popup-notes .version-notes {
|
||||
overflow: auto;
|
||||
|
@ -559,6 +453,11 @@ table.data-grid tr.comments td {
|
|||
border-top: none;
|
||||
background: #def;
|
||||
}
|
||||
thead th {
|
||||
&:first-child {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting_old, .waiting_med, .waiting_new {
|
||||
height: 20px;
|
||||
|
@ -1368,39 +1267,6 @@ iframe#manifest-contents {
|
|||
}
|
||||
}
|
||||
.data-grid {
|
||||
tr {
|
||||
th, td {
|
||||
border: 1px solid @faint-gray;
|
||||
line-height: 12px;
|
||||
font-size: 11px;
|
||||
padding: 4px 2px;
|
||||
text-align: center;
|
||||
}
|
||||
// Don't add borders around table cell for locked icon.
|
||||
th:nth-child(1),
|
||||
td:nth-child(1) {
|
||||
border-right: 0;
|
||||
}
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
thead th {
|
||||
&:nth-child(2) {
|
||||
max-width: 300px;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
min-width: 55px;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
min-width: 45px;
|
||||
}
|
||||
}
|
||||
th {
|
||||
font-weight: 400;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.addon-row a {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
INSERT INTO waffle_switch_mkt (name, active, created, modified, note)
|
||||
VALUES ('view-transactions', 0, NOW(), NOW(),
|
||||
'Enable transaction pages on Marketplace.');
|
|
@ -35,6 +35,9 @@ CSS = {
|
|||
'css/common/forms.less',
|
||||
'css/devreg/devhub-forms.less',
|
||||
|
||||
# Tables.
|
||||
'css/mkt/data-grid.less',
|
||||
|
||||
# Landing page
|
||||
'css/devreg/landing.less',
|
||||
|
||||
|
@ -45,6 +48,7 @@ CSS = {
|
|||
'css/devreg/in-app-config.less',
|
||||
'css/devreg/payments.less',
|
||||
'css/devreg/refunds.less',
|
||||
'css/devreg/transactions.less',
|
||||
'css/devreg/status.less',
|
||||
|
||||
# Image Uploads (used for "Edit Listing" Images and Submission).
|
||||
|
@ -72,6 +76,7 @@ CSS = {
|
|||
'mkt/reviewers': (
|
||||
'css/mkt/buttons.less',
|
||||
'css/mkt/ratings.less',
|
||||
'css/mkt/data-grid.less',
|
||||
'css/mkt/reviewers.less',
|
||||
'css/mkt/themes_review.less',
|
||||
'css/mkt/paginator.less',
|
||||
|
|
|
@ -5,6 +5,7 @@ import os
|
|||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms.extras.widgets import SelectDateWidget
|
||||
from django.forms.models import formset_factory, modelformset_factory
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
|
||||
|
@ -34,6 +35,7 @@ import mkt
|
|||
from mkt.constants import APP_IMAGE_SIZES, MAX_PACKAGED_APP_SIZE
|
||||
from mkt.constants.ratingsbodies import (RATINGS_BY_NAME, ALL_RATINGS,
|
||||
RATINGS_BODIES)
|
||||
from mkt.site.forms import AddonChoiceField
|
||||
from mkt.webapps.models import (AddonExcludedRegion, ContentRating, ImageAsset,
|
||||
Webapp)
|
||||
|
||||
|
@ -849,3 +851,26 @@ class AppFormTechnical(addons.forms.AddonFormBase):
|
|||
af.update(uses_flash=bool(uses_flash))
|
||||
|
||||
return super(AppFormTechnical, self).save(commit=True)
|
||||
|
||||
|
||||
class TransactionFilterForm(happyforms.Form):
|
||||
app = AddonChoiceField(queryset=None, required=False, label=_lazy(u'App'))
|
||||
transaction_type = forms.ChoiceField(
|
||||
required=False, label=_lazy(u'Transaction Type'),
|
||||
choices=[(None, '')] + amo.MKT_TRANSACTION_CONTRIB_TYPES.items())
|
||||
transaction_id = forms.CharField(
|
||||
required=False, label=_lazy(u'Transaction ID'))
|
||||
|
||||
current_year = datetime.today().year
|
||||
years = [current_year - x for x in range(current_year - 2012)]
|
||||
date_from = forms.DateTimeField(
|
||||
required=False, widget=SelectDateWidget(years=years),
|
||||
label=_lazy(u'From'))
|
||||
date_to = forms.DateTimeField(
|
||||
required=False, widget=SelectDateWidget(years=years),
|
||||
label=_lazy(u'To'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.apps = kwargs.pop('apps', [])
|
||||
super(TransactionFilterForm, self).__init__(*args, **kwargs)
|
||||
self.fields['app'].queryset = self.apps
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
{% extends 'developers/base_impala.html' %}
|
||||
{% from 'site/helpers/form_row.html' import form_row %}
|
||||
|
||||
{% set title = _('Transaction Details') %}
|
||||
{% block title %}{{ hub_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
{{ hub_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
<h1>{{ title }}</h1>
|
||||
</header>
|
||||
|
||||
<div id="tx-filters" class="island">
|
||||
<form>
|
||||
{{ form_row(form, ('app',)) }}
|
||||
{{ form_row(form, ('transaction_type',)) }}
|
||||
{{ form_row(form, ('transaction_id',)) }}
|
||||
<div class="date-from">
|
||||
{{ form_row(form, ('date_from',)) }}
|
||||
</div>
|
||||
<div class="date-to">
|
||||
{{ form_row(form, ('date_to',)) }}
|
||||
</div>
|
||||
<button type="submit">{{ _('Filter Transactions') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<strong class="results-found">{{ ngettext('{num} transaction found',
|
||||
'{num} transactions found',
|
||||
count)|f(num=count) }}</strong>
|
||||
|
||||
<div class="island tabcontent">
|
||||
{% if not transactions.paginator.count %}
|
||||
<p class="no-results">
|
||||
{{ _('Your apps currently have no transactions.') }}
|
||||
</p>
|
||||
{% else %}
|
||||
<table class="data-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('App') }}</th>
|
||||
<th>{{ _('Date') }}</th>
|
||||
<th>{{ _('Type') }}</th>
|
||||
<th>{{ _('Transaction ID') }}</th>
|
||||
<th>{{ _('Currency') }}</th>
|
||||
<th class="amount">{{ _('Amount') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in transactions.object_list %}
|
||||
<tr>
|
||||
<td>{{ transaction.addon.name }}</td>
|
||||
<td>{{ transaction.created|datetime }}</td>
|
||||
<td>{{ CONTRIB_TYPES[transaction.type] }}</td>
|
||||
<td>{{ transaction.transaction_id }}</td>
|
||||
<td>{{ transaction.currency }}</td>
|
||||
{% if transaction.amount %}
|
||||
<td class="amount">{{ transaction.amount|numberfmt }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ transactions|impala_paginator }}
|
||||
{% endblock %}
|
|
@ -12,13 +12,13 @@ from test_utils import RequestFactory
|
|||
|
||||
import amo
|
||||
import amo.tests
|
||||
from amo.tests import app_factory
|
||||
from amo.tests.test_helpers import get_image_path
|
||||
from addons.models import Addon, AddonCategory, Category
|
||||
from files.helpers import copyfileobj
|
||||
|
||||
import mkt
|
||||
from mkt.developers import forms
|
||||
from mkt.site.fixtures import fixture
|
||||
from mkt.webapps.models import (AddonExcludedRegion as AER, ContentRating,
|
||||
Webapp)
|
||||
|
||||
|
@ -283,3 +283,34 @@ class TestPackagedAppForm(amo.tests.AMOPaths, amo.tests.WebappTestCase):
|
|||
eq_(validation['messages'][0]['message'],
|
||||
[u'Packaged app too large for submission.',
|
||||
u'Packages must be less than 5 bytes.'])
|
||||
|
||||
|
||||
class TestTransactionFilterForm(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
(app_factory(), app_factory())
|
||||
# Need queryset to initialize form.
|
||||
self.apps = Webapp.objects.all()
|
||||
self.data = {
|
||||
'app': self.apps[0].id,
|
||||
'transaction_type': 1,
|
||||
'transaction_id': 1,
|
||||
'date_from_day': '1',
|
||||
'date_from_month': '1',
|
||||
'date_from_year': '2012',
|
||||
'date_to_day': '1',
|
||||
'date_to_month': '1',
|
||||
'date_to_year': '2013',
|
||||
}
|
||||
|
||||
def test_basic(self):
|
||||
"""Test the form doesn't crap out."""
|
||||
form = forms.TransactionFilterForm(self.data, apps=self.apps)
|
||||
eq_(form.is_valid(), True)
|
||||
|
||||
def test_app_choices(self):
|
||||
"""Test app choices."""
|
||||
form = forms.TransactionFilterForm(self.data, apps=self.apps)
|
||||
for app in self.apps:
|
||||
assertion = (app.id, app.name) in form.fields['app'].choices
|
||||
assert assertion, '(%s, %s) not in choices' % (app.id, app.name)
|
||||
|
|
|
@ -8,6 +8,7 @@ from contextlib import contextmanager
|
|||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage as storage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
import mock
|
||||
import waffle
|
||||
|
@ -19,7 +20,7 @@ from pyquery import PyQuery as pq
|
|||
import amo
|
||||
import amo.tests
|
||||
from addons.models import Addon, AddonDeviceType, AddonUpsell, AddonUser
|
||||
from amo.tests import assert_no_validation_errors
|
||||
from amo.tests import app_factory, assert_no_validation_errors
|
||||
from amo.tests.test_helpers import get_image_path
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.utils import urlparams
|
||||
|
@ -27,6 +28,7 @@ from browse.tests import test_default_sort, test_listing_sort
|
|||
from files.models import FileUpload
|
||||
from files.tests.test_models import UploadTest as BaseUploadTest
|
||||
from market.models import AddonPremium, Price
|
||||
from stats.models import Contribution
|
||||
from translations.models import Translation
|
||||
from users.models import UserProfile
|
||||
from versions.models import Version
|
||||
|
@ -35,6 +37,8 @@ import mkt
|
|||
from mkt.constants import MAX_PACKAGED_APP_SIZE
|
||||
from mkt.developers import tasks
|
||||
from mkt.developers.models import ActivityLog
|
||||
from mkt.developers.views import _filter_transactions, _get_transactions
|
||||
from mkt.site.fixtures import fixture
|
||||
from mkt.submit.models import AppSubmissionChecklist
|
||||
from mkt.webapps.models import Webapp
|
||||
|
||||
|
@ -1039,3 +1043,72 @@ class TestTerms(amo.tests.TestCase):
|
|||
eq_(doc('#site-notice').length, 0)
|
||||
eq_(doc('#dev-agreement').length, 1)
|
||||
eq_(doc('#agreement-form').length, 0)
|
||||
|
||||
|
||||
class TestTransactionList(amo.tests.TestCase):
|
||||
fixtures = fixture('user_999')
|
||||
|
||||
def setUp(self):
|
||||
"""Create and set up apps for some filtering fun."""
|
||||
self.create_switch(name='view-transactions')
|
||||
self.url = reverse('mkt.developers.transactions')
|
||||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
|
||||
self.apps = [app_factory(), app_factory()]
|
||||
self.user = UserProfile.objects.get(id=999)
|
||||
for app in self.apps:
|
||||
AddonUser.objects.create(addon=app, user=self.user)
|
||||
|
||||
# Set up transactions.
|
||||
tx0 = Contribution.objects.create(addon=self.apps[0],
|
||||
type=amo.CONTRIB_PURCHASE,
|
||||
transaction_id=12345)
|
||||
tx1 = Contribution.objects.create(addon=self.apps[1],
|
||||
type=amo.CONTRIB_REFUND,
|
||||
transaction_id=67890)
|
||||
tx0.update(created=datetime.date(2011, 12, 25))
|
||||
tx1.update(created=datetime.date(2012, 1, 1))
|
||||
self.txs = [tx0, tx1]
|
||||
|
||||
def test_200(self):
|
||||
r = self.client.get(self.url)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
def test_own_apps(self):
|
||||
"""Only user's transactions are shown."""
|
||||
app_factory()
|
||||
r = RequestFactory().get(self.url)
|
||||
r.user = self.user
|
||||
transactions = _get_transactions(r)[1]
|
||||
self.assertSetEqual([tx.addon for tx in transactions], self.apps)
|
||||
|
||||
def test_filter(self):
|
||||
"""For each field in the form, run it through view and check results.
|
||||
"""
|
||||
tx0 = self.txs[0]
|
||||
tx1 = self.txs[1]
|
||||
|
||||
self.do_filter(self.txs)
|
||||
|
||||
self.do_filter([tx0], app=tx0.id)
|
||||
self.do_filter([tx1], app=tx1.id)
|
||||
|
||||
self.do_filter([tx0], transaction_type=tx0.type)
|
||||
self.do_filter([tx1], transaction_type=tx1.type)
|
||||
|
||||
self.do_filter([tx0], transaction_id=tx0.transaction_id)
|
||||
self.do_filter([tx1], transaction_id=tx1.transaction_id)
|
||||
|
||||
self.do_filter(self.txs, date_from=datetime.date(2011, 12, 1))
|
||||
self.do_filter([tx1], date_from=datetime.date(2011, 12, 30),
|
||||
date_to=datetime.date(2012, 2, 1))
|
||||
|
||||
def do_filter(self, expected_txs, **kw):
|
||||
"""Checks that filter returns the expected ids
|
||||
|
||||
expected_ids -- list of app ids expected in the result.
|
||||
"""
|
||||
qs = _filter_transactions(Contribution.objects.all(), kw)
|
||||
|
||||
self.assertSetEqual(qs.values_list('id', flat=True),
|
||||
[tx.id for tx in expected_txs])
|
||||
|
|
|
@ -132,6 +132,8 @@ urlpatterns = decorate(write, patterns('',
|
|||
views.docs, name='mkt.developers.docs'),
|
||||
|
||||
url('^statistics/', include(all_apps_stats_patterns)),
|
||||
url('^transactions/', views.transactions,
|
||||
name='mkt.developers.transactions'),
|
||||
|
||||
# Bango-specific stuff.
|
||||
url('^bango/', include(bango_patterns('bango'))),
|
||||
|
|
|
@ -37,6 +37,7 @@ from files.models import File, FileUpload
|
|||
from files.utils import parse_addon
|
||||
from lib.cef_loggers import inapp_cef
|
||||
from market.models import Refund
|
||||
from stats.models import Contribution
|
||||
from translations.models import delete_translation
|
||||
from users.models import UserProfile
|
||||
from users.views import _login
|
||||
|
@ -49,7 +50,7 @@ from mkt.developers.forms import (AppFormBasic, AppFormDetails, AppFormMedia,
|
|||
AppFormSupport, AppFormTechnical,
|
||||
CategoryForm, ImageAssetFormSet,
|
||||
NewPackagedAppForm, PreviewFormSet,
|
||||
trap_duplicate)
|
||||
TransactionFilterForm, trap_duplicate)
|
||||
from mkt.developers.forms_payments import InappConfigForm
|
||||
from mkt.developers.utils import check_upload
|
||||
from mkt.inapp_pay.models import InappConfig
|
||||
|
@ -467,7 +468,8 @@ def refresh_manifest(request, addon_id, addon, webapp=False):
|
|||
@json_view
|
||||
def _upload_manifest(request, is_standalone=False):
|
||||
form = forms.NewManifestForm(request.POST, is_standalone=is_standalone)
|
||||
if not is_standalone and waffle.switch_is_active('webapps-unique-by-domain'):
|
||||
if (not is_standalone and
|
||||
waffle.switch_is_active('webapps-unique-by-domain')):
|
||||
# Helpful error if user already submitted the same manifest.
|
||||
dup_msg = trap_duplicate(request, request.POST.get('manifest'))
|
||||
if dup_msg:
|
||||
|
@ -871,3 +873,39 @@ def blocklist(request, addon):
|
|||
messages.info(request, _('App already blocklisted.'))
|
||||
|
||||
return redirect(addon.get_dev_url('versions'))
|
||||
|
||||
|
||||
@waffle_switch('view-transactions')
|
||||
@login_required
|
||||
def transactions(request):
|
||||
form, transactions = _get_transactions(request)
|
||||
return jingo.render(
|
||||
request, 'developers/transactions.html',
|
||||
{'form': form,
|
||||
'CONTRIB_TYPES': amo.CONTRIB_TYPES,
|
||||
'count': transactions.count(),
|
||||
'transactions': amo.utils.paginate(request,
|
||||
transactions, per_page=50)})
|
||||
|
||||
|
||||
def _get_transactions(request):
|
||||
apps = addon_listing(request, webapp=True)[0]
|
||||
transactions = Contribution.objects.filter(addon__in=list(apps))
|
||||
|
||||
form = TransactionFilterForm(request.GET, apps=apps)
|
||||
if form.is_valid():
|
||||
transactions = _filter_transactions(transactions, form.cleaned_data)
|
||||
return form, transactions
|
||||
|
||||
|
||||
def _filter_transactions(qs, data):
|
||||
"""Handle search filters and queries for transactions."""
|
||||
filter_mapping = {'app': 'addon_id',
|
||||
'transaction_type': 'type',
|
||||
'transaction_id': 'transaction_id',
|
||||
'date_from': 'created__gte',
|
||||
'date_to': 'created__lte'}
|
||||
for form_field, db_field in filter_mapping.iteritems():
|
||||
if data.get(form_field):
|
||||
qs = qs.filter(**{db_field: data[form_field]})
|
||||
return qs
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends 'reviewers/base.html' %}
|
||||
{% from 'site/helpers/form_row.html' import form_row %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ reviewers_breadcrumbs(queue=tab) }}
|
||||
|
@ -88,26 +89,17 @@
|
|||
{% endif %}
|
||||
|
||||
<div id="advanced-search" class="c{% if not adv_searching %} hidden{% endif %}">
|
||||
{% macro form_element(search_form, elems) -%}
|
||||
<div class="form-row">
|
||||
{% for elem in elems %}
|
||||
{{ search_form[elem].label_tag() }}
|
||||
<div class="form-elem">{{ search_form[elem] }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
<div class="basic-filters">
|
||||
{{ form_element(search_form,
|
||||
{{ form_row(search_form,
|
||||
('admin_review', 'has_editor_comment',
|
||||
'has_info_request', 'waiting_time_days',
|
||||
'app_type')) }}
|
||||
</div>
|
||||
<div class="device-type-select">
|
||||
{{ form_element(search_form, ('device_type_ids',)) }}
|
||||
{{ form_row(search_form, ('device_type_ids',)) }}
|
||||
</div>
|
||||
<div class="premium-type-select">
|
||||
{{ form_element(search_form, ('premium_type_ids',)) }}
|
||||
{{ form_row(search_form, ('premium_type_ids',)) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{% macro form_row(search_form, elems) -%}
|
||||
<div class="form-row">
|
||||
{% for elem in elems %}
|
||||
{{ search_form[elem].label_tag() }}
|
||||
<div class="form-elem">{{ search_form[elem] }}</div>
|
||||
{{ search_form[elem].errors }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{%- endmacro %}
|
Загрузка…
Ссылка в новой задаче