diff --git a/apps/constants/payments.py b/apps/constants/payments.py
index e219420e8cd..bf80b6a2ff5 100644
--- a/apps/constants/payments.py
+++ b/apps/constants/payments.py
@@ -73,6 +73,13 @@
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
diff --git a/apps/stats/models.py b/apps/stats/models.py
index 527576e1a08..1adb9ea5c85 100644
--- a/apps/stats/models.py
+++ b/apps/stats/models.py
@@ -14,7 +14,7 @@
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 @@ def get_or_create(cls, request):
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:
diff --git a/apps/stats/tasks.py b/apps/stats/tasks.py
index 0d7984d5484..5cf70146c19 100644
--- a/apps/stats/tasks.py
+++ b/apps/stats/tasks.py
@@ -5,7 +5,6 @@
from django.db import connection, transaction
from django.db.models import Sum, Max
-
from apiclient.discovery import build
import requests
import commonware.log
diff --git a/media/css/devreg/transactions.less b/media/css/devreg/transactions.less
new file mode 100644
index 00000000000..2b76a4b0ff8
--- /dev/null
+++ b/media/css/devreg/transactions.less
@@ -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;
+}
diff --git a/media/css/mkt/data-grid.less b/media/css/mkt/data-grid.less
new file mode 100644
index 00000000000..30b382ae796
--- /dev/null
+++ b/media/css/mkt/data-grid.less
@@ -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;
+ }
+ }
+}
diff --git a/media/css/mkt/reviewers.less b/media/css/mkt/reviewers.less
index 775072d3ea3..e55e3517ae1 100644
--- a/media/css/mkt/reviewers.less
+++ b/media/css/mkt/reviewers.less
@@ -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;
diff --git a/migrations/519-view-tx-waffle b/migrations/519-view-tx-waffle
new file mode 100644
index 00000000000..05135114acb
--- /dev/null
+++ b/migrations/519-view-tx-waffle
@@ -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.');
diff --git a/mkt/asset_bundles.py b/mkt/asset_bundles.py
index 09dbde7fa38..e22136c7034 100644
--- a/mkt/asset_bundles.py
+++ b/mkt/asset_bundles.py
@@ -35,6 +35,9 @@
'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/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 @@
'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',
diff --git a/mkt/developers/forms.py b/mkt/developers/forms.py
index 3e57be1faff..c0bb858f66f 100644
--- a/mkt/developers/forms.py
+++ b/mkt/developers/forms.py
@@ -5,6 +5,7 @@
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 @@
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 @@ def save(self, addon, commit=False):
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
diff --git a/mkt/developers/templates/developers/transactions.html b/mkt/developers/templates/developers/transactions.html
new file mode 100644
index 00000000000..7f784c8be44
--- /dev/null
+++ b/mkt/developers/templates/developers/transactions.html
@@ -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 %}
+ {{ title }}
+
+ {{ _('Your apps currently have no transactions.') }} +
+ {% else %} +{{ _('App') }} | +{{ _('Date') }} | +{{ _('Type') }} | +{{ _('Transaction ID') }} | +{{ _('Currency') }} | +{{ _('Amount') }} | +
---|---|---|---|---|---|
{{ transaction.addon.name }} | +{{ transaction.created|datetime }} | +{{ CONTRIB_TYPES[transaction.type] }} | +{{ transaction.transaction_id }} | +{{ transaction.currency }} | + {% if transaction.amount %} +{{ transaction.amount|numberfmt }} | + {% endif %} +