Commit 8e2f5ad6 authored by Nicolas Dandrimont's avatar Nicolas Dandrimont 🤔
Browse files

Merge branch 'qa-rework' into devel

parents 65c09271 8694e84d
...@@ -89,6 +89,15 @@ class Changes(object): ...@@ -89,6 +89,15 @@ class Changes(object):
""" """
return self._data[key] return self._data[key]
def __contains__(self, key):
"""
Returns whether the specified RFC822 key exists.
``key``
Key of data to check for existence.
"""
return key in self._data
def get(self, key, default=None): def get(self, key, default=None):
""" """
Returns the value of the rfc822 key specified, but defaults Returns the value of the rfc822 key specified, but defaults
......
...@@ -36,12 +36,14 @@ __author__ = 'Jonny Lamb' ...@@ -36,12 +36,14 @@ __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb, Copyright © 2010 Jan Dittberner' __copyright__ = 'Copyright © 2008 Jonny Lamb, Copyright © 2010 Jan Dittberner'
__license__ = 'MIT' __license__ = 'MIT'
from debian import deb822
import logging import logging
import os import os
import tempfile
import shutil import shutil
import sys import sys
import tempfile
import traceback
from debian import deb822
import pylons import pylons
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -203,13 +205,15 @@ class Plugins(object): ...@@ -203,13 +205,15 @@ class Plugins(object):
# The 'plugin' object points to the class containing the actual plugin/test # The 'plugin' object points to the class containing the actual plugin/test
if hasattr(module, 'plugin'): if hasattr(module, 'plugin'):
p = getattr(module, 'plugin')(name=plugin, changes=self.changes, \ p = getattr(module, 'plugin')(name=plugin, changes=self.changes, \
changes_file=self.changes_file, tempdir=self.tempdir, \ changes_file=self.changes_file, tempdir=self.tempdir)
outcomes=getattr(module, 'outcomes'))
for item in self.kw: for item in self.kw:
setattr(p, item, self.kw[item]) setattr(p, item, self.kw[item])
result.extend(p.run()) try:
result.extend(p.run())
except Exception:
log.debug("Something wrong happened while running the plugin '%s': %s" % (plugin, traceback.format_exc()))
if self.conf['extract']: if self.conf['extract']:
self._cleanup() self._cleanup()
......
...@@ -35,12 +35,26 @@ __author__ = 'Jonny Lamb' ...@@ -35,12 +35,26 @@ __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = 'Copyright © 2008 Jonny Lamb'
__license__ = 'MIT' __license__ = 'MIT'
import json
import os
from mako.lookup import TemplateLookup
from mako.exceptions import TopLevelLookupException
from pylons import config
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
import debexpo.lib
from debexpo.model import meta, OrmObject from debexpo.model import meta, OrmObject
from debexpo.model.package_versions import PackageVersion from debexpo.model.package_versions import PackageVersion
# templates are in [...]/templates/plugins
PLUGINS_TEMPLATE_DIRS = [os.path.join(path, "plugins")
for path in config["pylons.paths"]["templates"]]
t_package_info = sa.Table('package_info', meta.metadata, t_package_info = sa.Table('package_info', meta.metadata,
sa.Column('id', sa.types.Integer, primary_key=True), sa.Column('id', sa.types.Integer, primary_key=True),
sa.Column('package_version_id', sa.types.Integer, sa.ForeignKey('package_versions.id')), sa.Column('package_version_id', sa.types.Integer, sa.ForeignKey('package_versions.id')),
...@@ -53,6 +67,46 @@ t_package_info = sa.Table('package_info', meta.metadata, ...@@ -53,6 +67,46 @@ t_package_info = sa.Table('package_info', meta.metadata,
class PackageInfo(OrmObject): class PackageInfo(OrmObject):
foreign = ['package_version'] foreign = ['package_version']
@property
def rich_data(self):
try:
return json.loads(self.data)
except ValueError:
return self.data
@rich_data.setter
def rich_data(self, value):
self.data = json.dumps(value)
def render(self, render_format):
"""Render the plugin data to the given format"""
# Files to try out for plugin data rendering
try_files = [
"%s/%s.mako" % (self.from_plugin, render_format),
"%s/text.mako" % (self.from_plugin),
"default/%s.mako" % render_format,
"default/text.mako",
]
lookup = TemplateLookup(directories = PLUGINS_TEMPLATE_DIRS)
for basefile in try_files:
try:
template = lookup.get_template(basefile)
except TopLevelLookupException:
continue
else:
break
else:
# No template file found, something weird happened
return "%s (!! no template found)" % self.data
rendered_data = template.render(o = self, h = debexpo.lib.helpers)
return rendered_data
orm.mapper(PackageInfo, t_package_info, properties={ orm.mapper(PackageInfo, t_package_info, properties={
'package_version' : orm.relation(PackageVersion, backref='package_info'), 'package_version' : orm.relation(PackageVersion, backref='package_info'),
}) })
...@@ -77,8 +77,6 @@ class BasePlugin(object): ...@@ -77,8 +77,6 @@ class BasePlugin(object):
``severity`` ``severity``
Severity of the result. Severity of the result.
""" """
if data is None:
data = self.outcomes.get(outcome)['name']
self.result.append(PluginResult(from_plugin=self.name, outcome=outcome, self.result.append(PluginResult(from_plugin=self.name, outcome=outcome,
data=data, severity=severity)) data=data, severity=severity))
...@@ -96,8 +94,6 @@ class BasePlugin(object): ...@@ -96,8 +94,6 @@ class BasePlugin(object):
Severity of the result. Severity of the result.
""" """
if data is None:
data = self.outcomes.get(outcome)['name']
self.result.append(PluginResult(from_plugin=self.name, outcome=outcome, self.result.append(PluginResult(from_plugin=self.name, outcome=outcome,
data=data, severity=severity)) data=data, severity=severity))
...@@ -111,12 +107,6 @@ class BasePlugin(object): ...@@ -111,12 +107,6 @@ class BasePlugin(object):
``data`` ``data``
Resulting data from the plugin, like more detail about the process. Resulting data from the plugin, like more detail about the process.
""" """
if data is None:
try:
data = self.outcomes.get(outcome)['name']
except TypeError:
data = self.outcomes.get(outcome)
self.result.append(PluginResult(from_plugin=self.name, outcome=outcome, self.result.append(PluginResult(from_plugin=self.name, outcome=outcome,
data=data, severity=constants.PLUGIN_SEVERITY_INFO)) data=data, severity=constants.PLUGIN_SEVERITY_INFO))
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2010 Jan Dittberner <jandd@debian.org> # Copyright © 2010 Jan Dittberner <jandd@debian.org>
# Copyright © 2012 Nicolas Dandrimont <nicolas.dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -33,10 +34,16 @@ Holds the buildsystem plugin. ...@@ -33,10 +34,16 @@ Holds the buildsystem plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb, Copyright © 2010 Jan Dittberner' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2010 Jan Dittberner',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
import logging import logging
import os
from debian import deb822 from debian import deb822
from debexpo.lib import constants from debexpo.lib import constants
...@@ -54,26 +61,39 @@ class BuildSystemPlugin(BasePlugin): ...@@ -54,26 +61,39 @@ class BuildSystemPlugin(BasePlugin):
dsc = deb822.Dsc(file(self.changes.get_dsc())) dsc = deb822.Dsc(file(self.changes.get_dsc()))
data = {}
severity = constants.PLUGIN_SEVERITY_INFO
build_depends = dsc.get('Build-Depends', '') build_depends = dsc.get('Build-Depends', '')
if 'cdbs' in build_depends: if 'cdbs' in build_depends:
log.debug('Package uses CDBS') outcome = "Package uses CDBS"
self.info('uses-cdbs', None) data["build-system"] = "cdbs"
elif 'debhelper (7' in build_depends:
log.debug('Package uses debhelper 7')
self.info('uses-dh', None)
elif 'debhelper' in build_depends: elif 'debhelper' in build_depends:
log.debug('Package uses straight debhelper') data["build-system"] = "debhelper"
self.info('uses-debhelper', None)
# Retrieve the debhelper compat level
compatpath = os.path.join(self.tempdir, "extracted/debian/compat")
try:
with open(compatpath, "rb") as f:
compat_level = int(f.read().strip())
except IOError:
compat_level = None
data["compat-level"] = compat_level
# Warn on old compatibility levels
if compat_level is None or compat_level <= 4:
outcome = "Package uses debhelper with an old compatibility level"
severity = constants.PLUGIN_SEVERITY_WARNING
else:
outcome = "Package uses debhelper"
else: else:
log.warning('Build system cannot be determined') outcome = "Package uses an unknown build system"
self.failed('unknown-build-system', None, constants.PLUGIN_SEVERITY_WARNING) data["build-system"] = "unknown"
severity = constants.PLUGIN_SEVERITY_WARNING
plugin = BuildSystemPlugin self.failed(outcome, data, severity)
outcomes = {
'uses-cdbs' : { 'name' : 'The packages uses CDBS' }, plugin = BuildSystemPlugin
'uses-debhelper' : { 'name' : 'The package uses straight debhelper' },
'uses-dh' : { 'name' : 'The package uses debhelper 7' },
'unknown-build-system' : { 'name' : 'The package\'s build system cannot be determined' },
}
...@@ -65,5 +65,3 @@ class ChangesListPlugin(BasePlugin): ...@@ -65,5 +65,3 @@ class ChangesListPlugin(BasePlugin):
dest=self.changes.get_pool_path()) dest=self.changes.get_pool_path())
plugin = ChangesListPlugin plugin = ChangesListPlugin
outcomes = {}
...@@ -36,6 +36,7 @@ __author__ = 'Arno Töll' ...@@ -36,6 +36,7 @@ __author__ = 'Arno Töll'
__copyright__ = 'Copyright © 2011 Arno Töll' __copyright__ = 'Copyright © 2011 Arno Töll'
__license__ = 'MIT' __license__ = 'MIT'
from collections import defaultdict
import logging import logging
from debexpo.lib import constants from debexpo.lib import constants
...@@ -53,64 +54,73 @@ class ClosedBugsPlugin(BasePlugin): ...@@ -53,64 +54,73 @@ class ClosedBugsPlugin(BasePlugin):
Check to make sure the bugs closed belong to the package. Check to make sure the bugs closed belong to the package.
""" """
try: if 'Closes' not in self.changes:
self.changes['Closes']
except KeyError:
log.debug('Package does not close any bugs') log.debug('Package does not close any bugs')
return return
log.debug('Checking whether the bugs closed in the package belong to the package') log.debug('Checking whether the bugs closed in the package belong to the package')
bugs = [int(x) for x in self.changes['Closes'].split(' ')] bugs = [int(x) for x in self.changes['Closes'].split()]
binary_packages = self.changes['Description'].split('\n') binary_packages = self.changes['Description'].split('\n')
binary_packages = [t.strip() for t in binary_packages] binary_packages = [t.strip() for t in binary_packages]
if (len(bugs)): if bugs:
log.debug('Creating SOAP proxy to bugs.debian.org') log.debug('Creating SOAP proxy to bugs.debian.org')
try: try:
server = SOAPpy.SOAPProxy( ClosedBugsPlugin.URL, ClosedBugsPlugin.NS, simplify_objects = 1 ) server = SOAPpy.SOAPProxy(self.URL, self.NS, simplify_objects = 1)
bugs_retrieved = server.get_status( *bugs ) bugs_retrieved = server.get_status( *bugs )
bugs_retrieved = bugs_retrieved['item'] if 'item' in bugs_retrieved:
bugs_retrieved = bugs_retrieved['item']
else:
bugs_retrieved = []
# Force argument to be a list, SOAPpy returns a dictionary instead of a dictionary list # Force argument to be a list, SOAPpy returns a dictionary instead of a dictionary list
# if only one bug was found # if only one bug was found
if ( not isinstance( bugs_retrieved, list ) ): if not isinstance(bugs_retrieved, list):
bugs_retrieved = ( bugs_retrieved, ) bugs_retrieved = [bugs_retrieved]
except Exception as e: except Exception as e:
log.critical('An error occurred when creating the SOAP proxy at "%s" (ns: "%s"): %s' log.critical('An error occurred when creating the SOAP proxy at "%s" (ns: "%s"): %s'
% (ClosedBugsPlugin.URL, ClosedBugsPlugin.NS, e)) % (self.URL, self.NS, e))
self.failed('invalid-bug-specified', "%s: One or more bugs closed in this package do not exist" % (self.changes['Closes']), constants.PLUGIN_SEVERITY_ERROR)
return return
data = {
'buglist': bugs,
'raw': {},
'errors': [],
'bugs': defaultdict(list),
}
# Index bugs retrieved # Index bugs retrieved
bugs_bts = {}
for bug in bugs_retrieved: for bug in bugs_retrieved:
if 'key' in bug and 'value' in bug: if 'key' in bug and 'value' in bug:
bugs_bts[int(bug['key'])] = bug['value'] data["raw"][int(bug['key'])] = bug['value']
else: else:
continue continue
severity = constants.PLUGIN_SEVERITY_INFO
for bug in bugs: for bug in bugs:
if not bug in bugs_bts: if not bug in data['raw']:
log.error('Bug #%s does not exist' % bug) data["errors"].append('Bug #%s does not exist' % bug)
self.failed('bug-does-not-exist', bug, constants.PLUGIN_SEVERITY_ERROR) severity = max(severity, constants.PLUGIN_SEVERITY_ERROR)
name = bugs_bts[bug]['package']
if self._package_in_descriptions(name, binary_packages):
message = ('Closes bug #%d: "%s" in package %s' %
(bugs_bts[bug]['bug_num'], bugs_bts[bug]['subject'], bugs_bts[bug]['package']))
log.debug(message)
self.passed('bug-in-package', message, constants.PLUGIN_SEVERITY_INFO)
elif name == 'wnpp':
message = ('Closes WNPP bug #%d: "%s"' % (bugs_bts[bug]['bug_num'], bugs_bts[bug]['subject']))
log.debug(message)
self.passed('bug-in-package', message, constants.PLUGIN_SEVERITY_INFO)
else:
log.error('Bug #%s does not belong to this package' % bug)
self.failed('bug-not-in-package', bug, constants.PLUGIN_SEVERITY_ERROR)
name = data["raw"][bug]['package']
if self._package_in_descriptions(name, binary_packages) or name == "wnpp":
data["bugs"][name].append((bug, data["raw"][bug]["subject"], data["raw"][bug]["severity"]))
else:
data["errors"].append('Bug #%s does not belong to this package' % bug)
severity = max(severity, constants.PLUGIN_SEVERITY_ERROR)
if severity != constants.PLUGIN_SEVERITY_INFO:
outcome = "Package closes bugs in a wrong way"
elif "wnpp" in data["bugs"] and len(data["bugs"]) == 1:
outcome = "Package closes a WNPP bug"
else:
outcome = "Package closes bug%s" % ("s" if len(bugs) > 1 else "")
self.failed(outcome, data, severity)
else: else:
log.debug('Package does not close any bugs') log.debug('Package does not close any bugs')
...@@ -133,9 +143,3 @@ class ClosedBugsPlugin(BasePlugin): ...@@ -133,9 +143,3 @@ class ClosedBugsPlugin(BasePlugin):
return False return False
plugin = ClosedBugsPlugin plugin = ClosedBugsPlugin
outcomes = {
'invalid-bug-specified': {'name': 'One or more bugs closed in this package do not exist'},
'bug-not-in-package' : { 'name' : 'A bug closed in this package doesn\'t belong to this package' },
'bug-in-package' : { 'name' : 'A bug closed in this package belongs to this package' },
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2010 Jan Dittberner <jandd@debian.org> # Copyright © 2010 Jan Dittberner <jandd@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -33,28 +34,23 @@ Holds the controlfields plugin. ...@@ -33,28 +34,23 @@ Holds the controlfields plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb, Copyright © 2010 Jan Dittberner' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2010 Jan Dittberner',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
from debian import deb822 from debian import deb822
import logging import logging
from debexpo.lib import constants
from debexpo.plugins import BasePlugin from debexpo.plugins import BasePlugin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
fields = ['Homepage', 'Vcs-Browser', 'Vcs-Git', 'Vcs-Svn', 'Vcs-Bzr', 'Vcs-Hg'] fields = ['Homepage', 'Vcs-Browser', 'Vcs-Git', 'Vcs-Svn', 'Vcs-Bzr', 'Vcs-Hg']
def _gen_outcomes():
outcomes = {}
for field in fields:
for isisnot in ['', '-not']:
outcomes['%s-is%s-present' % (field.lower(), isisnot)] = \
'The %s field is%s present in debian/control' % (field, isisnot.replace('-', ' '))
return outcomes
class ControlFieldsPlugin(BasePlugin): class ControlFieldsPlugin(BasePlugin):
def test_control_fields(self): def test_control_fields(self):
...@@ -69,15 +65,21 @@ class ControlFieldsPlugin(BasePlugin): ...@@ -69,15 +65,21 @@ class ControlFieldsPlugin(BasePlugin):
log.critical('Could not open dsc file; skipping plugin') log.critical('Could not open dsc file; skipping plugin')
return return
data = {}
severity = constants.PLUGIN_SEVERITY_WARNING
outcome = "No Homepage field present"
for item in fields: for item in fields:
if item in dsc: if item in dsc:
self.info('%s-is-present' % item.lower(), '%s: %s' % (item, dsc[item])) data[item] = dsc[item]
log.debug('%s: %s' % (item, dsc[item]))
if "Homepage" in data:
severity = constants.PLUGIN_SEVERITY_INFO
if len(data) > 1:
outcome = "Homepage and VCS control fields present"
else: else:
# don't display missing VCS fields outcome = "Homepage control field present"
#self.info('%s-is-not-present' % item.lower(), None)
log.debug('%s field is not present' % item)
plugin = ControlFieldsPlugin self.failed(outcome, data, severity)
outcomes = _gen_outcomes() plugin = ControlFieldsPlugin
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# This file is part of debexpo - http://debexpo.workaround.org # This file is part of debexpo - http://debexpo.workaround.org
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -32,12 +33,18 @@ Holds the debian plugin. ...@@ -32,12 +33,18 @@ Holds the debian plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
import logging import logging
import urllib import lxml.etree
import re import urllib2
from debexpo.model import meta
from debexpo.model.users import User
from debexpo.plugins import BasePlugin from debexpo.plugins import BasePlugin
...@@ -45,121 +52,134 @@ log = logging.getLogger(__name__) ...@@ -45,121 +52,134 @@ log = logging.getLogger(__name__)
class DebianPlugin(BasePlugin): class DebianPlugin(BasePlugin):
def _get_qa_page(self):
if not hasattr(self, 'qa_page'):
self.qa_page = urllib.urlopen('http://packages.qa.debian.org/%s' % self.changes['Source'])
def _in_debian(self): def _in_debian(self):
self._get_qa_page() try:
qa_page_code = self.qa_page.getcode() self.qa_page = urllib2.urlopen('http://packages.qa.debian.org/%s' % self.changes['Source'])
if qa_page_code == 404: except urllib2.HTTPError:
self.in_debian = False self.in_debian = False
else: else:
self.in_debian = True self.in_debian = True
self.parsed_qa = lxml.etree.fromstring(self.qa_page.read())
def test_package_in_debian(self): def _qa_xpath(self, query, item = None):
"""Perform the xpath query on the given item"""
if item is None:
item = self.parsed_qa
return item.xpath(
query,
namespaces={'xhtml': 'http://www.w3.org/1999/xhtml'}
)
def _test_package_in_debian(self):
""" """
Finds whether the package is in Debian. Finds whether the package is in Debian.
""" """
log.debug('Testing whether the package is in Debian already') log.debug('Testing whether the package is in Debian already')
if not hasattr(self, 'in_debian'): if self.in_debian:
self._in_debian() self.outcome = "Package is already in Debian"
if self.in_debian == True:
log.debug('Package is in Debian')
self.info('package-is-in-debian', None)
else: else:
log.debug('Package is not in Debian') self.outcome = "Package is not in Debian"
self.info('package-is-not-in-debian', None)
def test_last_upload(self): self.data["in-debian"] = self.in_debian
def _test_last_upload(self):
""" """
Finds the date of the last upload of the package. Finds the date of the last upload of the package.
""" """
if not hasattr(self, 'in_debian'):
self._in_debian()
if self.in_debian == False:
return
log.debug('Finding when the last upload of the package was') log.debug('Finding when the last upload of the package was')
qa_page_lines = self.qa_page.readlines() news = self._qa_xpath('//xhtml:ul[@id="news-list"]')[0]
for item in qa_page_lines: for item in news.getchildren():
if 'Accepted' in item: if 'Accepted' in self._qa_xpath('xhtml:a/child::text()', item):
last_change = re.search("\[(\d{4}-\d{2}-\d{2})\]", item) last_change = item.text[1:11]
if not last_change:
continue
last_change = last_change.group(1)
log.debug('Last upload on %s' % last_change) log.debug('Last upload on %s' % last_change)
self.info('last-debian-upload', 'Last upload on %s' % last_change ) self.data["latest-upload"] = last_change
return return
log.warning('Couldn\'t find last upload date') log.warning('Couldn\'t find last upload date')
def test_is_nmu(self): def _test_is_nmu(self):
""" """
Finds out whether the package is an NMU. Finds out whether the package is an NMU.
""" """
log.debug('Finding out whether the package is an NMU') log.debug('Finding out whether the package is a NMU')
if 'nmu' in self.changes['Version']: import string
log.debug('Package is an NMU')
log.info('package-is-nmu', None) delete_chars = string.maketrans(
else: string.ascii_lowercase + "\n", " " * (len(string.ascii_lowercase) + 1)
log.debug('Package is not an NMU') )
def test_is_maintainer(self): changes = str(self.changes["Changes"]).lower().translate(None, delete_chars).splitlines()
self.data["nmu"] = (
any(change.startswith('nonmaintainerupload') for change in changes) or
any(change.startswith('nmu') for change in changes) or
'nmu' in self.changes["Version"]
)
def _get_debian_maintainer_data(self):
self.debian_maintainers = sorted(
self._qa_xpath('//xhtml:span[@title="maintainer"]/child::text()') +
self._qa_xpath('//xhtml:span[@title="uploader"]/child::text()')
)
self.user_name = ""
self.user_email = ""
if self.user_id is not None:
user = meta.session.query(User).get(self.user_id)
if user is not None:
self.user_name = user.name
self.user_email = user.email
def _test_is_debian_maintainer(self):
""" """
Tests whether the package Maintainer is the Debian Maintainer. Tests whether the package Maintainer is the Debian Maintainer.
""" """
if not self._in_debian():
return
log.debug('Finding out whether the package Maintainer is the Debian Maintainer') log.debug('Finding out whether the package Maintainer is the Debian Maintainer')
# TODO self.data["is-debian-maintainer"] = self.user_name in self.debian_maintainers
def test_is_new_maintainer(self):
def _test_has_new_maintainer(self):
""" """
Tests whether this package version introduces a new Maintainer. Tests whether this package version introduces a new Maintainer.
""" """
if not self._in_debian():
return
log.debug('Finding out whether this package version introduces a new Maintainer') log.debug('Finding out whether this package version introduces a new Maintainer')
# TODO # TODO
def test_package_closes_wnpp(self): def _test_previous_sponsors(self):
""" """
Tests whether the package closes wnpp bugs. Finds previous sponsors.
""" """
log.debug('Finding out whether the package closes wnpp bugs') log.debug('Finding previous sponsors of the package')
# TODO # TODO
def test_itp_information(self): def test_qa(self):
""" """Run the Debian QA tests"""
Finds information about any ITPs closed.
"""
log.debug('Finding information about any ITPs closed')
# TODO self._in_debian()
def test_previous_sponsors(self): self.outcome = ""
""" self.data = {}
Finds previous sponsors.
"""
log.debug('Finding previous sponsors of the package')
# TODO self._test_package_in_debian()
if self.in_debian:
self._test_last_upload()
self._test_is_nmu()
self._get_debian_maintainer_data()
self._test_is_debian_maintainer()
self._test_has_new_maintainer()
self._test_previous_sponsors()
self.info(self.outcome, self.data)
plugin = DebianPlugin
outcomes = { plugin = DebianPlugin
'package-is-in-debian' : { 'name' : 'Package is in Debian' },
'package-is-not-in-debian' : { 'name' : 'Package is not in Debian' },
'last-debian-upload' : { 'name' : 'Date package was last uploaded to Debian' },
'package-is-nmu' : { 'name' : 'Package is a Non-Maintainer Upload' },
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# This file is part of debexpo - http://debexpo.workaround.org # This file is part of debexpo - http://debexpo.workaround.org
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -32,7 +33,10 @@ Holds the diffclean plugin. ...@@ -32,7 +33,10 @@ Holds the diffclean plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
import subprocess import subprocess
...@@ -59,22 +63,23 @@ class DiffCleanPlugin(BasePlugin): ...@@ -59,22 +63,23 @@ class DiffCleanPlugin(BasePlugin):
diffstat = subprocess.Popen(["diffstat", "-p1", difffile], stdout=subprocess.PIPE).communicate()[0] diffstat = subprocess.Popen(["diffstat", "-p1", difffile], stdout=subprocess.PIPE).communicate()[0]
dirty = False data = {
for item in diffstat.split('\n')[:-1]: "dirty": False,
if not item.startswith(' debian/'): "modified-files": [],
dirty = True }
break
if not dirty: # Last line is the summary line
for item in diffstat.splitlines()[:-1]:
filename, stats = [i.strip() for i in item.split("|")]
if not filename.startswith('debian/'):
data["dirty"] = True
data["modified-files"].append((filename, stats))
if not data["dirty"]:
log.debug('Diff file %s is clean' % difffile) log.debug('Diff file %s is clean' % difffile)
self.passed('diff-clean', None, constants.PLUGIN_SEVERITY_INFO) self.passed("The package's .diff.gz does not modify files outside of debian/", data, constants.PLUGIN_SEVERITY_INFO)
else: else:
log.error('Diff file %s is not clean' % difffile) log.error('Diff file %s is not clean' % difffile)
self.failed('diff-dirty', diffstat, constants.PLUGIN_SEVERITY_ERROR) self.failed("The package's .diff.gz modifies files outside of debian/", data, constants.PLUGIN_SEVERITY_WARNING)
plugin = DiffCleanPlugin plugin = DiffCleanPlugin
outcomes = {
'diff-clean' : { 'name' : 'The diff.gz file is clean' },
'diff-dirty' : { 'name' : 'The diff.gz file is dirty' },
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# This file is part of debexpo - http://debexpo.workaround.org # This file is part of debexpo - http://debexpo.workaround.org
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -32,9 +33,13 @@ Holds the lintian plugin. ...@@ -32,9 +33,13 @@ Holds the lintian plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
from collections import defaultdict
import subprocess import subprocess
import logging import logging
...@@ -51,32 +56,61 @@ class LintianPlugin(BasePlugin): ...@@ -51,32 +56,61 @@ class LintianPlugin(BasePlugin):
""" """
log.debug('Running lintian on the package') log.debug('Running lintian on the package')
output = subprocess.Popen(["lintian", self.changes_file], stdout=subprocess.PIPE).communicate()[0] output = subprocess.Popen(["lintian",
"-E",
"-I",
"--pedantic",
"--show-overrides",
self.changes_file], stdout=subprocess.PIPE).communicate()[0]
items = output.split('\n') items = output.split('\n')
if items and output != '':
# Yes, three levels of defaultdict and one of list...
def defaultdict_defaultdict_list():
def defaultdict_list():
return defaultdict(list)
return defaultdict(defaultdict_list)
lintian_warnings = defaultdict(defaultdict_defaultdict_list)
lintian_severities = set()
override_comments = []
for item in items:
if not item:
continue
# lintian output is of the form """SEVERITY: package: lintian_tag [lintian tag arguments]""" or """N: Override comment"""
if item.startswith("N: "):
override_comments.append(item[3:].strip())
continue
severity, package, rest = item.split(': ', 2)
lintian_severities.add(severity)
lintian_tag_data = rest.split()
lintian_tag = lintian_tag_data[0]
lintian_data = lintian_tag_data[1:]
if override_comments:
lintian_data.append("(override comment: " + " ".join(override_comments) + ")")
override_comments = []
lintian_warnings[package][severity][lintian_tag].append(lintian_data)
severity = constants.PLUGIN_SEVERITY_INFO
if 'E' in lintian_severities:
severity = constants.PLUGIN_SEVERITY_ERROR
outcome = 'Package has lintian errors'
elif 'W' in lintian_severities:
severity = constants.PLUGIN_SEVERITY_WARNING severity = constants.PLUGIN_SEVERITY_WARNING
outcome = 'lintian-warnings' outcome = 'Package has lintian warnings'
logmessage = log.warning elif 'I' in lintian_severities:
for item in items: outcome = 'Package has lintian informational warnings'
if item.startswith('E:'): elif 'O' in lintian_severities:
severity = constants.PLUGIN_SEVERITY_ERROR outcome = 'Package has overridden lintian tags'
outcome = 'lintian-errors' elif 'P' in lintian_severities or 'X' in lintian_severities:
logmessage = log.error outcome = 'Package has lintian pedantic/experimental warnings'
break
logmessage('Package is not Lintian clean')
self.failed(outcome, output, severity)
self.info(outcome, None)
else: else:
log.debug('Package is Lintian clean') outcome = 'Package is lintian clean'
self.passed('lintian-clean', None, constants.PLUGIN_SEVERITY_INFO)
plugin = LintianPlugin self.failed(outcome, lintian_warnings, severity)
outcomes = { plugin = LintianPlugin
'lintian-clean' : { 'name' : 'Package is Lintian clean' },
'lintian-warnings' : { 'name' : 'Package has Lintian warnings' },
'lintian-errors' : { 'name' : 'Package has Lintian errors' },
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# This file is part of debexpo - http://debexpo.workaround.org # This file is part of debexpo - http://debexpo.workaround.org
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -32,9 +33,13 @@ Holds the maintaineremail plugin. ...@@ -32,9 +33,13 @@ Holds the maintaineremail plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
import email.utils
import logging import logging
import re import re
...@@ -60,36 +65,36 @@ class MaintainerEmailPlugin(BasePlugin): ...@@ -60,36 +65,36 @@ class MaintainerEmailPlugin(BasePlugin):
user = meta.session.query(User).get(self.user_id) user = meta.session.query(User).get(self.user_id)
if user is not None: if user is not None:
regex = re.compile(r'^(.*) ?(<.+@.+>)$') maintainer_name, maintainer_email = email.utils.parseaddr(self.changes['Maintainer'])
maintainer_email = regex.match(self.changes['Maintainer']).group(2)[1:-1]
uploader_emails = [] uploader_emails = []
dsc = deb822.Dsc(file(self.changes.get_dsc())) dsc = deb822.Dsc(file(self.changes.get_dsc()))
if 'Uploaders' in dsc: if 'Uploaders' in dsc:
for uploader in dsc['Uploaders'].split(','): for uploader_name, uploader_email in email.utils.getaddresses([dsc['Uploaders']]):
match = regex.match(uploader) uploader_emails.append(uploader_email)
if match:
uploader_emails.append(match.group(2)[1:-1])
severity = constants.PLUGIN_SEVERITY_INFO
if user.email == maintainer_email: if user.email == maintainer_email:
log.debug('Maintainer email is the same as the uploader') log.debug('"Maintainer" email is the same as the uploader')
self.passed('maintainer-is-uploader', None, constants.PLUGIN_SEVERITY_INFO) outcome = '"Maintainer" email is the same as the uploader'
elif user.email in uploader_emails: elif user.email in uploader_emails:
log.debug('The uploader is in the package\'s "Uploaders" field') log.debug('The uploader is in the package\'s "Uploaders" field')
self.passed('uploader-in-uploaders', None, constants.PLUGIN_SEVERITY_INFO) outcome = 'The uploader is in the package\'s "Uploaders" field'
else: else:
log.warning('%s != %s' % (user.email, maintainer_email)) log.warning('%s != %s' % (user.email, maintainer_email))
self.failed('maintainer-is-not-uploader', '%s != %s' % (user.email, maintainer_email), outcome = 'The uploader is not in the package\'s "Maintainer" or "Uploaders" fields'
constants.PLUGIN_SEVERITY_WARNING) severity = constants.PLUGIN_SEVERITY_WARNING
data = {
'user-is-maintainer': (severity == constants.PLUGIN_SEVERITY_INFO),
'user-email': user.email,
'maintainer-email': maintainer_email,
'uploader-emails': uploader_emails,
}
self.failed(outcome, data, severity)
else: else:
log.warning('Could not get the uploader\'s user details from the database') log.warning('Could not get the uploader\'s user details from the database')
plugin = MaintainerEmailPlugin plugin = MaintainerEmailPlugin
outcomes = {
'maintainer-is-uploader' : { 'name' : 'The maintainer and uploader emails are the same' },
'uploader-in-uploaders' : { 'name' : 'The uploader is in the package\'s "Uploaders" field' },
'maintainer-is-not-uploader' : { 'name' : 'The maintainer and uploader emails are not the same' },
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# This file is part of debexpo - http://debexpo.workaround.org # This file is part of debexpo - http://debexpo.workaround.org
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -32,7 +33,10 @@ Holds the native plugin. ...@@ -32,7 +33,10 @@ Holds the native plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = ", ".join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
import logging import logging
...@@ -57,14 +61,9 @@ class NativePlugin(BasePlugin): ...@@ -57,14 +61,9 @@ class NativePlugin(BasePlugin):
# Most uploads will not be native, and especially on mentors, a native # Most uploads will not be native, and especially on mentors, a native
# package is almost probably in error. # package is almost probably in error.
log.warning('Package is native') log.warning('Package is native')
self.failed('is-native', None, constants.PLUGIN_SEVERITY_WARNING) self.failed('Package is native', {"native": True}, constants.PLUGIN_SEVERITY_WARNING)
else: else:
log.debug('Package is not native') log.debug('Package is not native')
self.passed('is-not-native', None, constants.PLUGIN_SEVERITY_INFO) self.passed('Package is not native', {"native": False}, constants.PLUGIN_SEVERITY_INFO)
plugin = NativePlugin plugin = NativePlugin
outcomes = {
'is-native' : { 'name' : 'Package is native' },
'is-not-native' : { 'name' : 'Package is not native' },
}
...@@ -43,6 +43,8 @@ from debexpo.lib import constants ...@@ -43,6 +43,8 @@ from debexpo.lib import constants
from debexpo.model import meta from debexpo.model import meta
from debexpo.model.packages import Package from debexpo.model.packages import Package
from pylons import config
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class NotUploaderPlugin(BasePlugin): class NotUploaderPlugin(BasePlugin):
...@@ -53,7 +55,7 @@ class NotUploaderPlugin(BasePlugin): ...@@ -53,7 +55,7 @@ class NotUploaderPlugin(BasePlugin):
make sure it was uploaded by the same uploader. make sure it was uploaded by the same uploader.
""" """
packagename = self.changes['Source'] packagename = self.changes['Source']
log.debug('Checking whether the %s is in the archive already' % packagename) log.debug('Checking whether %s is in the archive already' % packagename)
package = meta.session.query(Package).filter_by(name=packagename).first() package = meta.session.query(Package).filter_by(name=packagename).first()
...@@ -68,10 +70,8 @@ class NotUploaderPlugin(BasePlugin): ...@@ -68,10 +70,8 @@ class NotUploaderPlugin(BasePlugin):
# This isn't even worth setting an outcome. # This isn't even worth setting an outcome.
else: else:
log.error('Package does not belong to uploader') log.error('Package does not belong to uploader')
self.failed('package-does-not-belong-to-user', None, constants.PLUGIN_SEVERITY_CRITICAL) self.failed('This package was previously uploaded to %s by another user' % config["debexpo.sitename"],
None,
constants.PLUGIN_SEVERITY_CRITICAL)
plugin = NotUploaderPlugin plugin = NotUploaderPlugin
outcomes = {
'package-does-not-belong-to-user' : 'The uploaded package already has a version in the archive, which was uploaded by a different user'
}
...@@ -84,5 +84,3 @@ class RemovePackagePlugin(BasePlugin): ...@@ -84,5 +84,3 @@ class RemovePackagePlugin(BasePlugin):
meta.session.commit() meta.session.commit()
plugin = RemovePackagePlugin plugin = RemovePackagePlugin
outcomes = {}
...@@ -55,10 +55,6 @@ class UbuntuVersionPlugin(BasePlugin): ...@@ -55,10 +55,6 @@ class UbuntuVersionPlugin(BasePlugin):
# This isn't even worth setting an outcome. # This isn't even worth setting an outcome.
else: else:
log.error('Package has ubuntu in the version') log.error('Package has ubuntu in the version')
self.failed('package-has-ubuntu-version', None, constants.PLUGIN_SEVERITY_CRITICAL) self.failed('The uploaded package has "ubuntu" in the version', None, constants.PLUGIN_SEVERITY_CRITICAL)
plugin = UbuntuVersionPlugin plugin = UbuntuVersionPlugin
outcomes = {
'package-has-ubuntu-version' : 'The uploaded package has "ubuntu" in the version'
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
# This file is part of debexpo - http://debexpo.workaround.org # This file is part of debexpo - http://debexpo.workaround.org
# #
# Copyright © 2008 Jonny Lamb <jonny@debian.org> # Copyright © 2008 Jonny Lamb <jonny@debian.org>
# Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
# #
# Permission is hereby granted, free of charge, to any person # Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation # obtaining a copy of this software and associated documentation
...@@ -32,7 +33,10 @@ Holds the watchfile plugin. ...@@ -32,7 +33,10 @@ Holds the watchfile plugin.
""" """
__author__ = 'Jonny Lamb' __author__ = 'Jonny Lamb'
__copyright__ = 'Copyright © 2008 Jonny Lamb' __copyright__ = ', '.join([
'Copyright © 2008 Jonny Lamb',
'Copyright © 2012 Nicolas Dandrimont',
])
__license__ = 'MIT' __license__ = 'MIT'
import subprocess import subprocess
...@@ -61,55 +65,46 @@ class WatchFilePlugin(BasePlugin): ...@@ -61,55 +65,46 @@ class WatchFilePlugin(BasePlugin):
self._run_uscan() self._run_uscan()
return (self.output.find('Newest version on remote site is') != -1) return (self.output.find('Newest version on remote site is') != -1)
def test_watch_file_present(self): def test_uscan(self):
""" """
Check to see whether there is a watch file in the package. Run the watch file-related checks in the package
""" """
data = {
"watch-file-present": False,
}
log.debug('Checking to see whether there is a watch file in the package') log.debug('Checking to see whether there is a watch file in the package')
if self._watch_file_present(): if self._watch_file_present():
log.debug('Watch file present') log.debug('Watch file present')
self.passed('watch-file-present', None, constants.PLUGIN_SEVERITY_INFO) data["watch-file-present"] = True
else: else:
log.warning('Watch file not present') log.warning('Watch file not present')
self.failed('watch-file-not-present', None, constants.PLUGIN_SEVERITY_WARNING) self.failed('Watch file is not present', data, constants.PLUGIN_SEVERITY_WARNING)
return
def test_watch_file_works(self):
"""
Check to see whether the watch file works.
"""
if not self._watch_file_present(): return []
log.debug('Checking to see whether the watch file works')
if self._watch_file_works(): if self._watch_file_works():
log.debug('Watch file works') log.debug('Watch file works')
self.passed('watch-file-works', None, constants.PLUGIN_SEVERITY_INFO) data["watch-file-works"] = True
data["uscan-output"] = self.output
else: else:
log.warning('Watch file does not work') log.warning('Watch file does not work')
self.failed('watch-file-does-not-work', self.output, constants.PLUGIN_SEVERITY_WARNING) data["watch-file-works"] = False
data["uscan-output"] = self.output
self.failed("A watch file is present but doesn't work", data, constants.PLUGIN_SEVERITY_WARNING)
return
def test_new_upstream(self):
"""
Check to see whether there is a new upstream version.
"""
if not self._watch_file_present(): return []
if not self._watch_file_works(): return []
log.debug('Looking whether there is a new upstream version') log.debug('Looking whether there is a new upstream version')
if self.status == 1: if self.status == 1:
log.debug('Package is the latest upstream version') log.debug('Package is the latest upstream version')
self.passed('no-new-upstream-available', None, constants.PLUGIN_SEVERITY_INFO) data["latest-upstream"] = True
self.passed('Package is the latest upstream version', data, constants.PLUGIN_SEVERITY_INFO)
else: else:
log.warning('Package is not the latest upstream version') log.warning('Package is not the latest upstream version')
self.failed('new-upstream-available', self.output, constants.PLUGIN_SEVERITY_WARNING) data["latest-upstream"] = False
self.failed('Package is not the latest upstream version', data, constants.PLUGIN_SEVERITY_WARNING)
plugin = WatchFilePlugin plugin = WatchFilePlugin
outcomes = {
'watch-file-present' : { 'name' : 'A watch file is present' },
'watch-file-not-present' : { 'name' : 'A watch file is not present' },
'watch-file-works' : { 'name' : 'The watch file works' },
'watch-file-does-not-work' : { 'name' : 'The watch file does not work' },
'new-upstream-available' : { 'name' : 'A new upstream version is available' },
'no-new-upstream-available' : { 'name' : 'Package is the latest upstream version' },
}
/*
* debexpo.js – Javascript utility functions for the debexpo web interface
*
* This file is part of debexpo - http://debexpo.workaround.org
*
* Copyright © 2012 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
/* QA helper functions */
function toggle_qa(header, speed) {
vis = $(header).siblings(".visibility");
if (vis.html() == "+") {
vis.html("");
} else {
if ($(header).siblings(".qa-content").size() > 0) {
vis.html("+");
}
}
if (!speed) {
$(header).siblings(".qa-content").toggle();
} else {
$(header).siblings(".qa-content").toggle(speed);
}
}
function expand_qa(header, speed) {
vis = $(header).siblings(".visibility");
if (vis.html() == "+") {
toggle_qa(header, speed);
}
}
function collapse_qa(header, speed) {
vis = $(header).siblings(".visibility");
if (vis.html() != "+") {
toggle_qa(header, speed);
}
}
$(document).ready(function() {
/* General stuff */
$('.confirm').click(function(){
var answer = confirm('Are you sure?');
return answer
});
/* QA plugin stuff */
$(".qa-header").click(function() {
toggle_qa(this, "fast");
});
$(".visibility").click(function() {
toggle_qa($(this).siblings(".qa-header"), "fast");
});
$(".severity-info .qa-header").each(function() {
toggle_qa(this);
});
$(".qa-toplevel-header").after('<div class="qa-toggle">Toggle [<span class="qa-toggle-all">All</span>|<span class="qa-toggle-info">Info</span>]</div>')
$(".qa-toggle-all").toggle(
function() {
$(this).parent().next(".qa").find(".qa-header").each(function() {
expand_qa(this, "fast");
})
},
function() {
$(this).parent().next(".qa").find(".qa-header").each(function() {
collapse_qa(this, "fast");
})
}
);
$(".qa-toggle-info").toggle(
function() {
$(this).parent().next(".qa").find(".severity-info .qa-header").each(function() {
expand_qa(this, "fast");
})
},
function() {
$(this).parent().next(".qa").find(".severity-info .qa-header").each(function() {
collapse_qa(this, "fast");
})
}
);
});
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment