Merge branch 'master' of github.com:mozilla/zamboni
This commit is contained in:
Коммит
67a87b2895
|
@ -204,10 +204,11 @@ Example body data::
|
|||
"device_types": ["desktop-1"],
|
||||
"summary": "wat...",
|
||||
"support_email": "a@a.com",
|
||||
"categories": [1L, 2L]
|
||||
"categories": [1L, 2L],
|
||||
"previews": [],
|
||||
}
|
||||
|
||||
*TODO*: should screenshot re-ordering be added here.
|
||||
Previews will be list of URLs pointing to the screenshot API.
|
||||
|
||||
Status
|
||||
======
|
||||
|
@ -244,43 +245,61 @@ Screenshots or videos
|
|||
=====================
|
||||
|
||||
These can be added as seperate API calls. There are limits in the marketplace
|
||||
for what screenshots and videos can be accepted.
|
||||
|
||||
*TODO*: implement this.
|
||||
for what screenshots and videos can be accepted. There is a 5MB limit on file
|
||||
uploads.
|
||||
|
||||
Create
|
||||
++++++
|
||||
|
||||
Create a screenshot or video::
|
||||
|
||||
PUT /api/apps/<slug>/screenshot
|
||||
PUT /en-US/api/apps/preview/?app=<app id>
|
||||
|
||||
The body should contain the screenshot or video to be uploaded.
|
||||
The body should contain the screenshot or video to be uploaded in the following
|
||||
format::
|
||||
|
||||
{"position": 1, "file": {"type": "image/jpg", "data": "iVBOR..."}}
|
||||
|
||||
Fields:
|
||||
|
||||
* `file`: a dictionary containing two fields:
|
||||
* `type`: the content type
|
||||
* `data`: base64 encoded string of the preview to be added
|
||||
* `position`: the position of the preview on the app. We show the previews in
|
||||
order
|
||||
|
||||
This will return a 201 if the screenshot or video is successfully created. If
|
||||
not we'll return the reason for the error.
|
||||
|
||||
Returns the screenshot id::
|
||||
|
||||
{"id": "12"}
|
||||
{"position": 1, "thumbnail_url": "/img/uploads/...",
|
||||
"image_url": "/img/uploads/...", "filetype": "image/png",
|
||||
"resource_uri": "/en-US/api/apps/preview/1/"}
|
||||
|
||||
Update
|
||||
++++++
|
||||
Get
|
||||
+++
|
||||
|
||||
Update a screenshot or video::
|
||||
Get information about the screenshot or video::
|
||||
|
||||
POST /api/apps/<slug>/screenshot/<id>
|
||||
|
||||
This will return a 200 if the screenshot or video is succesfully updated.
|
||||
GET /en-US/api/apps/preview/<preview id>/
|
||||
|
||||
Returns::
|
||||
|
||||
{"addon": "/en-US/api/apps/app/1/", "id": 1, "position": 1,
|
||||
"thumbnail_url": "/img/uploads/...", "image_url": "/img/uploads/...",
|
||||
"filetype": "image/png", "resource_uri": "/en-US/api/apps/preview/1/"}
|
||||
|
||||
|
||||
Delete
|
||||
++++++
|
||||
|
||||
Delete a screenshot of video::
|
||||
|
||||
DELETE /api/apps/<slug>/screenshot/<id>
|
||||
DELETE /en-US/api/apps/previe/<preview id>/
|
||||
|
||||
This will return a 200 if the screenshot has been deleted.
|
||||
This will return a 204 if the screenshot has been deleted.
|
||||
|
||||
|
||||
Other APIs
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
title: {
|
||||
text: null
|
||||
},
|
||||
tickmarkPlacement: 'on'
|
||||
tickmarkPlacement: 'on',
|
||||
startOfWeek: 0
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
|
@ -65,7 +66,8 @@
|
|||
hover: {
|
||||
lineWidth: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
connectNulls: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -80,19 +82,21 @@
|
|||
"versions" : "users",
|
||||
"statuses" : "users",
|
||||
"users_created" : "users",
|
||||
"installs" : "users",
|
||||
"downloads" : "downloads",
|
||||
"sources" : "downloads",
|
||||
"contributions" : "currency",
|
||||
"sales" : "currency",
|
||||
"revenue" : "currency",
|
||||
"reviews_created" : "reviews",
|
||||
"addons_in_use" : "addons",
|
||||
"addons_created" : "addons",
|
||||
"addons_updated" : "addons",
|
||||
"addons_downloaded" : "addons",
|
||||
"collections_created" : "collections",
|
||||
"collections_created": "collections",
|
||||
"subscribers" : "collections",
|
||||
"ratings" : "collections"
|
||||
"ratings" : "collections",
|
||||
"sales" : "sales",
|
||||
"refunds" : "refunds",
|
||||
"installs" : "installs"
|
||||
};
|
||||
|
||||
var acceptedGroups = {
|
||||
|
@ -146,6 +150,7 @@
|
|||
var step = '1 ' + group,
|
||||
point,
|
||||
dataSum = 0;
|
||||
|
||||
forEachISODate({start: start, end: end}, '1 '+group, data, function(row, d) {
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
field = fields[i];
|
||||
|
@ -156,17 +161,31 @@
|
|||
}
|
||||
}, this);
|
||||
|
||||
// highCharts seems to dislike 0 and null data when determining a yAxis range
|
||||
// Display marker if only one data point.
|
||||
baseConfig.plotOptions.line.marker.radius = 3;
|
||||
var count = 0,
|
||||
dateRegex = /\d{4}-\d{2}-\d{2}/;
|
||||
for (var key in data) {
|
||||
if (dateRegex.exec(key) && data.hasOwnProperty(key)) {
|
||||
count++;
|
||||
}
|
||||
if (count > 1) {
|
||||
baseConfig.plotOptions.line.marker.radius = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// highCharts seems to dislike 0 and null data when determining a yAxis range.
|
||||
if (dataSum === 0) {
|
||||
baseConfig.yAxis.max = 10;
|
||||
} else {
|
||||
baseConfig.yAxis.max = null;
|
||||
}
|
||||
|
||||
// Transform xAxis based on time grouping (day, week, month) and range
|
||||
// Transform xAxis based on time grouping (day, week, month) and range.
|
||||
var pointInterval = dayMsecs = 1 * 24 * 3600 * 1000;
|
||||
baseConfig.xAxis.tickInterval = (end - start) / 7;
|
||||
baseConfig.xAxis.min = start - dayMsecs;
|
||||
baseConfig.xAxis.min = start - dayMsecs; // Fix chart truncation.
|
||||
baseConfig.xAxis.max = end;
|
||||
if (group == 'month') {
|
||||
pointInterval = 30 * dayMsecs;
|
||||
|
@ -176,6 +195,18 @@
|
|||
baseConfig.xAxis.tickInterval = pointInterval;
|
||||
}
|
||||
|
||||
// Set minimum max value for yAxis to prevent duplicate yAxis values.
|
||||
var max = 0;
|
||||
for (var key in data) {
|
||||
if (data[key].count > max) {
|
||||
max = data[key].count;
|
||||
}
|
||||
}
|
||||
// Chart has minimum 5 ticks so set max to 5 to avoid pigeonholing.
|
||||
if (max < 5) {
|
||||
baseConfig.yAxis.max = 5;
|
||||
}
|
||||
|
||||
// Populate the chart config object.
|
||||
var chartData = [], id;
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
|
@ -186,8 +217,8 @@
|
|||
'name' : z.StatsManager.getPrettyName(view.metric, id),
|
||||
'id' : id,
|
||||
'pointInterval' : pointInterval,
|
||||
// compensate for timezone offsets from UTC.
|
||||
'pointStart' : start.getTime() - start.getTimezoneOffset() * 60000 + dayMsecs,
|
||||
// Compensate for timezone offsets from UTC.
|
||||
'pointStart' : start.getTime() - start.getTimezoneOffset() * 60000,
|
||||
'data' : series[field],
|
||||
'visible' : !(metric == 'contributions' && id !='total')
|
||||
});
|
||||
|
@ -199,14 +230,17 @@
|
|||
var xFormatter,
|
||||
yFormatter;
|
||||
function dayFormatter(d) { return Highcharts.dateFormat('%a, %b %e, %Y', new Date(d)); }
|
||||
function weekFormatter(d) { return "Week of " + Highcharts.dateFormat('%b %e, %Y', new Date(d)); }
|
||||
function weekFormatter(d) { return format(gettext('Week of {0}'), Highcharts.dateFormat('%b %e, %Y', new Date(d))); }
|
||||
function monthFormatter(d) { return Highcharts.dateFormat('%B %Y', new Date(d)); }
|
||||
function downloadFormatter(n) { return Highcharts.numberFormat(n, 0) + ' downloads'; }
|
||||
function userFormatter(n) { return Highcharts.numberFormat(n, 0) + ' users'; }
|
||||
function addonsFormatter(n) { return Highcharts.numberFormat(n, 0) + ' addons'; }
|
||||
function collectionsFormatter(n) { return Highcharts.numberFormat(n, 0) + ' collections'; }
|
||||
function reviewsFormatter(n) { return Highcharts.numberFormat(n, 0) + ' reviews'; }
|
||||
function downloadFormatter(n) { return gettext(Highcharts.numberFormat(n, 0) + 'downloads'); }
|
||||
function userFormatter(n) { return format(gettext('{0} users'), Highcharts.numberFormat(n, 0)); }
|
||||
function addonsFormatter(n) { return format(gettext('{0} add-ons'), Highcharts.numberFormat(n, 0)); }
|
||||
function collectionsFormatter(n) { return format(gettext('{0} collections'), Highcharts.numberFormat(n, 0)); }
|
||||
function reviewsFormatter(n) { return format(gettext('{0} reviews'), Highcharts.numberFormat(n, 0)); }
|
||||
function currencyFormatter(n) { return '$' + Highcharts.numberFormat(n, 2); }
|
||||
function salesFormatter(n) { return format(gettext('{0} sales'), Highcharts.numberFormat(n, 0)); }
|
||||
function refundsFormatter(n) { return format(gettext('{0} refunds'), Highcharts.numberFormat(n, 0)); }
|
||||
function installsFormatter(n) { return format(gettext('{0} installs'), Highcharts.numberFormat(n, 0)); }
|
||||
function addEventData(s, date) {
|
||||
var e = events[date];
|
||||
if (e) {
|
||||
|
@ -259,7 +293,7 @@
|
|||
case "downloads":
|
||||
yFormatter = downloadFormatter;
|
||||
break;
|
||||
case "currency":
|
||||
case "currency": case "revenue":
|
||||
yFormatter = currencyFormatter;
|
||||
break;
|
||||
case "collections":
|
||||
|
@ -271,6 +305,15 @@
|
|||
case "addons":
|
||||
yFormatter = addonsFormatter;
|
||||
break;
|
||||
case "sales":
|
||||
yFormatter = salesFormatter;
|
||||
break;
|
||||
case "refunds":
|
||||
yFormatter = refundsFormatter;
|
||||
break;
|
||||
case "installs":
|
||||
yFormatter = installsFormatter;
|
||||
break;
|
||||
}
|
||||
return function() {
|
||||
var ret = "<b>" + this.series.name + "</b><br>" +
|
||||
|
|
|
@ -234,7 +234,7 @@
|
|||
function monthFormatter(d) { return Highcharts.dateFormat('%B %Y', new Date(d)); }
|
||||
function downloadFormatter(n) { return gettext(Highcharts.numberFormat(n, 0) + 'downloads'); }
|
||||
function userFormatter(n) { return format(gettext('{0} users'), Highcharts.numberFormat(n, 0)); }
|
||||
function addonsFormatter(n) { return format(gettext('{0} addons'), Highcharts.numberFormat(n, 0)); }
|
||||
function addonsFormatter(n) { return format(gettext('{0} add-ons'), Highcharts.numberFormat(n, 0)); }
|
||||
function collectionsFormatter(n) { return format(gettext('{0} collections'), Highcharts.numberFormat(n, 0)); }
|
||||
function reviewsFormatter(n) { return format(gettext('{0} reviews'), Highcharts.numberFormat(n, 0)); }
|
||||
function currencyFormatter(n) { return '$' + Highcharts.numberFormat(n, 2); }
|
||||
|
|
|
@ -4,11 +4,12 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||
|
||||
from tastypie import http
|
||||
from tastypie.bundle import Bundle
|
||||
from tastypie.exceptions import ImmediateHttpResponse, NotFound
|
||||
from tastypie.exceptions import ImmediateHttpResponse
|
||||
from tastypie.resources import ModelResource
|
||||
|
||||
from translations.fields import PurifiedField, TranslatedField
|
||||
|
||||
|
||||
class MarketplaceResource(ModelResource):
|
||||
|
||||
def get_resource_uri(self, bundle_or_obj):
|
||||
|
|
|
@ -84,6 +84,8 @@ class ValidationResource(MarketplaceResource):
|
|||
|
||||
|
||||
class AppResource(MarketplaceResource):
|
||||
previews = fields.ToManyField('mkt.api.resources.PreviewResource',
|
||||
'previews', readonly=True)
|
||||
|
||||
class Meta:
|
||||
queryset = Webapp.objects.all().no_transforms()
|
||||
|
@ -197,23 +199,22 @@ class CategoryResource(MarketplaceResource):
|
|||
|
||||
|
||||
class PreviewResource(MarketplaceResource):
|
||||
addon = fields.ForeignKey(AppResource, 'addon')
|
||||
image_url = fields.CharField(attribute='image_url', readonly=True)
|
||||
thumbnail_url = fields.CharField(attribute='thumbnail_url', readonly=True)
|
||||
|
||||
class Meta:
|
||||
queryset = Preview.objects.all()
|
||||
list_allowed_methods = ['post']
|
||||
allowed_methods = ['get', 'delete']
|
||||
always_return_data = True
|
||||
fields = ['id']
|
||||
fields = ['id', 'filetype']
|
||||
authentication = MarketplaceAuthentication()
|
||||
authorization = OwnerAuthorization()
|
||||
resource_name = 'preview'
|
||||
filtering = {'addon': ALL_WITH_RELATIONS}
|
||||
|
||||
def obj_create(self, bundle, request, **kwargs):
|
||||
filters = self.build_filters(filters=request.GET.copy())
|
||||
addon = self.get_object_or_404(Webapp,
|
||||
pk=filters.get('addon__exact'))
|
||||
addon = self.get_object_or_404(Webapp, pk=request.GET.get('app'))
|
||||
if not AppOwnerAuthorization().is_authorized(request, object=addon):
|
||||
raise ImmediateHttpResponse(response=http.HttpForbidden())
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.conf import settings
|
|||
from mock import patch
|
||||
from nose.tools import eq_
|
||||
|
||||
from addons.models import Addon, AddonUser, Category, DeviceType
|
||||
from addons.models import Addon, AddonUser, Category, DeviceType, Preview
|
||||
import amo
|
||||
from amo.tests import AMOPaths
|
||||
from files.models import FileUpload
|
||||
|
@ -219,6 +219,14 @@ class TestAppCreateHandler(CreateHandler, AMOPaths):
|
|||
content = json.loads(res.content)
|
||||
eq_(content['status'], 0)
|
||||
|
||||
def test_get_previews(self):
|
||||
app = self.create_app()
|
||||
res = self.client.get(self.get_url)
|
||||
eq_(len(json.loads(res.content)['previews']), 0)
|
||||
Preview.objects.create(addon=app)
|
||||
res = self.client.get(self.get_url)
|
||||
eq_(len(json.loads(res.content)['previews']), 1)
|
||||
|
||||
def test_get_not_mine(self):
|
||||
obj = self.create_app()
|
||||
obj.authors.clear()
|
||||
|
@ -367,7 +375,7 @@ class TestPreviewHandler(BaseOAuth, AMOPaths):
|
|||
AddonUser.objects.create(user=self.user, addon=self.app)
|
||||
self.file = base64.b64encode(open(self.mozball_image(), 'r').read())
|
||||
self.list_url = ('api_dispatch_list', {'resource_name': 'preview'},
|
||||
{'addon__exact': self.app.pk})
|
||||
{'app': self.app.pk})
|
||||
self.good = {'file': {'data': self.file, 'type': 'image/jpg'},
|
||||
'position': 1}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче