Adds a screen to manage bulk addon validation (bug 651139)

This just sets the initial structure; it does not implement actual addon
validation with celery tasks or any post-validation actions.
This commit is contained in:
Kumar McMillan 2011-04-26 15:07:59 -05:00
Родитель ff446e4ed5
Коммит 6e9fe32969
9 изменённых файлов: 450 добавлений и 4 удалений

81
apps/zadmin/forms.py Normal file
Просмотреть файл

@ -0,0 +1,81 @@
from django import forms
from django.db import connection
import happyforms
from tower import ugettext_lazy as _lazy
import amo
from amo.urlresolvers import reverse
from applications.models import Application, AppVersion
from zadmin.models import ValidationJob, ValidationResult
class BulkValidationForm(happyforms.ModelForm):
application = forms.ChoiceField(
label=_lazy(u'Application'),
choices=[(a.id, a.pretty) for a in amo.APPS_ALL.values()])
curr_max_version = forms.ChoiceField(
label=_lazy(u'Current Max. Version'),
choices=[('', _lazy(u'Select an application first'))])
target_version = forms.ChoiceField(
label=_lazy(u'Target Version'),
choices=[('', _lazy(u'Select an application first'))])
finish_email = forms.CharField(required=False,
label=_lazy(u'Email when finished'))
class Meta:
model = ValidationJob
fields = ('application', 'curr_max_version', 'target_version',
'finish_email')
def __init__(self, *args, **kw):
super(BulkValidationForm, self).__init__(*args, **kw)
w = self.fields['application'].widget
# Get the URL after the urlconf has loaded.
w.attrs['data-url'] = reverse('zadmin.application_versions_json')
def version_choices_for_app_id(self, app_id):
versions = AppVersion.objects.filter(application__id=app_id)
return [(v.id, v.version) for v in versions]
def clean_application(self):
app_id = int(self.cleaned_data['application'])
app = Application.objects.get(pk=app_id)
self.cleaned_data['application'] = app
choices = self.version_choices_for_app_id(app_id)
self.fields['target_version'].choices = choices
self.fields['curr_max_version'].choices = choices
return self.cleaned_data['application']
def _clean_appversion(self, field):
return AppVersion.objects.get(pk=int(field))
def clean_curr_max_version(self):
return self._clean_appversion(self.cleaned_data['curr_max_version'])
def clean_target_version(self):
return self._clean_appversion(self.cleaned_data['target_version'])
def save(self):
job = super(BulkValidationForm, self).save()
sql = """
select files.id
from files
join versions v on v.id=files.version_id
join versions_summary vs on vs.version_id=v.id
where
vs.application_id = %(application_id)s
and vs.max = %(curr_max_version)s
and files.status in %(file_status)s"""
cursor = connection.cursor()
cursor.execute(sql, {'application_id': job.application.id,
'curr_max_version': job.curr_max_version.id,
'file_status': [amo.STATUS_LISTED,
amo.STATUS_PUBLIC]})
for row in cursor:
# TODO(Kumar) queue up the task to validate in the background.
# Task should create validation when complete:
# file_id = row[0]
# fv = FileValidation.from_json(validation)
ValidationResult.objects.create(validation_job=job)
return job

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

@ -1,8 +1,14 @@
from decimal import Decimal
import json
from django.db import models
from django.utils.functional import memoize
import amo
import amo.models
from applications.models import Application, AppVersion
from files.models import FileValidation
_config_cache = {}
@ -34,3 +40,43 @@ def set_config(conf, value):
cf.value = value
cf.save()
_config_cache.clear()
class ValidationJob(amo.models.ModelBase):
application = models.ForeignKey(Application)
curr_max_version = models.ForeignKey(AppVersion,
related_name='validation_current_set')
target_version = models.ForeignKey(AppVersion,
related_name='validation_target_set')
finish_email = models.CharField(max_length=255, null=True)
completed = models.DateTimeField(null=True, db_index=True)
@amo.cached_property
def stats(self):
total = self.result_set.count()
completed = self.result_set.exclude(completed=None).count()
passing = (self.result_set.exclude(completed=None)
.filter(file_validation__errors=0).count())
# TODO(Kumar) count exceptions here?
return {
'number_of_addons': total,
'passing_addons': passing,
'failing_addons': total - passing,
'percent_complete': ((Decimal(total) / Decimal(completed))
* Decimal(100)
if (total and completed) else 0)
}
class Meta:
db_table = 'validation_job'
class ValidationResult(amo.models.ModelBase):
validation_job = models.ForeignKey(ValidationJob,
related_name='result_set')
file_validation = models.ForeignKey(FileValidation, null=True)
task_error = models.TextField(null=True)
completed = models.DateTimeField(null=True, db_index=True)
class Meta:
db_table = 'validation_result'

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

@ -0,0 +1,56 @@
{% extends "admin/base.html" %}
{% set title = _('Bulk Addon Validation') %}
{% block title %}{{ page_title(title) }}{% endblock %}
{% block js %}
{{ super() }}
<script src="{{ media('js/zamboni/admin_validation.js') }}"></script>
{% endblock %}
{% block content %}
<section id="admin-validation">
<h2>{{ title }}</h2>
{{ form.errors }}
<form action="{{ url('zadmin.start_validation') }}" method="post">
{{ csrf() }}
<div class="form-row">
{% for elem in ('application', 'curr_max_version', 'target_version') %}
<label id="label-{{ elem }}" for="id_{{ elem }}">{{ form[elem].label }}</label>
<span id="elem-{{ elem }}">{{ form[elem] }}</span>
{% endfor %}
</div>
<div class="form-row last-row">
<label for="id_finish_email">{{ form['finish_email'].label }}</label>
<span id="elem-finish_email">{{ form['finish_email'] }}</span>
<div class="button">
<button type="submit">{{ _('Start validation') }}</button>
</div>
</div>
</form>
<table>
<tr>
<th>{{ _('Tests Started') }}</th>
<th>{{ _('Tests Finished') }}</th>
<th>{{ _('Current Version') }}</th>
<th>{{ _('Target Version') }}</th>
<th>{{ _('Tested') }}</th>
<th>{{ _('Failing') }}</th>
<th>{{ _('Passing') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
{% for job in validation_jobs %}
<tr>
<td>{{ job.created }}</td>
<td>{{ job.completed }}</td>
<td>{{ job.curr_max_version.version }}</td>
<td>{{ job.target_version.version }}</td>
<td>{{ job.stats['number_of_addons'] }}</td>
<td>{{ job.stats['failing_addons'] }}</td>
<td>{{ job.stats['passing_addons'] }}</td>
<td></td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}

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

@ -1,13 +1,19 @@
from datetime import datetime
import json
from django import test
from nose.tools import eq_
from pyquery import PyQuery as pq
import test_utils
from nose.tools import eq_
import amo
from amo.urlresolvers import reverse
from addons.models import Addon
from files.models import Approval
from versions.models import Version
from applications.models import AppVersion, Application
from files.models import Approval, FileValidation, File
from versions.models import Version, VersionSummary
from zadmin.models import ValidationJob, ValidationResult
class TestFlagged(test_utils.TestCase):
@ -62,6 +68,88 @@ class TestFlagged(test_utils.TestCase):
eq_(addons[0], Addon.objects.get(id=3))
class TestBulkValidation(test_utils.TestCase):
fixtures = ['base/addon_3615', 'base/appversion', 'base/users']
def setUp(self):
assert self.client.login(username='admin@mozilla.com',
password='password')
self.addon = Addon.objects.get(pk=3615)
self.version = self.addon.get_version()
app = Application.objects.get(pk=amo.FIREFOX.id)
# pretend this version supports all Firefox versions:
for av in AppVersion.objects.filter(application=app):
VersionSummary.objects.create(application=app,
version=self.version,
addon=self.addon,
max=av.id)
def appversion(self, version, application=amo.FIREFOX.id):
return AppVersion.objects.get(application=application,
version=version)
def test_start(self):
r = self.client.post(reverse('zadmin.start_validation'),
{'application': amo.FIREFOX.id,
'curr_max_version': self.appversion('3.5.*').id,
'target_version': self.appversion('3.6.*').id,
'finish_email': 'fliggy@mozilla.com'},
follow=True)
if 'form' in r.context:
eq_(r.context['form'].errors.as_text(), '')
self.assertRedirects(r, reverse('zadmin.validation'))
job = ValidationJob.objects.get()
eq_(job.application_id, amo.FIREFOX.id)
eq_(job.curr_max_version.version, '3.5.*')
eq_(job.target_version.version, '3.6.*')
eq_(job.finish_email, 'fliggy@mozilla.com')
eq_(job.completed, None)
eq_(job.result_set.all().count(),
len(self.version.all_files))
def test_grid(self):
kw = dict(application_id=amo.FIREFOX.id,
curr_max_version=self.appversion('3.5.*'),
target_version=self.appversion('3.6.*'))
job = ValidationJob.objects.create(**kw)
for i, res in enumerate((dict(errors=0), dict(errors=1))):
f = File.objects.create(version=self.version,
filename='file-%s' % i,
platform_id=amo.PLATFORM_ALL.id,
status=amo.STATUS_PUBLIC)
kw = dict(file=f,
validation='{}',
errors=0,
warnings=0,
notices=0)
kw.update(res)
res['valid'] = kw['errors'] == 0
fv = FileValidation.objects.create(**kw)
ValidationResult.objects.create(file_validation=fv,
validation_job=job,
task_error=None,
completed=datetime.now())
r = self.client.get(reverse('zadmin.validation'))
eq_(r.status_code, 200)
doc = pq(r.content)
eq_(doc('table tr td').eq(2).text(), '3.5.*')
eq_(doc('table tr td').eq(3).text(), '3.6.*')
eq_(doc('table tr td').eq(4).text(), '2')
eq_(doc('table tr td').eq(5).text(), '1')
eq_(doc('table tr td').eq(6).text(), '1')
def test_application_versions_json(self):
r = self.client.post(reverse('zadmin.application_versions_json'),
{'application_id': amo.FIREFOX.id})
eq_(r.status_code, 200)
data = json.loads(r.content)
empty = True
for id, ver in data['choices']:
empty = False
eq_(AppVersion.objects.get(pk=id).version, ver)
assert not empty, "Unexpected: %r" % data
def test_settings():
# Are you there, settings page?
response = test.Client().get(reverse('zadmin.settings'), follow=True)

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

@ -14,6 +14,12 @@ urlpatterns = patterns('',
url('^hera', views.hera, name='zadmin.hera'),
url('^settings', views.settings, name='zadmin.settings'),
url('^fix-disabled', views.fix_disabled_file, name='zadmin.fix-disabled'),
url(r'^validation/application_versions\.json$',
views.application_versions_json,
name='zadmin.application_versions_json'),
url(r'^validation/start$', views.start_validation,
name='zadmin.start_validation'),
url(r'^validation$', views.validation, name='zadmin.validation'),
# The Django admin.
url('^models/', include(admin.site.urls)),

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

@ -14,10 +14,14 @@ import jinja2
import jingo
from amo import messages
from amo.decorators import login_required, json_view, post_required
import amo.models
from amo.urlresolvers import reverse
from addons.models import Addon
from files.models import Approval, File
from versions.models import Version
from zadmin.forms import BulkValidationForm
from zadmin.models import ValidationJob
log = commonware.log.getLogger('z.zadmin')
@ -131,3 +135,31 @@ def fix_disabled_file(request):
return jingo.render(request, 'zadmin/fix-disabled.html',
{'file': file_,
'file_id': request.POST.get('file', '')})
@login_required
@post_required
@json_view
def application_versions_json(request):
app_id = request.POST['application_id']
f = BulkValidationForm()
return {'choices': f.version_choices_for_app_id(app_id)}
@admin.site.admin_view
def validation(request, form=None):
if not form:
form = BulkValidationForm()
jobs = ValidationJob.objects.filter(completed=None).order_by('-created')
return jingo.render(request, 'zadmin/validation.html',
{'form': form, 'validation_jobs': jobs})
@admin.site.admin_view
def start_validation(request):
form = BulkValidationForm(request.POST)
if form.is_valid():
form.save()
return redirect(reverse('zadmin.validation'))
else:
return validation(request, form=form)

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

@ -38,3 +38,54 @@ fieldset {
.footerlogo {
display: none;
}
#admin-validation form label,
#admin-validation form span {
display: block;
float: left;
}
#admin-validation form label {
padding: 0 1em;
width: 10em;
}
#admin-validation form #label-application {
width: 5em;
}
#admin-validation form #elem-application {
width: 8em;
}
#admin-validation form #label-target_version {
width: 7em;
}
#admin-validation form #elem-finish_email,
#admin-validation form #elem-finish_email input {
width: 13em;
}
#admin-validation form span {
width: 14em;
}
#admin-validation form .form-row {
clear: both;
height: 2.5em;
}
#admin-validation form .last-row {
padding-left: 15em;
}
#admin-validation form .button {
float: left;
padding-left: 10em;
}
#admin-validation .errorlist li {
list-style: disc;
margin-left: 2em;
}
#admin-validation .errorlist li li {
list-style: none;
margin-left: 1em;
}
#admin-validation .errorlist ul {
margin-bottom: 1em;
}
#admin-validation .errorlist ul ul {
margin: 0;
}

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

@ -0,0 +1,43 @@
(function() {
"use strict";
$(function() {
if ($('#admin-validation').length) {
initAdminValidation($('#admin-validation'));
}
});
function initAdminValidation(doc) {
var $elem = $('#id_application', doc);
$elem.change(function(e) {
var maxVer = $('#id_curr_max_version, #id_target_version', doc),
sel = $(e.target),
appId = $('option:selected', sel).val();
if (!appId) {
$('option', maxVer).remove();
maxVer.append(format('<option value="{0}">{1}</option>',
['', gettext('Select an application first')]));
return;
}
$.post(sel.attr('data-url'), {'application_id': appId}, function(d) {
$('option', maxVer).remove();
$.each(d.choices, function(i, ch) {
maxVer.append(format('<option value="{0}">{1}</option>',
[ch[0], ch[1]]));
});
});
});
if ($elem.children('option:selected').val()
&& !$('#id_curr_max_version option:selected, ' +
'#id_target_version option:selected', doc).val()) {
// If an app is selected when page loads and it's not a form post.
$elem.trigger('change');
}
}
})();

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

@ -0,0 +1,43 @@
CREATE TABLE `validation_job` (
`id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`application_id` int(11) unsigned NOT NULL,
`curr_max_version_id` int(11) unsigned NOT NULL,
`target_version_id` int(11) unsigned NOT NULL,
`finish_email` varchar(255),
`completed` datetime
)
;
ALTER TABLE `validation_job`
ADD CONSTRAINT `application_id_refs_id_e6541345`
FOREIGN KEY (`application_id`) REFERENCES `applications` (`id`);
ALTER TABLE `validation_job`
ADD CONSTRAINT `curr_max_version_id_refs_id_c959f479`
FOREIGN KEY (`curr_max_version_id`) REFERENCES `appversions` (`id`);
ALTER TABLE `validation_job`
ADD CONSTRAINT `target_version_id_refs_id_c959f479`
FOREIGN KEY (`target_version_id`) REFERENCES `appversions` (`id`);
CREATE TABLE `validation_result` (
`id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
`validation_job_id` int(11) unsigned NOT NULL,
`file_validation_id` int(11) unsigned,
`task_error` longtext,
`completed` datetime
)
;
ALTER TABLE `validation_result`
ADD CONSTRAINT `validation_job_id_refs_id_3b0311f8`
FOREIGN KEY (`validation_job_id`) REFERENCES `validation_job` (`id`);
ALTER TABLE `validation_result`
ADD CONSTRAINT `file_validation_id_refs_id_36081e0`
FOREIGN KEY (`file_validation_id`) REFERENCES `file_validation` (`id`);
CREATE INDEX `validation_job_398529ef` ON `validation_job` (`application_id`);
CREATE INDEX `validation_job_cc1f3b9a` ON `validation_job` (`curr_max_version_id`);
CREATE INDEX `validation_job_1cf8b594` ON `validation_job` (`target_version_id`);
CREATE INDEX `validation_job_e490d511` ON `validation_job` (`completed`);
CREATE INDEX `validation_result_61162f45` ON `validation_result` (`validation_job_id`);
CREATE INDEX `validation_result_4878d95` ON `validation_result` (`file_validation_id`);
CREATE INDEX `validation_result_e490d511` ON `validation_result` (`completed`);