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:
Родитель
ff446e4ed5
Коммит
6e9fe32969
|
@ -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`);
|
Загрузка…
Ссылка в новой задаче