Refactored draft submission checks so that new checkers can be slotted in through a configuration in settings.py. Refactored the calling of idnits to use the new API, and added a pyang validation check.

- Legacy-Id: 10894
This commit is contained in:
Henrik Levkowetz 2016-03-04 20:08:29 +00:00
parent 1c8a171703
commit 76bb233b70
18 changed files with 607 additions and 62 deletions

View file

@ -2,6 +2,7 @@ import os
from django.conf import settings
from django.core import checks
from django.utils.module_loading import import_string
@checks.register('directories')
def check_cdn_directory_exists(app_configs, **kwargs):
@ -98,3 +99,46 @@ def check_id_submission_files(app_configs, **kwargs):
id = "datatracker.E0007",
))
return errors
@checks.register('submission-checkers')
def check_id_submission_checkers(app_configs, **kwargs):
errors = []
for checker_path in settings.IDSUBMIT_CHECKER_CLASSES:
try:
checker_class = import_string(checker_path)
except Exception as e:
errors.append(checks.Critical(
"An exception was raised when trying to import the draft submission"
"checker class '%s':\n %s" % (checker_path, e),
hint = "Please check that the class exists and can be imported.",
id = "datatracker.E0008",
))
try:
checker = checker_class()
except Exception as e:
errors.append(checks.Critical(
"An exception was raised when trying to instantiate the draft submission"
"checker class '%s': %s" % (checker_path, e),
hint = "Please check that the class can be instantiated.",
id = "datatracker.E0009",
))
continue
for attr in ('name',):
if not hasattr(checker, attr):
errors.append(checks.Critical(
"The draft submission checker '%s' has no attribute '%s', which is required" % (checker_path, attr),
hint = "Please update the class.",
id = "datatracker.E0010",
))
checker_methods = ("check_file_txt", "check_file_xml", "check_fragment_txt", "check_fragment_xml", )
for method in checker_methods:
if hasattr(checker, method):
break
else:
errors.append(checks.Critical(
"The draft submission checker '%s' has no recognised checker method; "
"should be one or more of %s." % (checker_path, checker_methods),
hint = "Please update the class.",
id = "datatracker.E0011",
))
return errors

View file

@ -472,10 +472,16 @@ IDSUBMIT_REPOSITORY_PATH = INTERNET_DRAFT_PATH
IDSUBMIT_STAGING_PATH = '/a/www/www6s/staging/'
IDSUBMIT_STAGING_URL = '//www.ietf.org/staging/'
IDSUBMIT_IDNITS_BINARY = '/a/www/ietf-datatracker/scripts/idnits'
IDSUBMIT_PYANG_COMMAND = 'pyang -p %(workdir)s --verbose --ietf %(model)s'
IDSUBMIT_CHECKER_CLASSES = (
"ietf.submit.checkers.DraftIdnitsChecker",
"ietf.submit.checkers.DraftYangChecker",
)
IDSUBMIT_MANUAL_STAGING_DIR = '/tmp/'
IDSUBMIT_FILE_TYPES = (
'txt',
'xml',

View file

@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse as urlreverse
from django.contrib import admin
from ietf.submit.models import Preapproval, Submission
from ietf.submit.models import Preapproval, Submission, SubmissionCheck
class SubmissionAdmin(admin.ModelAdmin):
list_display = ['id', 'draft_link', 'status_link', 'submission_date',]
@ -23,9 +23,13 @@ class SubmissionAdmin(admin.ModelAdmin):
else:
return instance.name
draft_link.allow_tags = True
admin.site.register(Submission, SubmissionAdmin)
class SubmissionCheckAdmin(admin.ModelAdmin):
list_display = ['submission', 'time', 'checker', 'passed', 'errors', 'warnings', 'items']
raw_id_fields = ['submission']
admin.site.register(SubmissionCheck, SubmissionCheckAdmin)
class PreapprovalAdmin(admin.ModelAdmin):
pass
admin.site.register(Preapproval, PreapprovalAdmin)

171
ietf/submit/checkers.py Normal file
View file

@ -0,0 +1,171 @@
# Copyright The IETF Trust 2016, All Rights Reserved
import os
import re
from xym import xym
import shutil
import tempfile
from django.conf import settings
import debug # pyflakes:ignore
from ietf.utils.pipe import pipe
from ietf.utils.log import log
class DraftSubmissionChecker():
name = ""
def check_file_txt(self, text):
"Run checks on a text file"
raise NotImplementedError
def check_file_xml(self, xml):
"Run checks on an xml file"
raise NotImplementedError
def check_fragment_txt(self, text):
"Run checks on a fragment from a text file"
raise NotImplementedError
def check_fragment_xml(self, xml):
"Run checks on a fragment from an xml file"
raise NotImplementedError
class DraftIdnitsChecker(object):
"""
Draft checker class for idnits. Idnits can only handle whole text files,
so only check_file_txt() is defined; check_file_xml and check_fragment_*
methods are undefined.
Furthermore, idnits doesn't provide an error code or line-by-line errors,
so a bit of massage is needed in order to return the expected failure flag.
"""
name = "idnits check"
def check_file_txt(self, path):
"""
Run an idnits check, and return a passed/failed indication, a message,
and error and warning messages.
Error and warning list items are tuples:
(line_number, line_text, message)
"""
filename = os.path.basename(path)
result = {}
items = []
errors = 0
warnings = 0
errstart = [' ** ', ' ~~ ']
warnstart = [' == ', ' -- ']
cmd = "%s --submitcheck --nitcount %s" % (settings.IDSUBMIT_IDNITS_BINARY, path)
code, out, err = pipe(cmd)
if code != 0 or out == "":
message = "idnits error: %s:\n Error %s: %s" %( cmd, code, err)
log(message)
passed = False
else:
message = out
if re.search("\s+Summary:\s+0\s+|No nits found", out):
passed = True
else:
passed = False
item = None
for line in message.splitlines():
if line[:5] in (errstart + warnstart):
item = line.rstrip()
elif line.strip() == "" and item:
tuple = (None, None, item)
items.append(tuple)
if item[:5] in errstart:
errors += 1
elif item[:5] in warnstart:
warnings += 1
else:
raise RuntimeError("Unexpected state in idnits checker: item: %s, line: %s" % (item, line))
item = None
elif item and line.strip() != "":
item += " " + line.strip()
else:
pass
result[filename] = {
"passed": passed,
"message": message,
"errors": errors,
"warnings":warnings,
"items": items,
}
return passed, message, errors, warnings, result
class DraftYangChecker(object):
name = "yang validation"
def check_file_txt(self, path):
name = os.path.basename(path)
workdir = tempfile.mkdtemp()
results = {}
extractor = xym.YangModuleExtractor(path, workdir, strict=True, debug_level = 0)
with open(path) as file:
try:
# This places the yang models as files in workdir
extractor.extract_yang_model(file.readlines())
model_list = extractor.get_extracted_models()
except Exception as exc:
passed = False
message = exc
errors = [ (name, None, None, exc) ]
warnings = []
return passed, message, errors, warnings
for model in model_list:
path = os.path.join(workdir, model)
with open(path) as file:
text = file.readlines()
cmd = settings.IDSUBMIT_PYANG_COMMAND % {"workdir": workdir, "model": path, }
code, out, err = pipe(cmd)
errors = 0
warnings = 0
items = []
if code > 0:
error_lines = err.splitlines()
for line in error_lines:
fn, lnum, msg = line.split(':', 2)
lnum = int(lnum)
line = text[lnum-1].rstrip()
items.append((lnum, line, msg))
if 'error: ' in msg:
errors += 1
if 'warning: ' in msg:
warnings += 1
results[model] = {
"passed": code == 0,
"message": out+"No validation errors\n" if code == 0 else err,
"warnings": warnings,
"errors": errors,
"items": items,
}
shutil.rmtree(workdir)
## For now, never fail because of failed yang validation.
if len(model_list):
passed = True
else:
passed = None
#passed = all( res["passed"] for res in results.values() )
message = "\n\n".join([ "\n".join([model+':', res["message"]]) for model, res in results.items() ])
errors = sum(res["errors"] for res in results.values() )
warnings = sum(res["warnings"] for res in results.values() )
items = [ e for res in results.values() for e in res["items"] ]
return passed, message, errors, warnings, items

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import jsonfield
class Migration(migrations.Migration):
dependencies = [
('submit', '0003_auto_20150713_1104'),
]
operations = [
migrations.CreateModel(
name='SubmissionCheck',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('time', models.DateTimeField(default=None, auto_now=True)),
('checker', models.CharField(max_length=256, blank=True)),
('passed', models.NullBooleanField(default=False)),
('message', models.TextField(null=True, blank=True)),
('warnings', models.IntegerField(null=True, blank=True, default=None)),
('errors', models.IntegerField(null=True, blank=True, default=None)),
('items', jsonfield.JSONField(null=True, blank=True, default=b'{}')),
('submission', models.ForeignKey(related_name='checks', to='submit.Submission')),
],
options={
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from django.db import migrations
def convert_to_submission_check(apps, schema_editor):
Submission = apps.get_model('submit','Submission')
SubmissionCheck = apps.get_model('submit','SubmissionCheck')
for s in Submission.objects.all():
passed = re.search('\s+Summary:\s+0\s+|No nits found', s.idnits_message) != None
c = SubmissionCheck(submission=s, checker='idnits check', passed=passed, message=s.idnits_message)
c.save()
def convert_from_submission_check(apps, schema_editor):
SubmissionCheck = apps.get_model('submit','SubmissionCheck')
for c in SubmissionCheck.objects.filter(checker='idnits check'):
c.submission.idnits_message = c.message
c.save()
pass
class Migration(migrations.Migration):
dependencies = [
('submit', '0004_submissioncheck'),
]
operations = [
migrations.RunPython(convert_to_submission_check, convert_from_submission_check)
]

View file

@ -2,6 +2,7 @@ import re
import datetime
from django.db import models
import jsonfield
from ietf.doc.models import Document
from ietf.person.models import Person
@ -66,6 +67,23 @@ class Submission(models.Model):
def existing_document(self):
return Document.objects.filter(name=self.name).first()
class SubmissionCheck(models.Model):
time = models.DateTimeField(auto_now=True, default=None) # The default is to make makemigrations happy
submission = models.ForeignKey(Submission, related_name='checks')
checker = models.CharField(max_length=256, blank=True)
passed = models.NullBooleanField(default=False)
message = models.TextField(null=True, blank=True)
errors = models.IntegerField(null=True, blank=True, default=None)
warnings = models.IntegerField(null=True, blank=True, default=None)
items = jsonfield.JSONField(null=True, blank=True, default='{}')
#
def __unicode__(self):
return "%s submission check: %s: %s" % (self.checker, 'Passed' if self.passed else 'Failed', self.message[:48]+'...')
def has_warnings(self):
return self.warnings != '[]'
def has_errors(self):
return self.errors != '[]'
class SubmissionEvent(models.Model):
submission = models.ForeignKey(Submission)
time = models.DateTimeField(default=datetime.datetime.now)

View file

@ -1,6 +1,6 @@
# Autogenerated by the mkresources management command 2014-11-13 23:53
from tastypie.resources import ModelResource
from tastypie.fields import ToOneField
from tastypie.fields import ToOneField, ToManyField
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from ietf import api
@ -28,6 +28,7 @@ from ietf.name.resources import DraftSubmissionStateNameResource
class SubmissionResource(ModelResource):
state = ToOneField(DraftSubmissionStateNameResource, 'state')
group = ToOneField(GroupResource, 'group', null=True)
checks = ToManyField(SubmissionCheck, 'checks', null=True)
class Meta:
queryset = Submission.objects.all()
serializer = api.Serializer()
@ -51,7 +52,6 @@ class SubmissionResource(ModelResource):
"document_date": ALL,
"submission_date": ALL,
"submitter": ALL,
"idnits_message": ALL,
"state": ALL_WITH_RELATIONS,
"group": ALL_WITH_RELATIONS,
}
@ -74,3 +74,19 @@ class SubmissionEventResource(ModelResource):
}
api.submit.register(SubmissionEventResource())
class SubmissionCheckResource(ModelResource):
submission = ToOneField(SubmissionResource, 'submission')
class Meta:
queryset = SubmissionCheck.objects.all()
serializer = api.Serializer()
#resource_name = 'submissioncheck'
filtering = {
"id": ALL,
"checker": ALL,
"passed": ALL,
"warning": ALL,
"message": ALL,
"errors": ALL,
"submission": ALL_WITH_RELATIONS,
}
api.submit.register(SubmissionCheckResource())

View file

@ -65,15 +65,115 @@ Table of Contents
3. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 2
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 2
1. Introduction
1. Introduction
This document describes a protocol for testing tests.
2. Security Considerations
2. Yang
<CODE BEGINS> file "ietf-mpls@2015-10-16.yang"
module ietf-mpls {
namespace "urn:ietf:params:xml:ns:yang:ietf-mpls";
prefix "mpls";
import ietf-routing {
prefix "rt";
}
import ietf-interfaces {
prefix "if";
}
organization "TBD";
contact "TBD";
description
"This YANG module defines the essential components for the
management of the MPLS subsystem.";
revision "2015-10-16" {
description
"Initial revision";
reference "RFC 3031: A YANG Data Model for base MPLS";
}
typedef mpls-label {
type uint32 {
range "0..1048575";
}
description
"The MPLS label range";
}
typedef percent {
type uint16 {
range "0 .. 100";
}
description "Percentage";
}
grouping interface-mpls {
description "MPLS interface properties grouping";
leaf enabled {
type boolean;
description
"'true' if mpls encapsulation is enabled on the
interface. 'false' if mpls encapsulation is enabled
on the interface.";
}
}
augment "/rt:routing/rt:routing-instance" {
description "MPLS augmentation.";
container mpls {
description
"MPLS container, to be used as an augmentation target node
other MPLS sub-features config, e.g. MPLS static LSP, MPLS
LDP LSPs, and Trafic Engineering MPLS LSP Tunnels, etc.";
list interface {
key "name";
description "List of MPLS interfaces";
leaf name {
type if:interface-ref;
description
"The name of a configured MPLS interface";
}
container config {
description "Holds intended configuration";
uses interface-mpls;
}
container state {
config false;
description "Holds inuse configuration";
uses interface-mpls;
}
}
}
}
augment "/rt:routing-state/rt:routing-instance" {
description "MPLS augmentation.";
container mpls {
config false;
description
"MPLS container, to be used as an augmentation target node
other MPLS sub-features state";
}
}
}
<CODE ENDS>
3. Security Considerations
There are none.
3. IANA Considerations
4. IANA Considerations
No new registrations for IANA.

View file

@ -20,7 +20,7 @@
<workgroup>%(group)s</workgroup>
<abstract>
<t>
This document describes how to test tests.
This document describes how to test tests.
</t>
</abstract>
</front>
@ -28,17 +28,123 @@
<middle>
<section title="Introduction">
<t>
This document describes a protocol for testing tests.
This document describes a protocol for testing tests.
</t>
</section>
<section title="Yang">
<figure>
<artwork>
<![CDATA[
<CODE BEGINS> file "ietf-mpls@2015-10-16.yang"
module ietf-mpls {
namespace "urn:ietf:params:xml:ns:yang:ietf-mpls";
prefix "mpls";
import ietf-routing {
prefix "rt";
}
import ietf-interfaces {
prefix "if";
}
organization "TBD";
contact "TBD";
description
"This YANG module defines the essential components for the
management of the MPLS subsystem.";
revision "2015-10-16" {
description
"Initial revision";
reference "RFC 3031: A YANG Data Model for base MPLS";
}
typedef mpls-label {
type uint32 {
range "0..1048575";
}
description
"The MPLS label range";
}
typedef percent {
type uint16 {
range "0 .. 100";
}
description "Percentage";
}
grouping interface-mpls {
description "MPLS interface properties grouping";
leaf enabled {
type boolean;
description
"'true' if mpls encapsulation is enabled on the
interface. 'false' if mpls encapsulation is enabled
on the interface.";
}
}
augment "/rt:routing/rt:routing-instance" {
description "MPLS augmentation.";
container mpls {
description
"MPLS container, to be used as an augmentation target node
other MPLS sub-features config, e.g. MPLS static LSP, MPLS
LDP LSPs, and Trafic Engineering MPLS LSP Tunnels, etc.";
list interface {
key "name";
description "List of MPLS interfaces";
leaf name {
type if:interface-ref;
description
"The name of a configured MPLS interface";
}
container config {
description "Holds intended configuration";
uses interface-mpls;
}
container state {
config false;
description "Holds inuse configuration";
uses interface-mpls;
}
}
}
}
augment "/rt:routing-state/rt:routing-instance" {
description "MPLS augmentation.";
container mpls {
config false;
description
"MPLS container, to be used as an augmentation target node
other MPLS sub-features state";
}
}
}
<CODE ENDS>
]]>
</artwork>
</figure>
</section>
<section anchor="Security" title="Security Considerations">
<t>
There are none.
There are none.
</t>
</section>
<section anchor="IANA" title="IANA Considerations">
<t>
No new registrations for IANA.
No new registrations for IANA.
</t>
</section>
</middle>

View file

@ -1,7 +1,6 @@
import datetime
import os
import shutil
import re
from django.conf import settings
@ -98,7 +97,7 @@ class SubmitTests(TestCase):
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, u"%s-%s.%s" % (name, rev, format))))
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
submission = Submission.objects.get(name=name)
self.assertTrue(re.search('\s+Summary:\s+0\s+errors|No nits found', submission.idnits_message))
self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ]))
self.assertEqual(len(submission.authors_parsed()), 1)
author = submission.authors_parsed()[0]
self.assertEqual(author["name"], "Author Name")

View file

@ -1,5 +1,4 @@
import os
import re
import datetime
from django.conf import settings
@ -18,23 +17,6 @@ from ietf.submit.mail import announce_to_lists, announce_new_version, announce_t
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName
from ietf.utils import unaccent
from ietf.utils.log import log
from ietf.utils.pipe import pipe
def check_idnits(path):
#p = subprocess.Popen([self.idnits, '--submitcheck', '--nitcount', path], stdout=subprocess.PIPE)
cmd = "%s --submitcheck --nitcount %s" % (settings.IDSUBMIT_IDNITS_BINARY, path)
code, out, err = pipe(cmd)
if code != 0:
log("idnits error: %s:\n Error %s: %s" %( cmd, code, err))
return out
def found_idnits(idnits_message):
if not idnits_message:
return False
success_re = re.compile('\s+Summary:\s+0\s+|No nits found')
if success_re.search(idnits_message):
return True
return False
def validate_submission(submission):
errors = {}

View file

@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse as urlreverse
from django.core.validators import validate_email, ValidationError
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.module_loading import import_string
import debug # pyflakes:ignore
@ -17,9 +18,9 @@ from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
from ietf.submit.forms import SubmissionUploadForm, NameEmailForm, EditSubmissionForm, PreapprovalForm, ReplacesForm
from ietf.submit.mail import send_full_url, send_approval_request_to_group, send_submission_confirmation, send_manual_post_request
from ietf.submit.models import Submission, Preapproval, DraftSubmissionStateName
from ietf.submit.models import Submission, SubmissionCheck, Preapproval, DraftSubmissionStateName
from ietf.submit.utils import approvable_submissions_for_user, preapprovals_for_user, recently_approved_by_user
from ietf.submit.utils import check_idnits, found_idnits, validate_submission, create_submission_event
from ietf.submit.utils import validate_submission, create_submission_event
from ietf.submit.utils import post_submission, cancel_submission, rename_submission_files
from ietf.utils.accesstoken import generate_random_key, generate_access_token
from ietf.utils.draft import Draft
@ -92,9 +93,6 @@ def upload_submission(request):
else:
abstract = form.parsed_draft.get_abstract()
# check idnits
idnits_message = check_idnits(file_name['txt'])
# save submission
try:
submission = Submission.objects.create(
@ -114,12 +112,28 @@ def upload_submission(request):
submission_date=datetime.date.today(),
document_date=form.parsed_draft.get_creation_date(),
replaces="",
idnits_message=idnits_message,
)
except Exception as e:
log("Exception: %s\n" % e)
raise
# run submission checkers
def apply_check(submission, checker, method, fn):
func = getattr(checker, method)
passed, message, errors, warnings, items = func(fn)
check = SubmissionCheck(submission=submission, checker=checker.name, passed=passed, message=message, errors=errors, warnings=warnings, items=items)
check.save()
for checker_path in settings.IDSUBMIT_CHECKER_CLASSES:
checker_class = import_string(checker_path)
checker = checker_class()
# ordered list of methods to try
for method in ("check_fragment_xml", "check_file_xml", "check_fragment_txt", "check_file_txt", ):
ext = method[-3:]
if hasattr(checker, method) and ext in file_name:
apply_check(submission, checker, method, file_name[ext])
break
create_submission_event(request, submission, desc="Uploaded submission")
return redirect("submit_submission_status_by_hash", submission_id=submission.pk, access_token=submission.access_token())
@ -175,7 +189,7 @@ def submission_status(request, submission_id, access_token=None):
raise Http404
errors = validate_submission(submission)
passes_idnits = found_idnits(submission.idnits_message)
passes_checks = all([ c.passed!=False for c in submission.checks.all() ])
is_secretariat = has_role(request.user, "Secretariat")
is_chair = submission.group and submission.group.has_role(request.user, "chair")
@ -316,7 +330,7 @@ def submission_status(request, submission_id, access_token=None):
'selected': 'status',
'submission': submission,
'errors': errors,
'passes_idnits': passes_idnits,
'passes_checks': passes_checks,
'submitter_form': submitter_form,
'replaces_form': replaces_form,
'message': message,

View file

@ -1 +1,3 @@
<div class='alert alert-warning'>
{% for item in help_text_and_errors %} {{ item }}<br> {% endfor %}
</div>

View file

@ -37,32 +37,52 @@
<p class="alert alert-danger">Please fix errors in the form below.</p>
{% endif %}
<h2>I-D nits</h2>
<h2>Submission checks</h2>
<p>
{% if passes_idnits %}
Your draft has been verified to meet I-D nits requirements.
{% if passes_checks %}
Your draft has been verified to pass the submission checks.
{% else %}
Your draft has <b>NOT</b> been verified to meet I-D nits requirements.
Your draft has <b>NOT</b> been verified to pass the submission checks.
{% endif %}
</p>
<button class="btn btn-default" data-toggle="modal" data-target="#nits">View I-D nits</button>
{% for check in submission.checks.all %}
{% if check.errors %}
<p class="alert alert-warning">
The {{check.checker}} returned {{ check.errors }} error{{ check.errors|pluralize }}
and {{ check.warnings }} warning
<div class="modal fade" id="nits" tabindex="-1" role="dialog" aria-labelledby="nits" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="nitslabel">I-D nits for {{ submission.name }}-{{ submission.rev }}</h4>
</div>
<div class="modal-body">
<pre>{{ submission.idnits_message }}</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
{{ check.warnings|pluralize }} ; click the button
below to see details. Please fix those, and resubmit.
</p>
{% elif check.warnings %}
<p class="alert alert-warning">
The {{check.checker}} returned {{ check.warnings }} warning{{ check.warnings|pluralize }}.
</p>
{% endif %}
{% endfor %}
{% for check in submission.checks.all %}
{% if check.passed != None %}
<button class="btn btn-{% if check.passed %}{% if check.warnings %}warning{% elif check.errors %}warning{% else %}success{% endif %}{% else %}danger{% endif %}" data-toggle="modal" data-target="#check-{{check.pk}}">View {{ check.checker }}</button>
<div class="modal fade" id="check-{{check.pk}}" tabindex="-1" role="dialog" aria-labelledby="check-{{check.pk}}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="nitslabel">{{ check.checker|title }} for {{ submission.name }}-{{ submission.rev }}</h4>
</div>
<div class="modal-body">
<pre>{{ check.message }}</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
<div class="modal fade" id="twopages" tabindex="-1" role="dialog" aria-labelledby="twopageslabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
@ -218,7 +238,7 @@
</form>
<p>Leads to manual post by the secretariat.</p>
{% if passes_idnits and not errors %}
{% if passes_checks and not errors %}
<h2>Please edit the following meta-data before posting:</h2>
<form class="idsubmit" method="post">

View file

@ -50,6 +50,7 @@
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}

View file

@ -16,6 +16,7 @@ class AsyncCoreLoopThread(object):
kwargs={'exit_condition':self.exit_condition,'timeout':1.0}
self.thread = threading.Thread(target=self.wrap_loop, kwargs=kwargs)
self.thread.daemon = True
self.thread.daemon = True
self.thread.start()
def stop(self):

View file

@ -295,4 +295,3 @@ class TestCase(django.test.TestCase):
self.assertTrue(resp['Content-Type'].startswith('text/html'))
self.assertValidHTML(resp.content)