Skip to content
This repository has been archived by the owner on Mar 15, 2018. It is now read-only.

Commit

Permalink
adding addon compat reporter endpoints (bug 647031, 647032, 647034)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff Balogh committed May 2, 2011
1 parent b9d0589 commit 7c078ff
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 22 deletions.
17 changes: 0 additions & 17 deletions apps/addons/models.py
Expand Up @@ -1203,23 +1203,6 @@ def transformer(addons):
addon.all_categories = cats.get(addon.id, [])


class CompatibilityReport(models.Model):
guid = models.CharField(max_length=128, db_index=True)
works_properly = models.NullBooleanField()
app_guid = models.CharField(max_length=128, blank=True)
app_version = models.CharField(max_length=128, blank=True)
app_build = models.CharField(max_length=128, blank=True)
client_os = models.CharField(max_length=128, blank=True)
client_ip = models.CharField(max_length=128, blank=True)
version = models.CharField(max_length=128, default='0.0')
comments = models.TextField()
other_addons = models.TextField()
created = models.DateTimeField(null=True)

class Meta:
db_table = 'compatibility_reports'


class Feature(amo.models.ModelBase):
addon = models.ForeignKey(Addon)
start = models.DateTimeField()
Expand Down
21 changes: 21 additions & 0 deletions apps/compat/models.py
@@ -0,0 +1,21 @@
from django.db import models

import json_field

import amo.models


class CompatReport(amo.models.ModelBase):
guid = models.CharField(max_length=128)
version = models.CharField(max_length=128)
app_guid = models.CharField(max_length=128)
app_version = models.CharField(max_length=128)
app_build = models.CharField(max_length=128)
client_os = models.CharField(max_length=128)
client_ip = models.CharField(max_length=128)
comments = models.TextField()
other_addons = json_field.JSONField()
works_properly = models.BooleanField()

class Meta:
db_table = 'compatibility_reports'
37 changes: 37 additions & 0 deletions apps/compat/templates/compat/reporter.html
@@ -0,0 +1,37 @@
{% extends "base.html" %}

{% block title %}{{ page_title(_('Add-on Compatibility Reports')) }}{% endblock %}

{% block content %}
<div class="primary">
<hgroup>
<h2>{{ _('Add-on Compatibility Reports') }}</h2>
<h4>{{ _("Enter the GUID of an add-on below to view any reports we've received.") }}</h4>
</hgroup>
<form action="">
<input name="guid" value="{{ query }}" id="compat-query">
</form>
</div>
<div class="secondary">
<div class="highlight">
{% trans url_ = '/en-US/firefox/addon/15003' %}<p>
Reports submitted to us through the
<a href="{{ url_ }}">Add-on Compatibility Reporter</a> are collected here
for developers to view. These reports help us determine which add-ons will
need help supporting an upcoming Firefox version.
</p>{% endtrans %}
</div>
{% if addons %}
<div class="highlight">
<h4>{{ _('Reports for your Add-ons') }}</h4>
<ul>
{% for addon in addons %}
<li>
<a href="{{ url('compat.reporter_detail', addon.guid) }}">{{ addon.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endblock %}
66 changes: 66 additions & 0 deletions apps/compat/templates/compat/reporter_detail.html
@@ -0,0 +1,66 @@
{% extends "base.html" %}

{% block title %}{{ page_title(_('{addon} Compatibility Reports')|f(addon=name)) }}{% endblock %}

{% block bodyclass %}inverse{% endblock %}

{% block content %}
<div class="primary">
{{ breadcrumbs([(url('compat.reporter'), _('Add-on Compatibility Center')),
(None, _('{addon} Compatibility Reports')|f(addon=name))]) }}
<h2>{{ _('{addon} Compatibility Reports')|f(addon=name) }}</h2>
{% trans url_ = '/en-US/firefox/addon/15003' %}<p>
Reports submitted to us through the
<a href="{{ url_ }}">Add-on Compatibility Reporter</a> are collected here
for developers to view. These reports help us determine which add-ons will
need help supporting an upcoming Firefox version.
</p>{% endtrans %}

<table class="compat-info">
<thead>
<th>{{ _('Report Type') }}</th>
<th>{{ _('Version') }}</th>
<th>{{ _('Application') }}</th>
<th>{{ _('Application Build') }}</th>
<th>{{ _('Operating System') }}</th>
<th>{{ _('Submitted') }}</th>
</thead>
<tbody>
{% for report in reports.object_list %}
{% set cls = 'success' if report.works_properly else 'problem' %}
<tr class="{{ cls }}">
<td>{{ _('Success') if report.works_properly else _('Problem') }}</td>
<td>{{ report.version }}</td>
<td>{{ amo.APP_GUIDS[report.app_guid].pretty }} {{ report.app_version }}</td>
<td>{{ report.app_build }}</td>
<td>{{ report.client_os }}</td>
<td>{{ report.created|datetime }}</td>
</tr>
{% if report.comments %}
<tr class="comments" class="{{ cls }}">
<td colspan="6">{{ report.comments }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{{ reports|paginator }}
</div>

<div class="secondary">
<div class="highlight">
<ul>
<li>
<a href="{{ url('compat.reporter_detail', guid)|urlparams(works_properly=1) }}">
{{ ngettext('{0} success report', '{0} success reports',
works['success'])|f(works['success']|numberfmt) }}</a>
</li>
<li>
<a href="{{ url('compat.reporter_detail', guid)|urlparams(works_properly=0) }}">
{{ ngettext('{0} problem report', '{0} problem reports',
works['problem'])|f(works['failure']|numberfmt) }}</a>
</li>
</ul>
</div>
</div>
{% endblock %}
79 changes: 79 additions & 0 deletions apps/compat/tests.py
@@ -0,0 +1,79 @@
import json

from django.conf import settings

import test_utils
from nose.tools import eq_

import amo
from amo.urlresolvers import reverse
from compat.models import CompatReport


# This is the structure sent to /compatibility/incoming from the ACR.
incoming_data = {
'appBuild': '20110429030623',
'appGUID': '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}',
'appVersion': '6.0a1',
'clientOS': 'Intel Mac OS X 10.6',
'comments': 'what the what',
'guid': 'jid0-VsMuA0YYTKCjBh5F0pxHAudnEps@jetpack',
'otherAddons': [['yslow@yahoo-inc.com', '2.1.0']],
'version': '2.2',
'worksProperly': False,
}


class TestIncoming(test_utils.TestCase):

def setUp(self):
self.url = reverse('compat.incoming')
self.data = dict(incoming_data)
self.json = json.dumps(self.data)

def test_success(self):
count = CompatReport.objects.count()
r = self.client.post(self.url, self.json,
content_type='application/json')
eq_(r.status_code, 204)
eq_(CompatReport.objects.count(), count + 1)

cr = CompatReport.objects.order_by('-id')[0]
eq_(cr.app_build, incoming_data['appBuild'])
eq_(cr.app_guid, incoming_data['appGUID'])
eq_(cr.works_properly, incoming_data['worksProperly'])
eq_(cr.comments, incoming_data['comments'])
eq_(cr.client_ip, '127.0.0.1')

# Check that the other_addons field is stored as json.
vals = CompatReport.objects.filter(id=cr.id).values('other_addons')
eq_(vals[0]['other_addons'],
json.dumps(incoming_data['otherAddons'], separators=(',', ':')))

def test_bad_json(self):
r = self.client.post(self.url, 'wuuu#$',
content_type='application/json')
eq_(r.status_code, 400)

def test_bad_field(self):
self.data['save'] = 1
js = json.dumps(self.data)
r = self.client.post(self.url, js, content_type='application/json')
eq_(r.status_code, 400)


class TestCompat(test_utils.TestCase):

def test_success(self):
r = self.client.get(reverse('compat.index'))
eq_(r.status_code, 200)
version = [v['version'] for v in settings.COMPAT
if v['app'] == amo.FIREFOX.id][0]
eq_(r.context['version'], version)


class TestReporter(test_utils.TestCase):

def test_success(self):
r = self.client.get(reverse('compat.reporter'))
eq_(r.status_code, 200)
9 changes: 7 additions & 2 deletions apps/compat/urls.py
@@ -1,9 +1,14 @@
from django.conf.urls.defaults import patterns, url
from django.conf.urls.defaults import url

from . import views


urlpatterns = patterns('',
urlpatterns = (
url('^incoming/?$', views.incoming, name='compat.incoming'),
url('^reporter/?$', views.reporter, name='compat.reporter'),
url('^reporter/([^/]+)$',
views.reporter_detail, name='compat.reporter_detail'),

url('^(?P<version>[.\w]+)?$', views.index, name='compat.index'),
url('^(?P<version>[.\w]+)/details$', views.details, name='compat.details'),
)
81 changes: 78 additions & 3 deletions apps/compat/views.py
@@ -1,10 +1,21 @@
import json
import re

from django import http
from django.conf import settings
from django.db.models import Count
from django.shortcuts import redirect, get_object_or_404
from django.views.decorators.csrf import csrf_exempt

import jingo
import redisutils
from tower import ugettext_lazy as _lazy

import amo.utils
from amo.decorators import post_required
from addons.models import Addon
from .models import CompatReport

KEYS = (
('latest', _lazy('Latest')),
('beta', _lazy('Beta')),
Expand All @@ -14,13 +25,14 @@


def index(request, version=None):
version = version or settings.COMPAT[0]['version']
if version not in [v['version'] for v in settings.COMPAT]:
COMPAT = [v for v in settings.COMPAT if v['app'] == request.APP.id]
if version is None and COMPAT:
version = COMPAT[0]['version']
if version not in [v['version'] for v in COMPAT]:
raise http.Http404()
redis = redisutils.connections['master']
compat = redis.hgetall('compat:%s:%s' % (request.APP.id, version))
versions = dict((k, int(v)) for k, v in compat.items())
print versions
total = sum(versions.values())
keys = [(k, unicode(v)) for k, v in KEYS]
return jingo.render(request, 'compat/index.html',
Expand All @@ -30,3 +42,66 @@ def index(request, version=None):

def details(request, version):
pass


@csrf_exempt
@post_required
def incoming(request):
# Turn camelCase into snake_case.
snake_case = lambda s: re.sub('[A-Z]+', '_\g<0>', s).lower()
try:
data = [(snake_case(k), v)
for k, v in json.loads(request.raw_post_data).items()]
except Exception:
return http.HttpResponseBadRequest()

# Build up a new report.
report = CompatReport(client_ip=request.META.get('REMOTE_ADDR', ''))
fields = CompatReport._meta.get_all_field_names()
for key, value in data:
if key in fields:
setattr(report, key, value)
else:
return http.HttpResponseBadRequest()

report.save()
return http.HttpResponse(status=204)


def reporter(request):
query = request.GET.get('guid')
if query:
qs = Addon.objects.filter(id=query)
if not qs:
qs = Addon.objects.filter(slug=query)
if not qs:
qs = Addon.objects.filter(guid=query)
if not qs and len(query) > 4:
qs = CompatReport.objects.filter(guid__startswith=query)
if qs:
return redirect('compat.reporter_detail', qs[0].guid)
addons = (request.amo_user.addons.all()
if request.user.is_authenticated() else [])
return jingo.render(request, 'compat/reporter.html',
dict(query=query, addons=addons))


def reporter_detail(request, guid):
qs = CompatReport.objects.filter(guid=guid)
if not qs.exists():
raise http.Http404()

works_ = dict(qs.values_list('works_properly').annotate(Count('id')))
works = {'success': works_[True], 'failure': works_[False]}

if 'works_properly' in request.GET:
qs = qs.filter(works_properly=request.GET['works_properly'])
reports = amo.utils.paginate(request, qs.order_by('-created'), 100)

addon = Addon.objects.filter(guid=guid)
name = addon[0].name if addon else guid

return jingo.render(request, 'compat/reporter_detail.html',
dict(reports=reports, works=works,
name=name, guid=guid))

4 changes: 4 additions & 0 deletions migrations/189-compat-reports.sql
@@ -0,0 +1,4 @@
ALTER TABLE compatibility_reports ADD COLUMN `modified` datetime DEFAULT NULL;
CREATE INDEX created_idx ON compatibility_reports (created);
CREATE INDEX guid_created_idx ON compatibility_reports (guid, created);
CREATE INDEX guid_wp_idx ON compatibility_reports (guid, works_properly);

0 comments on commit 7c078ff

Please sign in to comment.