Exemple #1
0
def _get_bodhi_history(username):
    """ Print the last action performed on bodhi by the given FAS user.

    :arg username, the fas username whose action is searched.
    """
    from bodhi.client.bindings import BodhiClient
    bodhiclient = BodhiClient("https://bodhi.fedoraproject.org/")

    log.debug('Querying Bodhi for user: {0}'.format(username))
    json_obj = bodhiclient.send_request(
        "updates/?user=%s" % username, verb='GET')

    def dategetter(field):
        def getter(item):
            return datetime.datetime.strptime(item[field],
                                              "%Y-%m-%d %H:%M:%S")

        return getter

    print('Last package update on bodhi:')
    if json_obj['updates']:
        latest = sorted(json_obj['updates'], key=dategetter("date_submitted")
                        )[-1]
        print('   {0} on package {1}'.format(
            latest["date_submitted"], latest["title"]))
    else:
        print('   No activity found on bodhi')
Exemple #2
0
 def search(self, query):
     """ Perform Bodhi query """
     result = []
     current_page = 1
     original_query = query
     while current_page:
         log.debug("Bodhi query: {0}".format(query))
         client = BodhiClient(self.url)
         data = client.send_request(query, verb='GET')
         objects = data['updates']
         log.debug("Result: {0} fetched".format(listed(
             len(objects), "item")))
         log.data(pretty(data))
         result.extend(objects)
         if current_page < data['pages']:
             current_page = current_page + 1
             query = f"{original_query}&page={current_page}"
         else:
             current_page = None
     return result
class BodhiConnector(IConnector, ICall, IQuery):
    _method_paths = dict()
    _query_paths = dict()
    _cache_prompts = dict()

    def __init__(self, environ, request):
        super(BodhiConnector, self).__init__(environ, request)
        self._prod_url = config.get('fedoracommunity.connector.bodhi.produrl',
                                    'https://bodhi.fedoraproject.org')
        self._bodhi_client = BodhiClient(self._base_url,
                                         insecure=self._insecure)

    @classmethod
    def query_updates_cache_prompt(cls, msg):
        if '.bodhi.' not in msg['topic']:
            return

        msg = msg['msg']
        if 'update' in msg:
            update = msg['update']
            release = update['release']['name']
            status = update['status']
            nvrs = [build['nvr'] for build in update['builds']]
            names = ['-'.join(nvr.split('-')[:-2]) for nvr in nvrs]
            releases = [release, '']
            statuses = [status, '']
            groupings = [False]
            headers = ['package', 'release', 'status', 'group_updates']
            combinations = product(names, releases, statuses, groupings)
            for values in combinations:
                yield dict(zip(headers, values))

    @classmethod
    def query_active_releases_cache_prompt(cls, msg):
        if '.bodhi.' not in msg['topic']:
            return

        msg = msg['msg']
        if 'update' in msg:
            nvrs = [build['nvr'] for build in msg['update']['builds']]
            names = ['-'.join(nvr.split('-')[:-2]) for nvr in nvrs]
            for name in names:
                yield {'package': name}

    # IConnector
    @classmethod
    def register(cls):
        cls._base_url = config.get('fedoracommunity.connector.bodhi.baseurl',
                                   'https://bodhi.fedoraproject.org/')

        check_certs = asbool(config.get('fedora.clients.check_certs', True))
        cls._insecure = not check_certs

        cls.register_query_updates()
        cls.register_query_active_releases()

    def request_data(self, path, params):
        return self._bodhi_client.send_request(path, auth=False, params=params)

    def introspect(self):
        # FIXME: return introspection data
        return None

    #ICall
    def call(self, resource_path, params):
        log.debug('BodhiConnector.call(%s)' % locals())
        # proxy client only returns structured data so we can pass
        # this off to request_data but we should fix that in ProxyClient
        return self.request_data(resource_path, params)

    #IQuery
    @classmethod
    def register_query_updates(cls):
        path = cls.register_query('query_updates',
                                  cls.query_updates,
                                  cls.query_updates_cache_prompt,
                                  primary_key_col='request_id',
                                  default_sort_col='request_id',
                                  default_sort_order=-1,
                                  can_paginate=True)

        path.register_column('request_id',
                             default_visible=False,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('updateid',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('nvr',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('submitter',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('status',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('request',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('karma',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('nagged',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('type',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('approved',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('date_submitted',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('date_pushed',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('date_modified',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('comments',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('bugs',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('builds',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('releases',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('release',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('karma_level',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)

        f = ParamFilter()
        f.add_filter('package', ['nvr'], allow_none=False)
        f.add_filter('status', ['status'], allow_none=True)
        f.add_filter('group_updates', allow_none=True, cast=bool)
        f.add_filter('granularity', allow_none=True)
        f.add_filter('release', allow_none=False)
        cls._query_updates_filter = f

    def query_updates(self,
                      start_row=None,
                      rows_per_page=None,
                      order=-1,
                      sort_col=None,
                      filters=None,
                      **params):
        if not filters:
            filters = {}

        filters = self._query_updates_filter.filter(filters, conn=self)
        group_updates = filters.get('group_updates', True)

        params.update(filters)
        params['page'] = int(start_row / rows_per_page) + 1

        # If we're grouping updates, ask for twice as much.  This is so we can
        # handle the case where there are two updates for each package, one for
        # each release.  Yes, worst case we get twice as much data as we ask
        # for, but this allows us to do *much* more efficient database calls on
        # the server.
        if group_updates:
            params['rows_per_page'] = rows_per_page * 2
        else:
            params['rows_per_page'] = rows_per_page

        # Convert bodhi1 query format to bodhi2.
        if 'package' in params:
            params['packages'] = params.pop('package')
        if 'release' in params:
            params['releases'] = params.pop('release')

        results = self._bodhi_client.send_request('updates',
                                                  auth=False,
                                                  params=params)

        total_count = results['total']

        if group_updates:
            updates_list = self._group_updates(results['updates'],
                                               num_packages=rows_per_page)
        else:
            updates_list = results['updates']

        for up in updates_list:
            versions = []
            releases = []

            if group_updates:
                up['title'] = up['dist_updates'][0]['title']

                for dist_update in up['dist_updates']:
                    versions.append(dist_update['version'])
                    releases.append(dist_update['release_name'])

                up['name'] = up['package_name']

                up['versions'] = versions
                up['releases'] = releases
                up['status'] = up['dist_updates'][0]['status']
                up['nvr'] = up['dist_updates'][0]['title']
                up['request_id'] = up['package_name'] + \
                    dist_update['version'].replace('.', '')
            else:
                chunks = up['title'].split('-')
                up['name'] = '-'.join(chunks[:-2])
                up['version'] = '-'.join(chunks[-2:])
                up['versions'] = chunks[-2]
                up['releases'] = up['release']['long_name']
                up['nvr'] = up['title']
                up['request_id'] = up.get('updateid') or \
                    up['nvr'].replace('.', '').replace(',', '')

            up['id'] = up['nvr'].split(',')[0]

            # A unique id that we can use in HTML class fields.
            #up['request_id'] = up.get('updateid') or \
            #        up['nvr'].replace('.', '').replace(',', '')

            actions = []

            up['actions'] = ''
            for action in actions:
                reqs = ''
                if group_updates:
                    for u in up['dist_updates']:
                        reqs += "update_action('%s', '%s');" % (u['title'],
                                                                action[0])
                    title = up['dist_updates'][0]['title']
                else:
                    reqs += "update_action('%s', '%s');" % (up['title'],
                                                            action[0])
                    title = up['title']

                # FIXME: Don't embed HTML
                up['actions'] += """
                <button id="%s_%s" onclick="%s return false;">%s</button><br/>
                """ % (title.replace('.', ''), action[0], reqs, action[1])

            # Dates
            if group_updates:
                date_submitted = up['dist_updates'][0]['date_submitted']
                date_pushed = up['dist_updates'][0]['date_pushed']
            else:
                date_submitted = up['date_submitted']
                date_pushed = up['date_pushed']

            granularity = filters.get('granularity', 'day')
            ds = DateTimeDisplay(date_submitted)
            up['date_submitted_display'] = ds.age(granularity=granularity,
                                                  general=True) + ' ago'

            if date_pushed:
                dp = DateTimeDisplay(date_pushed)
                up['date_pushed'] = dp.datetime.strftime('%d %b %Y')
                up['date_pushed_display'] = dp.age(granularity=granularity,
                                                   general=True) + ' ago'

            # karma
            # FIXME: take into account karma from both updates
            if group_updates:
                k = up['dist_updates'][0]['karma']
            else:
                k = up['karma']
            if k:
                up['karma_str'] = "%+d" % k
            else:
                up['karma_str'] = " %d" % k
            up['karma_level'] = 'meh'
            if k > 0:
                up['karma_level'] = 'good'
            if k < 0:
                up['karma_level'] = 'bad'

            up['details'] = self._get_update_details(up)

        return (total_count, updates_list)

    def _get_update_details(self, update):
        details = ''
        if update['status'] == 'stable':
            if update.get('updateid'):
                details += HTML.tag('a',
                                    c=update['updateid'],
                                    href='%s/updates/%s' %
                                    (self._prod_url, update['alias']))
            if update.get('date_pushed'):
                details += HTML.tag('br') + update['date_pushed']
            else:
                details += 'In process...'
        elif update['status'] == 'pending' and update.get('request'):
            details += 'Pending push to %s' % update['request']
            details += HTML.tag('br')
            details += HTML.tag('a',
                                c="View update details >",
                                href="%s/updates/%s" %
                                (self._prod_url, update['alias']))
        elif update['status'] == 'obsolete':
            for comment in update['comments']:
                if comment['user']['name'] == 'bodhi':
                    if comment['text'].startswith('This update has been '
                                                  'obsoleted by '):
                        details += markdown.markdown(comment['text'],
                                                     safe_mode="replace")
        return details

    def _get_update_actions(self, update):
        actions = []
        if update['request']:
            actions.append(('revoke', 'Cancel push'))
        else:
            if update['status'] == 'testing':
                actions.append(('unpush', 'Unpush'))
                actions.append(('stable', 'Push to stable'))
            if update['status'] == 'pending':
                actions.append(('testing', 'Push to testing'))
                actions.append(('stable', 'Push to stable'))
        return actions

    def _group_updates(self, updates, num_packages=None):
        """
        Group a list of updates by release.
        This method allows allows you to limit the number of packages,
        for when we want to display 1 package per row, regardless of how
        many updates there are for it.
        """
        packages = {}
        done = False
        i = 0

        if not updates:
            return []

        for update in updates:
            for build in update['builds']:
                pkg = build['nvr'].rsplit('-', 2)[0]
                if pkg not in packages:
                    if num_packages and i >= num_packages:
                        done = True
                        break
                    packages[pkg] = {
                        'package_name': pkg,
                        'dist_updates': list()
                    }
                    i += 1
                else:
                    skip = False
                    for up in packages[pkg]['dist_updates']:
                        if up['release_name'] == \
                           update['release']['long_name']:
                            skip = True
                            break
                    if skip:
                        break
                packages[pkg]['dist_updates'].append({
                    'release_name':
                    update['release']['long_name'],
                    'version':
                    '-'.join(build['nvr'].split('-')[-2:])
                })
                packages[pkg]['dist_updates'][-1].update(update)
            if done:
                break

        result = [packages[p] for p in packages]

        sort_col = 'date_submitted'
        if result[0]['dist_updates'][0]['status'] == 'stable':
            sort_col = 'date_pushed'

        result = sorted(result,
                        reverse=True,
                        cmp=lambda x, y: cmp(x['dist_updates'][0][sort_col], y[
                            'dist_updates'][0][sort_col]))

        return result

    def add_updates_to_builds(self, builds):
        """Update a list of koji builds with the corresponding bodhi updates.

        This method makes a single query to bodhi, asking if it knows about
        any updates for a given list of koji builds.  For builds with existing
        updates, the `update` will be added to it's dictionary.

        Currently it also adds `update_details`, which is HTML for rendering
        the builds update options.  Ideally, this should be done client-side
        in the template (builds/templates/table_widget.mak).

        """
        start = datetime.now()
        updates = self.call('get_updates_from_builds',
                            {'builds': ' '.join([b['nvr'] for b in builds])})
        if updates:
            # FIXME: Lets stop changing the upstream APIs by putting the
            # session id as the first element, and the results in the second.
            updates = updates[1]

        for build in builds:
            if build['nvr'] in updates:
                build['update'] = updates[build['nvr']]
                status = build['update']['status']
                details = ''
                # FIXME: ideally, we should just return the update JSON and do
                # this logic client-side in the template when the grid data
                # comes in.
                if status == 'stable':
                    details = 'Pushed to updates'
                elif status == 'testing':
                    details = 'Pushed to updates-testing'
                elif status == 'pending':
                    details = 'Pending push to %s' % build['update']['request']

                details += HTML.tag('br')
                details += HTML.tag('a',
                                    c="View update details >",
                                    href="%s/updates/%s" %
                                    (self._prod_url, build['update']['alias']))
            else:
                details = HTML.tag('a',
                                   c='Push to updates >',
                                   href='%s/new?builds.text=%s' %
                                   (self._prod_url, build['nvr']))

            build['update_details'] = details

        log.debug("Queried bodhi for builds in: %s" % (datetime.now() - start))

    @classmethod
    def register_query_active_releases(cls):
        path = cls.register_query('query_active_releases',
                                  cls.query_active_releases,
                                  cls.query_active_releases_cache_prompt,
                                  primary_key_col='release',
                                  default_sort_col='release',
                                  default_sort_order=-1,
                                  can_paginate=True)
        path.register_column('release',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('stable_version',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('testing_version',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)

        f = ParamFilter()
        f.add_filter('package', ['nvr'], allow_none=False)
        cls._query_active_releases = f

    def get_all_releases(self):
        releases_obj = self.call('releases', {})
        releases_all = None
        for i in range(1, releases_obj['pages'] + 1):
            if releases_all == None:
                releases_all = releases_obj['releases']
                continue
            temp = self.call('releases?page=' + str(i), {})['releases']
            releases_all.extend(temp)

        if releases_all == None:
            raise TypeError

        return releases_all

    def query_active_releases(self, filters=None, **params):
        releases = list()
        queries = list()
        # Mapping of tag -> release
        release_tag = dict()
        # List of testing builds to query bodhi for
        testing_builds = list()
        # nvr -> release lookup table
        testing_builds_row = dict()
        if not filters:
            filters = dict()
        filters = self._query_updates_filter.filter(filters, conn=self)
        package = filters.get('package')
        koji = get_connector('koji')._koji_client
        koji.multicall = True

        releases_all = self.get_all_releases()
        releases_all.append({
            'dist_tag': 'rawhide',
            'long_name': 'Rawhide',
            'stable_tag': 'rawhide',
            'testing_tag': 'no_testing_tag_found',
            'state': 'current'
        })
        releases_all = sorted(releases_all,
                              key=lambda k: k['dist_tag'],
                              reverse=True)

        for release in releases_all:
            if release['state'] not in ['current', 'pending']\
              or 'Modular' in release['long_name']:
                continue
            tag = release['dist_tag']
            name = release['long_name']
            r = {
                'release': name,
                'stable_version': 'None',
                'testing_version': 'None'
            }
            if tag == 'rawhide':
                koji.listTagged(tag,
                                package=package,
                                latest=True,
                                inherit=True)
                queries.append(tag)
                release_tag[tag] = r
            else:
                stable_tag = release['stable_tag']
                testing_tag = release['testing_tag']

                koji.listTagged(stable_tag,
                                package=package,
                                latest=True,
                                inherit=True)
                queries.append(stable_tag)
                release_tag[stable_tag] = r
                koji.listTagged(testing_tag, package=package, latest=True)
                queries.append(testing_tag)
                release_tag[testing_tag] = r
            releases.append(r)

        results = koji.multiCall()

        for i, result in enumerate(results):
            if isinstance(result, dict):
                if 'faultString' in result:
                    log.error("FAULT: %s" % result['faultString'])
                else:
                    log.error("Can't find fault string in result: %s" % result)
            else:
                query = queries[i]
                row = release_tag[query]
                release = result[0]

                if query == 'dist-rawhide':
                    if release:
                        nvr = parse_build(release[0]['nvr'])
                        row['stable_version'] = HTML.tag(
                            'a',
                            c='%(version)s-%(release)s' % nvr,
                            href=koji_build_url % nvr)
                    else:
                        row['stable_version'] = \
                            'No builds tagged with %s' % tag
                    row['testing_version'] = HTML.tag('i', c='Not Applicable')
                    continue
                if release:
                    release = release[0]
                    if query.endswith('-testing'):
                        nvr = parse_build(release['nvr'])
                        row['testing_version'] = HTML.tag(
                            'a',
                            c='%(version)s-%(release)s' % nvr,
                            href=koji_build_url % nvr)
                        testing_builds.append(release['nvr'])
                        testing_builds_row[release['nvr']] = row
                    else:
                        # stable
                        nvr = parse_build(release['nvr'])
                        row['stable_version'] = HTML.tag(
                            'a',
                            c='%(version)s-%(release)s' % nvr,
                            href=koji_build_url % nvr)
                        if release['tag_name'].endswith('-updates'):
                            row['stable_version'] += ' (' + HTML.tag(
                                'a',
                                c='update',
                                href='%s/updates/?builds=%s' %
                                (self._prod_url, nvr['nvr'])) + ')'

        # If there are updates in testing, then query bodhi with a single call
        if testing_builds:
            data = self.call('updates', {'builds': ' '.join(testing_builds)})
            updates = data['updates']
            for up in updates:

                for build in up['builds']:
                    if build['nvr'] in testing_builds:
                        break
                else:
                    continue
                build = build['nvr']

                if up.karma > 1:
                    up.karma_icon = 'good'
                elif up.karma < 0:
                    up.karma_icon = 'bad'
                else:
                    up.karma_icon = 'meh'
                karma_ico_16 = '/images/16_karma-%s.png' % up.karma_icon
                karma_icon_url = \
                    self._request.environ.get('SCRIPT_NAME', '') + \
                    karma_ico_16
                karma = 'karma_%s' % up.karma_icon
                row = testing_builds_row[build]
                row['testing_version'] += " " + HTML.tag(
                    'div',
                    c=HTML.tag('a',
                               href="%s/updates/%s" %
                               (self._prod_url, up.alias),
                               c=HTML.tag('img', src=karma_icon_url) +
                               HTML.tag('span', c='%s karma' % up.karma)),
                    **{'class': '%s' % karma})

        return (len(releases), releases)
class BodhiConnector(IConnector, ICall, IQuery):
    _method_paths = dict()
    _query_paths = dict()
    _cache_prompts = dict()

    def __init__(self, environ, request):
        super(BodhiConnector, self).__init__(environ, request)
        self._prod_url = config.get(
            'fedoracommunity.connector.bodhi.produrl',
            'https://bodhi.fedoraproject.org')
        self._bodhi_client = BodhiClient(self._base_url,
                                         insecure=self._insecure)

    @classmethod
    def query_updates_cache_prompt(cls, msg):
        if '.bodhi.' not in msg['topic']:
            return

        msg = msg['msg']
        if 'update' in msg:
            update = msg['update']
            release = update['release']['name']
            status = update['status']
            nvrs = [build['nvr'] for build in update['builds']]
            names = ['-'.join(nvr.split('-')[:-2]) for nvr in nvrs]
            releases = [release, '']
            statuses = [status, '']
            groupings = [False]
            headers = ['package', 'release', 'status', 'group_updates']
            combinations = product(names, releases, statuses, groupings)
            for values in combinations:
                yield dict(zip(headers, values))

    @classmethod
    def query_active_releases_cache_prompt(cls, msg):
        if '.bodhi.' not in msg['topic']:
            return

        msg = msg['msg']
        if 'update' in msg:
            nvrs = [build['nvr'] for build in msg['update']['builds']]
            names = ['-'.join(nvr.split('-')[:-2]) for nvr in nvrs]
            for name in names:
                yield {'package': name}

    # IConnector
    @classmethod
    def register(cls):
        cls._base_url = config.get('fedoracommunity.connector.bodhi.baseurl',
                                   'https://bodhi.fedoraproject.org/')

        check_certs = asbool(config.get('fedora.clients.check_certs', True))
        cls._insecure = not check_certs

        cls.register_query_updates()
        cls.register_query_active_releases()

    def request_data(self, path, params):
        return self._bodhi_client.send_request(path, auth=False, params=params)

    def introspect(self):
        # FIXME: return introspection data
        return None

    #ICall
    def call(self, resource_path, params):
        log.debug('BodhiConnector.call(%s)' % locals())
        # proxy client only returns structured data so we can pass
        # this off to request_data but we should fix that in ProxyClient
        return self.request_data(resource_path, params)

    #IQuery
    @classmethod
    def register_query_updates(cls):
        path = cls.register_query(
            'query_updates',
            cls.query_updates,
            cls.query_updates_cache_prompt,
            primary_key_col='request_id',
            default_sort_col='request_id',
            default_sort_order=-1,
            can_paginate=True)

        path.register_column('request_id',
                             default_visible=False,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('updateid',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('nvr',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('submitter',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('status',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('request',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('karma',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('nagged',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('type',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('approved',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('date_submitted',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('date_pushed',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('date_modified',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('comments',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('bugs',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('builds',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('releases',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('release',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('karma_level',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)

        f = ParamFilter()
        f.add_filter('package', ['nvr'], allow_none=False)
        f.add_filter('status', ['status'], allow_none=True)
        f.add_filter('group_updates', allow_none=True, cast=bool)
        f.add_filter('granularity', allow_none=True)
        f.add_filter('release', allow_none=False)
        cls._query_updates_filter = f

    def query_updates(self, start_row=None,
                      rows_per_page=None,
                      order=-1,
                      sort_col=None,
                      filters=None,
                      **params):
        if not filters:
            filters = {}

        filters = self._query_updates_filter.filter(filters, conn=self)
        group_updates = filters.get('group_updates', True)

        params.update(filters)
        params['page'] = int(start_row/rows_per_page) + 1

        # If we're grouping updates, ask for twice as much.  This is so we can
        # handle the case where there are two updates for each package, one for
        # each release.  Yes, worst case we get twice as much data as we ask
        # for, but this allows us to do *much* more efficient database calls on
        # the server.
        if group_updates:
            params['rows_per_page'] = rows_per_page * 2
        else:
            params['rows_per_page'] = rows_per_page

        # Convert bodhi1 query format to bodhi2.
        if 'package' in params:
            params['packages'] = params.pop('package')
        if 'release' in params:
            params['releases'] = params.pop('release')

        results = self._bodhi_client.send_request('updates', auth=False, params=params)

        total_count = results['total']

        if group_updates:
            updates_list = self._group_updates(results['updates'],
                                               num_packages=rows_per_page)
        else:
            updates_list = results['updates']

        for up in updates_list:
            versions = []
            releases = []

            if group_updates:
                up['title'] = up['dist_updates'][0]['title']

                for dist_update in up['dist_updates']:
                    versions.append(dist_update['version'])
                    releases.append(dist_update['release_name'])

                up['name'] = up['package_name']

                up['versions'] = versions
                up['releases'] = releases
                up['status'] = up['dist_updates'][0]['status']
                up['nvr'] = up['dist_updates'][0]['title']
                up['request_id'] = up['package_name'] + \
                    dist_update['version'].replace('.', '')
            else:
                chunks = up['title'].split('-')
                up['name'] = '-'.join(chunks[:-2])
                up['version'] = '-'.join(chunks[-2:])
                up['versions'] = chunks[-2]
                up['releases'] = up['release']['long_name']
                up['nvr'] = up['title']
                up['request_id'] = up.get('updateid') or \
                    up['nvr'].replace('.', '').replace(',', '')

            up['id'] = up['nvr'].split(',')[0]

            # A unique id that we can use in HTML class fields.
            #up['request_id'] = up.get('updateid') or \
            #        up['nvr'].replace('.', '').replace(',', '')

            actions = []

            up['actions'] = ''
            for action in actions:
                reqs = ''
                if group_updates:
                    for u in up['dist_updates']:
                        reqs += "update_action('%s', '%s');" % (u['title'],
                                                                action[0])
                    title = up['dist_updates'][0]['title']
                else:
                    reqs += "update_action('%s', '%s');" % (up['title'],
                                                            action[0])
                    title = up['title']

                # FIXME: Don't embed HTML
                up['actions'] += """
                <button id="%s_%s" onclick="%s return false;">%s</button><br/>
                """ % (title.replace('.', ''), action[0], reqs, action[1])

            # Dates
            if group_updates:
                date_submitted = up['dist_updates'][0]['date_submitted']
                date_pushed = up['dist_updates'][0]['date_pushed']
            else:
                date_submitted = up['date_submitted']
                date_pushed = up['date_pushed']

            granularity = filters.get('granularity', 'day')
            ds = DateTimeDisplay(date_submitted)
            up['date_submitted_display'] = ds.age(granularity=granularity,
                                                  general=True) + ' ago'

            if date_pushed:
                dp = DateTimeDisplay(date_pushed)
                up['date_pushed'] = dp.datetime.strftime('%d %b %Y')
                up['date_pushed_display'] = dp.age(granularity=granularity,
                                                   general=True) + ' ago'

            # karma
            # FIXME: take into account karma from both updates
            if group_updates:
                k = up['dist_updates'][0]['karma']
            else:
                k = up['karma']
            if k:
                up['karma_str'] = "%+d" % k
            else:
                up['karma_str'] = " %d" % k
            up['karma_level'] = 'meh'
            if k > 0:
                up['karma_level'] = 'good'
            if k < 0:
                up['karma_level'] = 'bad'

            up['details'] = self._get_update_details(up)

        return (total_count, updates_list)

    def _get_update_details(self, update):
        details = ''
        if update['status'] == 'stable':
            if update.get('updateid'):
                details += HTML.tag('a', c=update['updateid'], href='%s/updates/%s' % (
                                    self._prod_url, update['alias']))
            if update.get('date_pushed'):
                details += HTML.tag('br') + update['date_pushed']
            else:
                details += 'In process...'
        elif update['status'] == 'pending' and update.get('request'):
            details += 'Pending push to %s' % update['request']
            details += HTML.tag('br')
            details += HTML.tag('a', c="View update details >",
                                href="%s/updates/%s" % (self._prod_url,
                                                update['alias']))
        elif update['status'] == 'obsolete':
            for comment in update['comments']:
                if comment['user']['name'] == 'bodhi':
                    if comment['text'].startswith('This update has been '
                                                  'obsoleted by '):
                        details += markdown.markdown(
                            comment['text'], safe_mode="replace")
        return details

    def _get_update_actions(self, update):
        actions = []
        if update['request']:
            actions.append(('revoke', 'Cancel push'))
        else:
            if update['status'] == 'testing':
                actions.append(('unpush', 'Unpush'))
                actions.append(('stable', 'Push to stable'))
            if update['status'] == 'pending':
                actions.append(('testing', 'Push to testing'))
                actions.append(('stable', 'Push to stable'))
        return actions

    def _group_updates(self, updates, num_packages=None):
        """
        Group a list of updates by release.
        This method allows allows you to limit the number of packages,
        for when we want to display 1 package per row, regardless of how
        many updates there are for it.
        """
        packages = {}
        done = False
        i = 0

        if not updates:
            return []

        for update in updates:
            for build in update['builds']:
                pkg = build['nvr'].rsplit('-', 2)[0]
                if pkg not in packages:
                    if num_packages and i >= num_packages:
                        done = True
                        break
                    packages[pkg] = {
                        'package_name': pkg,
                        'dist_updates': list()
                    }
                    i += 1
                else:
                    skip = False
                    for up in packages[pkg]['dist_updates']:
                        if up['release_name'] == \
                           update['release']['long_name']:
                            skip = True
                            break
                    if skip:
                        break
                packages[pkg]['dist_updates'].append({
                    'release_name': update['release']['long_name'],
                    'version': '-'.join(build['nvr'].split('-')[-2:])
                })
                packages[pkg]['dist_updates'][-1].update(update)
            if done:
                break

        result = [packages[p] for p in packages]

        sort_col = 'date_submitted'
        if result[0]['dist_updates'][0]['status'] == 'stable':
            sort_col = 'date_pushed'

        result = sorted(result, reverse=True,
                        cmp=lambda x, y: cmp(
                            x['dist_updates'][0][sort_col],
                            y['dist_updates'][0][sort_col])
                        )

        return result

    def add_updates_to_builds(self, builds):
        """Update a list of koji builds with the corresponding bodhi updates.

        This method makes a single query to bodhi, asking if it knows about
        any updates for a given list of koji builds.  For builds with existing
        updates, the `update` will be added to it's dictionary.

        Currently it also adds `update_details`, which is HTML for rendering
        the builds update options.  Ideally, this should be done client-side
        in the template (builds/templates/table_widget.mak).

        """
        start = datetime.now()
        updates = self.call('get_updates_from_builds', {
            'builds': ' '.join([b['nvr'] for b in builds])})
        if updates:
            # FIXME: Lets stop changing the upstream APIs by putting the
            # session id as the first element, and the results in the second.
            updates = updates[1]

        for build in builds:
            if build['nvr'] in updates:
                build['update'] = updates[build['nvr']]
                status = build['update']['status']
                details = ''
                # FIXME: ideally, we should just return the update JSON and do
                # this logic client-side in the template when the grid data
                # comes in.
                if status == 'stable':
                    details = 'Pushed to updates'
                elif status == 'testing':
                    details = 'Pushed to updates-testing'
                elif status == 'pending':
                    details = 'Pending push to %s' % build['update']['request']

                details += HTML.tag('br')
                details += HTML.tag('a', c="View update details >",
                                    href="%s/updates/%s" % (self._prod_url,
                                                    build['update']['alias']))
            else:
                details = HTML.tag('a', c='Push to updates >',
                                   href='%s/new?builds.text=%s' % (
                                       self._prod_url, build['nvr']))

            build['update_details'] = details

        log.debug(
            "Queried bodhi for builds in: %s" % (datetime.now() - start))

    @classmethod
    def register_query_active_releases(cls):
        path = cls.register_query('query_active_releases',
                                  cls.query_active_releases,
                                  cls.query_active_releases_cache_prompt,
                                  primary_key_col='release',
                                  default_sort_col='release',
                                  default_sort_order=-1,
                                  can_paginate=True)
        path.register_column('release',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('stable_version',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)
        path.register_column('testing_version',
                             default_visible=True,
                             can_sort=False,
                             can_filter_wildcards=False)

        f = ParamFilter()
        f.add_filter('package', ['nvr'], allow_none=False)
        cls._query_active_releases = f

    def get_all_releases(self):
        releases_obj = self.call('releases', {})
        releases_all = None
        for i in range(1, releases_obj['pages'] + 1):
            if releases_all == None:
                releases_all = releases_obj['releases']
                continue
            temp = self.call('releases?page=' + str(i), {})['releases']
            releases_all.extend(temp)

        if releases_all == None:
          raise TypeError

        return releases_all

    def query_active_releases(self, filters=None, **params):
        releases = list()
        queries = list()
        # Mapping of tag -> release
        release_tag = dict()
        # List of testing builds to query bodhi for
        testing_builds = list()
        # nvr -> release lookup table
        testing_builds_row = dict()
        if not filters:
            filters = dict()
        filters = self._query_updates_filter.filter(filters, conn=self)
        package = filters.get('package')
        koji = get_connector('koji')._koji_client
        koji.multicall = True

        releases_all = self.get_all_releases()
        releases_all.append({'dist_tag': 'rawhide',
                             'long_name':'Rawhide',
                             'stable_tag': 'rawhide',
                             'testing_tag': 'no_testing_tag_found',
                             'state': 'current'})
        releases_all = sorted(releases_all, key=lambda k: k['dist_tag'], reverse=True)

        for release in releases_all:
            if release['state'] not in ['current', 'pending']\
              or 'Modular' in release['long_name']:
                continue
            tag = release['dist_tag']
            name = release['long_name']
            r = {'release': name, 'stable_version': 'None',
                 'testing_version': 'None'}
            if tag == 'rawhide':
                koji.listTagged(
                    tag, package=package, latest=True, inherit=True)
                queries.append(tag)
                release_tag[tag] = r
            else:
                stable_tag = release['stable_tag']
                testing_tag = release['testing_tag']

                koji.listTagged(stable_tag, package=package,
                                latest=True, inherit=True)
                queries.append(stable_tag)
                release_tag[stable_tag] = r
                koji.listTagged(testing_tag, package=package, latest=True)
                queries.append(testing_tag)
                release_tag[testing_tag] = r
            releases.append(r)

        results = koji.multiCall()

        for i, result in enumerate(results):
            if isinstance(result, dict):
                if 'faultString' in result:
                    log.error("FAULT: %s" % result['faultString'])
                else:
                    log.error("Can't find fault string in result: %s" % result)
            else:
                query = queries[i]
                row = release_tag[query]
                release = result[0]

                if query == 'dist-rawhide':
                    if release:
                        nvr = parse_build(release[0]['nvr'])
                        row['stable_version'] = HTML.tag(
                            'a',
                            c='%(version)s-%(release)s' % nvr,
                            href=koji_build_url % nvr)
                    else:
                        row['stable_version'] = \
                            'No builds tagged with %s' % tag
                    row['testing_version'] = HTML.tag('i', c='Not Applicable')
                    continue
                if release:
                    release = release[0]
                    if query.endswith('-testing'):
                        nvr = parse_build(release['nvr'])
                        row['testing_version'] = HTML.tag(
                            'a',
                            c='%(version)s-%(release)s' % nvr,
                            href=koji_build_url % nvr)
                        testing_builds.append(release['nvr'])
                        testing_builds_row[release['nvr']] = row
                    else:
                        # stable
                        nvr = parse_build(release['nvr'])
                        row['stable_version'] = HTML.tag(
                            'a',
                            c='%(version)s-%(release)s' % nvr,
                            href=koji_build_url % nvr)
                        if release['tag_name'].endswith('-updates'):
                            row['stable_version'] +=  ' (' + HTML.tag(
                                'a', c='update',
                                href='%s/updates/?builds=%s' % (
                                    self._prod_url, nvr['nvr']
                                )
                            ) + ')'

        # If there are updates in testing, then query bodhi with a single call
        if testing_builds:
            data = self.call('updates', {
                'builds': ' '.join(testing_builds)
            })
            updates = data['updates']
            for up in updates:

                for build in up['builds']:
                    if build['nvr'] in testing_builds:
                        break
                else:
                    continue
                build = build['nvr']

                if up.karma > 1:
                    up.karma_icon = 'good'
                elif up.karma < 0:
                    up.karma_icon = 'bad'
                else:
                    up.karma_icon = 'meh'
                karma_ico_16 = '/images/16_karma-%s.png' % up.karma_icon
                karma_icon_url = \
                    self._request.environ.get('SCRIPT_NAME', '') + \
                    karma_ico_16
                karma = 'karma_%s' % up.karma_icon
                row = testing_builds_row[build]
                row['testing_version'] += " " + HTML.tag(
                    'div',
                    c=HTML.tag(
                        'a', href="%s/updates/%s" % (
                            self._prod_url, up.alias),
                        c=HTML.tag(
                            'img', src=karma_icon_url) + HTML.tag(
                            'span',
                            c='%s karma' % up.karma)),
                        **{'class': '%s' % karma})

        return (len(releases), releases)