from itertools import chain

from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_repr
from odoo.tools.misc import get_lang

class Pricelist(models.Model):
    _name = "product.pricelist"
    _description = "Pricelist"
    _order = "sequence asc, id desc"

    def _get_default_currency_id(self):
        return self.env.company.currency_id.id

    name = fields.Char('Pricelist Name', required=True, translate=True)
    active = fields.Boolean('Active', default=True, help="If unchecked, it will allow you to hide the pricelist without removing it.")
    item_ids = fields.One2many(
        'product.pricelist.item', 'pricelist_id', 'Pricelist Items',
        copy=True)
    currency_id = fields.Many2one('res.currency', 'Currency', default=_get_default_currency_id, required=True)
    company_id = fields.Many2one('res.company', 'Company')

    sequence = fields.Integer(default=16)
    country_group_ids = fields.Many2many('res.country.group', 'res_country_group_pricelist_rel',
                                         'pricelist_id', 'res_country_group_id', string='Country Groups')

    discount_policy = fields.Selection([
        ('with_discount', 'Discount included in the price'),
        ('without_discount', 'Show public price & discount to the customer')],
        default='with_discount')

    def name_get(self):
        return [(pricelist.id, '%s (%s)' % (pricelist.name, pricelist.currency_id.name)) for pricelist in self]

    @api.model
    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
        if name and operator == '=' and not args:
            # search on the name of the pricelist and its currency, opposite of name_get(),
            # Used by the magic context filter in the product search view.
            query_args = {'name': name, 'limit': limit, 'lang': get_lang(self.env).code}
            query = """SELECT p.id
                       FROM ((
                                SELECT pr.id, pr.name
                                FROM product_pricelist pr JOIN
                                     res_currency cur ON
                                         (pr.currency_id = cur.id)
                                WHERE pr.name || ' (' || cur.name || ')' = %(name)s
                            )
                            UNION (
                                SELECT tr.res_id as id, tr.value as name
                                FROM ir_translation tr JOIN
                                     product_pricelist pr ON (
                                        pr.id = tr.res_id AND
                                        tr.type = 'model' AND
                                        tr.name = 'product.pricelist,name' AND
                                        tr.lang = %(lang)s
                                     ) JOIN
                                     res_currency cur ON
                                         (pr.currency_id = cur.id)
                                WHERE tr.value || ' (' || cur.name || ')' = %(name)s
                            )
                        ) p
                       ORDER BY p.name"""
            if limit:
                query += " LIMIT %(limit)s"
            self._cr.execute(query, query_args)
            ids = [r[0] for r in self._cr.fetchall()]
            # regular search() to apply ACLs - may limit results below limit in some cases
            pricelist_ids = self._search([('id', 'in', ids)], limit=limit, access_rights_uid=name_get_uid)
            if pricelist_ids:
                return models.lazy_name_get(self.browse(pricelist_ids).with_user(name_get_uid))
        return super(Pricelist, self)._name_search(name, args, operator=operator, limit=limit, name_get_uid=name_get_uid)

    def _compute_price_rule_multi(self, products_qty_partner, date=False, uom_id=False):
        """ Low-level method - Multi pricelist, multi products
        Returns: dict{product_id: dict{pricelist_id: (price, suitable_rule)} }"""
        if not self.ids:
            pricelists = self.search([])
        else:
            pricelists = self
        results = {}
        for pricelist in pricelists:
            subres = pricelist._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id)
            for product_id, price in subres.items():
                results.setdefault(product_id, {})
                results[product_id][pricelist.id] = price
        return results

    def _compute_price_rule_get_items(self, products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids):
        self.ensure_one()
        # Load all rules
        self.env['product.pricelist.item'].flush(['price', 'currency_id', 'company_id'])
        self.env.cr.execute(
            """
            SELECT
                item.id
            FROM
                product_pricelist_item AS item
            LEFT JOIN product_category AS categ ON item.categ_id = categ.id
            WHERE
                (item.product_tmpl_id IS NULL OR item.product_tmpl_id = any(%s))
                AND (item.product_id IS NULL OR item.product_id = any(%s))
                AND (item.categ_id IS NULL OR item.categ_id = any(%s))
                AND (item.pricelist_id = %s)
                AND (item.date_start IS NULL OR item.date_start<=%s)
                AND (item.date_end IS NULL OR item.date_end>=%s)
            ORDER BY
                item.applied_on, item.min_quantity desc, categ.complete_name desc, item.id desc
            """,
            (prod_tmpl_ids, prod_ids, categ_ids, self.id, date, date))
        # NOTE: if you change `order by` on that query, make sure it matches
        # _order from model to avoid inconstencies and undeterministic issues.

        item_ids = [x[0] for x in self.env.cr.fetchall()]
        return self.env['product.pricelist.item'].browse(item_ids)

    def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False):
        """ Low-level method - Mono pricelist, multi products
        Returns: dict{product_id: (price, suitable_rule) for the given pricelist}

        Date in context can be a date, datetime, ...

            :param products_qty_partner: list of typles products, quantity, partner
            :param datetime date: validity date
            :param ID uom_id: intermediate unit of measure
        """
        self.ensure_one()
        if not date:
            date = self._context.get('date') or fields.Date.today()
        date = fields.Date.to_date(date)  # boundary conditions differ if we have a datetime
        if not uom_id and self._context.get('uom'):
            uom_id = self._context['uom']
        if uom_id:
            # rebrowse with uom if given
            products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
            products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
        else:
            products = [item[0] for item in products_qty_partner]

        if not products:
            return {}

        categ_ids = {}
        for p in products:
            categ = p.categ_id
            while categ:
                categ_ids[categ.id] = True
                categ = categ.parent_id
        categ_ids = list(categ_ids)

        is_product_template = products[0]._name == "product.template"
        if is_product_template:
            prod_tmpl_ids = [tmpl.id for tmpl in products]
            # all variants of all products
            prod_ids = [p.id for p in
                        list(chain.from_iterable([t.product_variant_ids for t in products]))]
        else:
            prod_ids = [product.id for product in products]
            prod_tmpl_ids = [product.product_tmpl_id.id for product in products]

        items = self._compute_price_rule_get_items(products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids)

        results = {}
        for product, qty, partner in products_qty_partner:
            results[product.id] = 0.0
            suitable_rule = False

            # Final unit price is computed according to `qty` in the `qty_uom_id` UoM.
            # An intermediary unit price may be computed according to a different UoM, in
            # which case the price_uom_id contains that UoM.
            # The final price will be converted to match `qty_uom_id`.
            qty_uom_id = self._context.get('uom') or product.uom_id.id
            price_uom_id = product.uom_id.id
            qty_in_product_uom = qty
            if qty_uom_id != product.uom_id.id:
                try:
                    qty_in_product_uom = self.env['uom.uom'].browse([self._context['uom']])._compute_quantity(qty, product.uom_id)
                except UserError:
                    # Ignored - incompatible UoM in context, use default product UoM
                    pass

            # if Public user try to access standard price from website sale, need to call price_compute.
            # TDE SURPRISE: product can actually be a template
            price = product.price_compute('list_price')[product.id]

            price_uom = self.env['uom.uom'].browse([qty_uom_id])
            for rule in items:
                if rule.min_quantity and qty_in_product_uom < rule.min_quantity:
                    continue
                if is_product_template:
                    if rule.product_tmpl_id and product.id != rule.product_tmpl_id.id:
                        continue
                    if rule.produc

t_id and not (product.product_variant_count == 1 and product.product_variant_id.id == rule.product_id.id):
                        # product rule acceptable on template if has only one variant
                        continue
                else:
                    if rule.product_tmpl_id and product.product_tmpl_id.id != rule.product_tmpl_id.id:
                        continue
                    if rule.product_id and product.id != rule.product_id.id:
                        continue

                if rule.categ_id:
                    cat = product.categ_id
                    while cat:
                        if cat.id == rule.categ_id.id:
                            break
                        cat = cat.parent_id
                    if not cat:
                        continue

                if rule.base == 'pricelist' and rule.base_pricelist_id:
                    price_tmp = rule.base_pricelist_id._compute_price_rule([(product, qty, partner)], date, uom_id)[product.id][0]  # TDE: 0 = price, 1 = rule
                    price = rule.base_pricelist_id.currency_id._convert(price_tmp, self.currency_id, self.env.company, date, round=False)
                else:
                    # if base option is public price take sale price else cost price of product
                    # price_compute returns the price in the context UoM, i.e. qty_uom_id
                    price = product.price_compute(rule.base)[product.id]

                convert_to_price_uom = (lambda price: product.uom_id._compute_price(price, price_uom))

                if price is not False:
                    if rule.compute_price == 'fixed':
                        price = convert_to_price_uom(rule.fixed_price)
                    elif rule.compute_price == 'percentage':
                        price = (price - (price * (rule.percent_price / 100))) or 0.0
                    else:
                        # complete formula
                        price_limit = price
                        price = (price - (price * (rule.price_discount / 100))) or 0.0
                        if rule.price_round:
                            price = tools.float_round(price, precision_rounding=rule.price_round)

                        if rule.price_surcharge:
                            price_surcharge = convert_to_price_uom(rule.price_surcharge)
                            price += price_surcharge

                        if rule.price_min_margin:
                            price_min_margin = convert_to_price_uom(rule.price_min_margin)
                            price = max(price, price_limit + price_min_margin)

                        if rule.price_max_margin:
                            price_max_margin = convert_to_price_uom(rule.price_max_margin)
                            price = min(price, price_limit + price_max_margin)
                    suitable_rule = rule
                break
            # Final price conversion into pricelist currency
            if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist':
                if suitable_rule.base == 'standard_price':
                    cur = product.cost_currency_id
                else:
                    cur = product.currency_id
                price = cur._convert(price, self.currency_id, self.env.company, date, round=False)

            if not suitable_rule:
                cur = product.currency_id
                price = cur._convert(price, self.currency_id, self.env.company, date, round=False)

            results[product.id] = (price, suitable_rule and suitable_rule.id or False)

        return results

    # New methods: product based
    def get_products_price(self, products, quantities, partners, date=False, uom_id=False):
        """ For a given pricelist, return price for products
        Returns: dict{product_id: product price}, in the given pricelist """
        self.ensure_one()
        return {
            product_id: res_tuple[0]
            for product_id, res_tuple in self._compute_price_rule(
                list(zip(products, quantities, partners)),
                date=date,
                uom_id=uom_id
            ).items()
        }

    def get_product_price(self, product, quantity, partner, date=False, uom_id=False):
        """ For a given pricelist, return price for a given product """
        self.ensure_one()
        return self._compute_price_rule([(product, quantity, partner)], date=date, uom_id=uom_id)[product.id][0]

    def get_product_price_rule(self, product, quantity, partner, date=False, uom_id=False):
        """ For a given pricelist, return price and rule for a given product """
        self.ensure_one()
        return self._compute_price_rule([(product, quantity, partner)], date=date, uom_id=uom_id)[product.id]

    def price_get(self, prod_id, qty, partner=None):
        """ Multi pricelist, mono product - returns price per pricelist """
        return {key: price[0] for key, price in self.price_rule_get(prod_id, qty, partner=partner).items()}

    def price_rule_get_multi(self, products_by_qty_by_partner):
        """ Multi pricelist, multi product  - return tuple """
        return self._compute_price_rule_multi(products_by_qty_by_partner)

    def price_rule_get(self, prod_id, qty, partner=None):
        """ Multi pricelist, mono product - return tuple """
        product = self.env['product.product'].browse([prod_id])
        return self._compute_price_rule_multi([(product, qty, partner)])[prod_id]

    @api.model
    def _price_get_multi(self, pricelist, products_by_qty_by_partner):
        """ Mono pricelist, multi product - return price per product """
        return pricelist.get_products_price(
            list(zip(**products_by_qty_by_partner)))

    def _get_partner_pricelist_multi_search_domain_hook(self, company_id):
        return [
            ('active', '=', True),
            ('company_id', 'in', [company_id, False]),
        ]

    def _get_partner_pricelist_multi_filter_hook(self):
        return self.filtered('active')

    def _get_partner_pricelist_multi(self, partner_ids, company_id=None):
        """ Retrieve the applicable pricelist for given partners in a given company.

            It will return the first found pricelist in this order:
            First, the pricelist of the specific property (res_id set), this one
                   is created when saving a pricelist on the partner form view.
            Else, it will return the pricelist of the partner country group
            Else, it will return the generic property (res_id not set), this one
                  is created on the company creation.
            Else, it will return the first available pricelist

            :param company_id: if passed, used for looking up properties,
                instead of current user's company
            :return: a dict {partner_id: pricelist}
        """
        # `partner_ids` might be ID from inactive uers. We should use active_test
        # as we will do a search() later (real case for website public user).
        Partner = self.env['res.partner'].with_context(active_test=False)
        company_id = company_id or self.env.company.id

        Property = self.env['ir.property'].with_context(force_company=company_id)
        Pricelist = self.env['product.pricelist']
        pl_domain = self._get_partner_pricelist_multi_search_domain_hook(company_id)

        # if no specific property, try to find a fitting pricelist
        result = Property.get_multi('property_product_pricelist', Partner._name, partner_ids)

        remaining_partner_ids = [pid for pid, val in result.items() if not val or
                                 not val._get_partner_pricelist_multi_filter_hook()]
        if remaining_partner_ids:
            # get fallback pricelist when no pricelist for a given country
            pl_fallback = (
                Pricelist.search(pl_domain + [('country_group_ids', '=', False)], limit=1) or
                Property.get('property_product_pricelist', 'res.partner') or
                Pricelist.search(pl_domain, limit=1)
            )
            # group partners by country, and find a pricelist for each country
            domain = [('id', 'in', remaining_partner_ids)]
            groups = Partner.read_group(domain, ['country_id'], ['country_id'])
            for group in groups:
                country_id = group['country_id'] and group['country_id'][0]
                pl = Pricelist.search(pl_domain + [('country_group_ids.country_ids', '=', country_id)], limit=1)
                pl = pl or pl_fallback
                for pid in Partner.search(group['__domain']).ids:
                    result[pid] = pl

        return result

    @api.model
    def get_import_templates(self):
        return [{
            'label': _('Import Template for Pricelists'),
            'template': '/product/static/xls/product_pricelist.xls'
        }]

class ResCountryGroup(models.Model):
    _inherit = 'res.country.group'

    pricelist_ids = fields.Many2many('product.pricelist', 'res_country_group_pricelist_rel',
                                     'res_country_group_id', 'pricelist_id', string='Pricelists')

class PricelistItem(models.Model):
    _name = "product.pricelist.item"
    _description = "Pricelist Rule"
    _order = "applied_on, min_quantity desc, categ_id desc, id desc"
    _check_company_auto = True
    # NOTE: if you change _order on this model, make sure it matches the SQL
    # query built in _compute_price_rule() above in this file to avoid

    def _default_pricelist_id(self):
        return self.env['product.pricelist'].search([
            '|', ('company_id', '=', False),
            ('company_id', '=', self.env.company.id)], limit=1)

    product_tmpl_id = fields.Many2one(
        'product.template', 'Product', ondelete='cascade', check_company=True,
        help="Specify a template if this rule only applies to one product template. Keep empty otherwise.")
    product_id = fields.Many2one(
        'product.product', 'Product Variant', ondelete='cascade', check_company=True,
        help="Specify a product if this rule only applies to one product. Keep empty otherwise.")
    categ_id = fields.Many2one(
        'product.category', 'Product Category', ondelete='cascade',
        help="Specify a product category if this rule only applies to products belonging to this category or its children categories. Keep empty otherwise.")
    min_quantity = fields.Integer(
        'Min. Quantity', default=0,
        help="For the rule to apply, bought/sold quantity must be greater "
             "than or equal to the minimum quantity specified in this field.\n"
             "Expressed in the default unit of measure of the product.")
    applied_on = fields.Selection([
        ('3_global', 'All Products'),
        ('2_product_category', 'Product Category'),
        ('1_product', 'Product'),
        ('0_product_variant', 'Product Variant')], "Apply On",
        default='3_global', required=True,
        help='Pricelist Item applicable on selected option')
    base = fields.Selection([
        ('list_price', 'Sales Price'),
        ('standard_price', 'Cost'),
        ('pricelist', 'Other Pricelist')], "Based on",
        default='list_price', required=True,
        help='Base price for computation.\n'
             'Sales Price: The base price will be the Sales Price.\n'
             'Cost Price : The base price will be the cost price.\n'
             'Other Pricelist : Computation of the base price based on another Pricelist.')
    base_pricelist_id = fields.Many2one('product.pricelist', 'Other Pricelist', check_company=True)
    pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', index=True, ondelete='cascade', required=True, default=_default_pricelist_id)
    price_surcharge = fields.Float(
        'Price Surcharge', digits='Product Price',
        help='Specify the fixed amount to add or substract(if negative) to the amount calculated with the discount.')
    price_discount = fields.Float('Price Discount', default=0, digits=(16, 2))
    price_round = fields.Float(
        'Price Rounding', digits='Product Price',
        help="Sets the price so that it is a multiple of this value.\n"
             "Rounding is applied after the discount and before the surcharge.\n"
             "To have prices that end in 9.99, set rounding 10, surcharge -0.01")
    price_min_margin = fields.Float(
        'Min. Price Margin', digits='Product Price',
        help='Specify the minimum amount of margin over the base price.')
    price_max_margin = fields.Float(
        'Max. Price Margin', digits='Product Price',
        help='Specify the maximum amount of margin over the base price.')
    company_id = fields.Many2one(
        'res.company', 'Company',
        readonly=True, related='pricelist_id.company_id', store=True)
    currency_id = fields.Many2one(
        'res.currency', 'Currency',
        readonly=True, related='pricelist_id.currency_id', store=True)
    active = fields.Boolean(
        readonly=True, related="pricelist_id.active", store=True)
    date_start = fields.Date('Start Date', help="Starting date for the pricelist item validation")
    date_end = fields.Date('End Date', help="Ending valid for the pricelist item validation")
    compute_price = fields.Selection([
        ('fixed', 'Fixed Price'),
        ('percentage', 'Percentage (discount)'),
        ('formula', 'Formula')], index=True, default='fixed', required=True)
    fixed_price = fields.Float('Fixed Price', digits='Product Price')
    percent_price = fields.Float('Percentage Price')
    # functional fields used for usability purposes
    name = fields.Char(
        'Name', compute='_get_pricelist_item_name_price',
        help="Explicit rule name for this pricelist line.")
    price = fields.Char(
        'Price', compute='_get_pricelist_item_name_price',
        help="Explicit rule name for this pricelist line.")

    @api.constrains('base_pricelist_id', 'pricelist_id', 'base')
    def _check_recursion(self):
        if any(item.base == 'pricelist' and item.pricelist_id and item.pricelist_id == item.base_pricelist_id for item in self):
            raise ValidationError(_('You cannot assign the Main Pricelist as Other Pricelist in PriceList Item'))
        return True

    @api.constrains('price_min_margin', 'price_max_margin')
    def _check_margin(self):
        if any(item.price_min_margin > item.price_max_margin for item in self):
            raise ValidationError(_('The minimum margin should be lower than the maximum margin.'))
        return True

    @api.constrains('product_id', 'product_tmpl_id', 'categ_id')
    def _check_product_consistency(self):
        for item in self:
            if item.applied_on == "2_product_category" and not item.categ_id:
                raise ValidationError(_("Please specify the category for which this rule should be applied"))
            elif item.applied_on == "1_product" and not item.product_tmpl_id:
                raise ValidationError(_("Please specify the product for which this rule should be applied"))
            elif item.applied_on == "0_product_variant" and not item.product_id:
                raise ValidationError(_("Please specify the product variant for which this rule should be applied"))

    @api.depends('applied_on', 'categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \
        'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge')
    def _get_pricelist_item_name_price(self):
        for item in self:
            if item.categ_id and item.applied_on == '2_product_category':
                item.name = _("Category: %s") % (item.categ_id.display_name)
            elif item.product_tmpl_id and item.applied_on == '1_product':
                item.name = _("Product: %s") % (item.product_tmpl_id.display_name)
            elif item.product_id and item.applied_on == '0_product_variant':
                item.name = _("Variant: %s") % (item.product_id.with_context(display_default_code=False).display_name)
            else:
                item.name = _("All Products")

            if item.compute_price == 'fixed':
                decimal_places = self.env['decimal.precision'].precision_get('Product Price')
                if item.currency_id.position == 'after':
                    item.price = "%s %s" % (
                        float_repr(
                            item.fixed_price,
                            decimal_places,
                        ),
                        item.currency_id.symbol,
                    )
                else:
                    item.price = "%s %s" % (
                        item.currency_id.symbol,
                        float_repr(
                            item.fixed_price,
                            decimal_places,
                        ),
                    )
            elif item.compute_price == 'percentage':
                item.price = _("%s %% discount") % (item.percent_price)
            else:
                item.price = _("%s %% discount and %s surcharge") % (item.price_discount, item.price_surcharge)

    @api.onchange('compute_price')
    def _onchange_compute_price(self):
        if self.compute_price != 'fixed':
            self.fixed_price = 0.0
        if self.compute_price != 'percentage':
            self.percent_price = 0.0
        if self.compute_price != 'formula':
            self.update({
                'price_discount': 0.0,
                'price_surcharge': 0.0,
                'price_round': 0.0,
                'price_min_margin': 0.0,
                'price_max_margin': 0.0,
            })

    @api.onchange('product_id')
    def _onchange_product_id(self):
        has_product_id = self.filtered('product_id')
        for item in has_product_id:
            item.product_tmpl_id = item.product_id.product_tmpl_id
        if self.env.context.get('default_applied_on', False) == '1_product':
            # If a product variant is specified, apply on variants instead
            # Reset if product variant is removed
            has_product_id.update({'applied_on': '0_product_variant'})
            (self - has_product_id).update({'applied_on': '1_product'})

    @api.onchange('product_tmpl_id')
    def _onchange_product_tmpl_id(self):
        has_tmpl_id = self.filtered('product_tmpl_id')
        for item in has_tmpl_id:
            if item.product_id and item.product_id.product_tmpl_id != item.product_tmpl_id:
                item.product_id = None

    @api.onchange('product_id', 'product_tmpl_id', 'categ_id')
    def _onchane_rule_content(self):
        if not self.user_has_groups('product.group_sale_pricelist') and not self.env.context.get('default_applied_on', False):
            # If advanced pricelists are disabled (applied_on field is not visible)
            # AND we aren't coming from a specific product template/variant.
            variants_rules = self.filtered('product_id')
            template_rules = (self-variants_rules).filtered('product_tmpl_id')
            variants_rules.update({'applied_on': '0_product_variant'})
            template_rules.update({'applied_on': '1_product'})
            (self-variants_rules-template_rules).update({'applied_on': '3_global'})

    @api.model_create_multi
    def create(self, vals_list):
        for values in vals_list:
            if values.get('applied_on', False):
                # Ensure item consistency for later searches.
                applied_on = values['applied_on']
                if applied_on == '3_global':
                    values.update(dict(product_id=None, product_tmpl_id=None, categ_id=None))
                elif applied_on == '2_product_category':
                    values.update(dict(product_id=None, product_tmpl_id=None))
                elif applied_on == '1_product':
                    values.update(dict(product_id=None, categ_id=None))
                elif applied_on == '0_product_variant':
                    values.update(dict(categ_id=None))
        return super(PricelistItem, self).create(vals_list)

    def write(self, values):
        if values.get('applied_on', False):
            # Ensure item consistency for later searches.
            applied_on = values['applied_on']
            if applied_on == '3_global':
                values.update(dict(product_id=None, product_tmpl_id=None, categ_id=None))
            elif applied_on == '2_product_category':
                values.update(dict(product_id=None, product_tmpl_id=None))
            elif applied_on == '1_product':
                values.update(dict(product_id=None, categ_id=None))
            elif applied_on == '0_product_variant':
                values.update(dict(categ_id=None))
        res = super(PricelistItem, self).write(values)
        # When the pricelist changes we need the product.template price
        # to be invalided and recomputed.
        self.flush()
        self.invalidate_cache()
        return res


# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)
    match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name
    current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录
    if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request, per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

def check_permission(func):

if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(*args,**kwargs)
    return  inner


def inner(*args,**kwargs):
        if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(*args,**kwargs)
    return  inner


match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name

# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)

    current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录
    if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request, per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

def check_permission(func):
    def inner(*args,**kwargs):
        if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(*args,**kwargs)
    return  inner


current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录

# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)
    match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name

    if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request, per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

def check_permission(func):
    def inner(*args,**kwargs):
        if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(*args,**kwargs)
    return  inner


if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request, per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)
    match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name
    current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录

def check_permission(func):
    def inner(*args,**kwargs):
        if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(*args,**kwargs)
    return  inner


# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)
    match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name
    current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录
    if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request, per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

def check_permission(func):
    def inner(*args,**kwargs):
        if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(


if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')

# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)
    match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name
    current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录
    if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request, per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

def check_permission(func):
    def inner(*args,**kwargs):

        return func(*args,**kwargs)
    return  inner


# kingadmin/permissions.py

from django.urls import resolve
from django.shortcuts import render,redirect,HttpResponse
from kingadmin.permission_list import perm_dic
from django.conf import settings

def perm_check(*args,**kwargs):
    #2.把url解析成url_name（通过resolve）
    #3.判断用户是否已登录（user.is_authenticated()）
    #3.拿url_name到permission_dict去匹配，匹配时要包括请求方法和参数
    #4.拿匹配到可权限key，调用user.has_perm(key)
    match_results = [None,]
    request = args[0]
    resolve_url_obj = resolve(request.path)
    #通过resolve解析出当前访问的url_name
    current_url_name = resolve_url_obj.url_name
    match_key = None
    #判断用户是否登录
    if request.user.is_authenticated() is False:
         return redirect(settings.LOGIN_URL)

    for permission_key,permission_val in  perm_dic.items():
        #key和value（值有四个参数）: 比如 'crm_table_index': ['table_index', 'GET', [], {}, ]
        per_url_name = permission_val[0]
        per_method  = permission_val[1]
        perm_args = permission_val[2]
        perm_kwargs = permission_val[3]
        perm_hook_func = permission_val[4] if len(permission_val) > 4 else None

        #如果当前访问的url_name匹配上了权限里面定义的url_name
        if per_url_name == current_url_name:
            #url_name匹配上，接着匹配方法（post,get....）
            if per_method == request.method:
                # if not  perm_args: #if no args defined in perm dic, then set this request to passed perm

                #逐个匹配参数，看每个参数是否都能对应的上。
                args_matched = False      #for args only
                for item in perm_args:
                    #通过反射获取到request.xxx函数   这里request_methon_func = request.GET/request.POST
                    request_method_func = getattr(request,per_method)

                    if request_method_func.get(item,None):   # request字典中有此参数
                        args_matched = True
                    else:
                        print("arg not match......")
                        args_matched = False
                        break          # 有一个参数不能匹配成功，则判定为假，退出该循环。因为可能有很多参数，必须所有参数都一样才匹配成功
                else:         # perm_dic里面的参数可能定义的就是空的，就走这里
                    args_matched = True

                #匹配有特定值的参数
                kwargs_matched = False
                for k,v in perm_kwargs.items():
                    request_method_func = getattr(request,

 per_method)
                    arg_val = request_method_func.get(k, None)  # request字典中有此参数
                    # print("perm kwargs check:",arg_val,type(arg_val),v,type(v))
                    if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值， 比如，需要request 对象里必须有一个叫 user_id=3的参数
                        kwargs_matched = True
                    else:
                        kwargs_matched = False
                        break # 有一个参数不能匹配成功，则判定为假，退出该循环。

                else:
                    kwargs_matched = True

                #开始匹配自定义权限钩子函数

                match_results = [args_matched,kwargs_matched]
                #列表里面的元素都为真
                if all(match_results): #都匹配上了
                    match_key = permission_key
                    break

    if all(match_results):
        #主要是获取到app_name
        app_name, *per_name = match_key.split('_')
        #per_obj = 例如：crm.crm_obj_list
        perm_obj = '%s.%s' % (app_name,match_key)
        if request.user.has_perm(perm_obj):
            return True
        else:
            print('当前用户没有该权限')
            return False

    else:
        print("未匹配到权限项，当前用户无权限")

def check_permission(func):
    def inner(*args,**kwargs):
        if not perm_check(*args,**kwargs):
            request = args[0]
            return render(request,'kingadmin/page_403.html')
        return func(*args,**kwargs)
    return  inner


"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


"""Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """


"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix

        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if True, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


"""Base classes for metric annotators for the standings generator.

Each metric annotator is responsible for computing a particular metric for each
item and annotating the standings with them, for example, in the case of teams,
number of wins (points), or draw strength. Subclasses should be defined in
context-specific files, e.g., teams.py or speakers.py.
"""

import logging

from django.db.models import Case, F, When

logger = logging.getLogger(__name__)

def metricgetter(items, negate=None):
    """Returns a callable object that fetches each item in `items` from its
    operand's `metrics` attribute, and returns a tuple containing the results.
    The tuple will have the same number for elements as `items`.

    For example:
     - After `f = metricgetter(("a",))`, the call `f(x)` returns `(x.metrics["a"],)`.
     - After `g = metricgetter((4, 9))`, the call `g(x)` returns `(x.metrics[4], x.metrics[9])`.

    If the metric is None (e.g. no scores so can't calculate stdev), use 0 instead to sort.
    """

    if negate is None:

        def metricitemgetter(x):
            return tuple(x.metrics[item] or 0 for item in items)

    else:
        assert len(items) == len(negate), "items had %d items but negate had %d" % (len(items), len(negate))
        coeffs = [-1 if neg else 1 for neg in negate]

        def metricitemgetter(x):
            return tuple(coeff * (x.metrics[item] or 0) for (coeff, item) in zip(coeffs, items))

    return metricitemgetter

class BaseMetricAnnotator:
    """Base class for all metric annotators.

    A metric annotator is a class that adds a metric to a Standings object.
    Subclasses must implement the method `annotate()`. Every annotator
    must add precisely one metric.

    Subclasses must set the `key`, `name` and `abbr` attributes.

    The default constructor does nothing, but subclasses may have constructors
    that initialise themselves with parameters."""

    key = None  # must be set by subclasses
    name = None  # must be set by subclasses
    abbr = None  # must be set by subclasses
    icon = None
    ranked_only = False
    repeatable = False
    listed = True
    ascending = False  # if Tru

e, this metric is sorted in ascending order, not descending
    combinable = False  # if True, use single query with all combinable metrics

    def run(self, queryset, standings, round=None):
        standings.record_added_metric(self.key, self.name, self.abbr, self.icon, self.ascending)
        self.annotate(queryset, standings, round)

    def annotate(self, queryset, standings, round=None):
        """Annotates the given `standings` by calling `add_metric()` on every
        `StandingInfo` object in `standings`.

        `queryset` is the queryset on which the standings are produced.
        `standings` is a `Standings` object.
        `round`, if specified, is a `Round` object that is assumed to be in the
            relevant tournament.
        """
        raise NotImplementedError("BaseMetricAnnotator subclasses must implement annotate()")

class RepeatedMetricAnnotator(BaseMetricAnnotator):
    """Base class for metric annotators that can be used multiple times.

    Subclasses should set the `key_prefix`, `name_prefix` and `abbr_prefix`
    class attributes, and use the `key` attribute when adding metrics in
    implementing `annotate()`."""

    ranked_only = True  # Repeated metrics don't make sense outside the precedence
    repeatable = True

    def __init__(self, index, keys):
        self.index = index
        self.key = self.key_prefix + str(index)
        self.name = self.name_prefix + " " + str(index)
        self.abbr = self.abbr_prefix + str(index)
        self.keys = keys

class QuerySetMetricAnnotator(BaseMetricAnnotator):
    """Base class for annotators that metrics based on conditional aggregations."""
    combinable = True

    def get_annotation(self, round):
        raise NotImplementedError("Subclasses of QuerySetMetricAnnotator must implement get_annotation().")

    def get_annotated_queryset(self, queryset, round=None):
        """Returns a QuerySet annotated with the metric given."""
        annotation = self.get_annotation(round=round)
        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
        self.queryset_annotated = True
        return queryset.annotate(**{self.key: annotation})

    def get_ranking_annotation(self, min_field, min_rounds):
        if min_rounds is None:
            return F(self.key)
        return Case(When(**{min_field + "__gte": min_rounds, "then": F(self.key)}))

    def annotate_with_queryset(self, queryset, standings):
        """Annotates items with the given QuerySet."""
        for item in queryset:
            standings.add_metric(item, self.key, getattr(item, self.key))

    def annotate(self, queryset, standings, round=None):
        if self.combinable:
            assert self.queryset_annotated, "get_annotated_queryset() must be run before annotate()"
            self.annotate_with_queryset(queryset, standings)
        else:
            self.annotate_with_queryset(self.get_annotated_queryset(queryset, round), standings)


# Preamble to mimick book environment
import logging
from pprint import pprint
from sys import stdout as STDOUT

import app

class Dialog(object):

pass

save_dialog = Dialog()

def show():
    print('Showing the dialog!')

def configure():
    save_dialog.save_dir = app.prefs.get('save_dir')


# Preamble to mimick book environment
import logging
from pprint import pprint
from sys import stdout as STDOUT

import app

class Dialog(object):
    def __init__(self):
        pass

save_dialog = Dialog()

def show():

def configure():
    save_dialog.save_dir = app.prefs.get('save_dir')


# Preamble to mimick book environment
import logging
from pprint import pprint
from sys import stdout as STDOUT

import app

class Dialog(object):
    def __init__(self):
        pass

save_dialog = Dialog()

def show():
    print('Showing the dialog!')

def configure():
    save_dialog.save_dir = app.prefs.get('save_