Merge branch 'master' of github.com:mozilla/zamboni

This commit is contained in:
Chris Van 2012-05-30 16:54:25 -07:00
Родитель c90dc3ec83 833404b07c
Коммит 67a87b2895
6 изменённых файлов: 114 добавлений и 42 удалений

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

@ -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}