Changeset - c3487917ceba
[Not reviewed]
0 2 0
Lance Edgar (lance) - 3 years ago 2021-11-06 17:35:15
lance@edbob.org
Only show POD image if so configured; use "image not found" fallback

also update a random docstring
2 files changed with 26 insertions and 10 deletions:
0 comments (0 inline, 0 general)
rattail/config.py
Show inline comments
 
@@ -487,192 +487,196 @@ class RattailConfig(object):
 

	
 
    def export_filepath(self, key, uuid, filename=None, makedirs=False):
 
        """
 
        Returns absolute path to export data file, generated from the given args.
 
        """
 
        rootdir = self.export_filedir(key)
 
        filedir = os.path.join(rootdir, uuid[:2], uuid[2:])
 
        if makedirs and not os.path.exists(filedir):
 
            os.makedirs(filedir)
 
        if filename:
 
            return os.path.join(filedir, filename)
 
        return filedir
 

	
 
    def upgrade_filedir(self):
 
        """
 
        Returns path to root folder where upgrade files are stored.
 
        """
 
        path = os.path.abspath(self.require('rattail.upgrades', 'files'))
 
        return path
 

	
 
    def upgrade_filepath(self, uuid, filename=None, makedirs=False):
 
        """
 
        Returns absolute path to upgrade data file, generated from the given args.
 
        """
 
        rootdir = self.upgrade_filedir()
 
        filedir = os.path.join(rootdir, uuid[:2], uuid[2:])
 
        if makedirs and not os.path.exists(filedir):
 
            os.makedirs(filedir)
 
        if filename:
 
            return os.path.join(filedir, filename)
 
        return filedir
 

	
 
    def upgrade_command(self, default='/bin/sleep 30'):
 
        """
 
        Returns command to be used when performing upgrades.
 
        """
 
        # NOTE: we don't allow command to be specified in DB, for security reasons..
 
        return self.getlist('rattail.upgrades', 'command', default=default, usedb=False)
 

	
 
    def base_url(self):
 
        """
 
        Returns the configured "base" (root) URL for the web app.
 
        """
 
        # first try "generic" config option
 
        url = self.get('rattail', 'base_url')
 

	
 
        # or use tailbone as fallback, since it's most likely
 
        if url is None:
 
            url = self.get('tailbone', 'url')
 

	
 
        if url is not None:
 
            return url.rstrip('/')
 

	
 
    def datasync_url(self):
 
        """
 
        Returns configured URL for managing datasync daemon.
 
        """
 
        return self.get('rattail.datasync', 'url')
 

	
 
    def get_enum(self, **kwargs):
 
        """
 
        Returns a reference to configured 'enum' module; defaults to
 
        :mod:`rattail.enum`.
 
        """
 
        kwargs.setdefault('usedb', False)
 
        spec = self.get('rattail', 'enum', default='rattail.enum', **kwargs)
 
        return import_module_path(spec)
 

	
 
    def get_model(self):
 
        """
 
        Returns a reference to configured 'model' module; defaults to
 
        :mod:`rattail.db.model`.
 
        """
 
        spec = self.get('rattail', 'model', default='rattail.db.model', usedb=False)
 
        return import_module_path(spec)
 

	
 
    def get_trainwreck_model(self):
 
        """
 
        Returns a reference to the configured data 'model' module for
 
        Trainwreck.  Note that there is *not* a default value for this; it must
 
        be configured.
 
        """
 
        spec = self.require('rattail.trainwreck', 'model', usedb=False)
 
        return import_module_path(spec)
 

	
 
    def product_key(self, default='upc'):
 
        """
 
        Returns the name of the attribute which should be treated as the
 
        canonical product key field, e.g. 'upc' or 'item_id' etc.
 
        """
 
        return self.get('rattail', 'product.key', default=default) or default
 

	
 
    def product_key_title(self, key=None):
 
        """
 
        Returns the title string to be used when displaying product key field,
 
        e.g. "UPC" or "Part No." etc.
 

	
 
        :param key: The product key for which to return a label.  This
 
           is optional; if not specified then :meth:`product_key()`
 
           will be called to determine the key.
 
        """
 
        title = self.get('rattail', 'product.key_title')
 
        if title:
 
            return title
 
        if not key:
 
            key = self.product_key()
 
        if key == 'upc':
 
            return "UPC"
 
        if key == 'item_id':
 
            return "Item ID"
 
        return prettify(key)
 

	
 
    def single_store(self):
 
        """
 
        Returns boolean indicating whether the system is configured to behave
 
        as if it belongs to a single Store.
 
        """
 
        return self.getbool('rattail', 'single_store', default=False)
 

	
 
    def get_store(self, session):
 
        """
 
        Returns a :class:`rattail.db.model.Store` instance corresponding to app
 
        config, or ``None``.
 
        """
 
        from rattail.db import api
 

	
 
        store = self.get('rattail', 'store')
 
        if store:
 
            return api.get_store(session, store)
 

	
 

	
 
    ##############################
 
    # deprecated methods
 
    ##############################
 

	
 
    def options(self, section):
 
        warnings.warn("RattailConfig.option() is deprecated, please find "
 
                      "another way to accomplish what you're after.",
 
                      DeprecationWarning)
 
        return self.parser.options(section)
 

	
 
    def has_option(self, section, option):
 
        warnings.warn("RattailConfig.has_option() is deprecated, please find "
 
                      "another way to accomplish what you're after.",
 
                      DeprecationWarning)
 
        return self.parser.has_option(section, option)
 

	
 

	
 
class ConfigExtension(object):
 
    """
 
    Base class for all config extensions.
 
    """
 
    key = None
 

	
 
    def __repr__(self):
 
        return "ConfigExtension(key={0})".format(repr(self.key))
 

	
 
    def configure(self, config):
 
        """
 
        All subclasses should override this method, to extend the config object
 
        in any way necessary etc.
 
        """
 

	
 

	
 
def make_config(files=None, usedb=None, preferdb=None, env=None, winsvc=None, extend=True, versioning=None):
 
    """
 
    Returns a new config object, initialized with the given parameters and
 
    further modified by all registered config extensions.
 

	
 
    :param versioning: Controls whether or not the versioning system is
 
       configured with the new config object.  If ``True``, versioning will be
 
       configured.  If ``False`` then it will not be configured.  If ``None``
 
       (the default) then versioning will be configured only if the config
 
       object itself says that it should be.
 
    """
 
    if env is None:
 
        env = os.environ
 
    if files is None:
 
        files = env.get('RATTAIL_CONFIG_FILES')
 
        if files is not None:
 
            files = files.split(os.pathsep)
 
        else:
 
            files = default_system_paths() + default_user_paths()
 
    elif isinstance(files, six.string_types):
 
        files = [files]
 

	
 
    # If making config for a Windows service, we must read the default config
 
    # file(s) first, and check it to see if there is an alternate config file
 
    # which should be considered the "root" file.  Normally we specify the root
 
    # config file(s) via command line etc., but there is no practical way to
 
    # pass parameters to a Windows service.  This way we can effectively do
 
    # just that, via config.
 
    if winsvc is not None:
 
        parser = configparser.SafeConfigParser()
 
        parser.read(files)
 
        if parser.has_section('rattail.config'):
rattail/products.py
Show inline comments
 
@@ -91,203 +91,215 @@ class ProductsHandler(GenericHandler):
 
                                             get_product_by_scancode,
 
                                             get_product_by_code,
 
                                             get_product_by_vendor_code)
 

	
 
        model = self.model
 
        only = kwargs.get('only')
 
        if isinstance(only, six.string_types):
 
            only = [only]
 
        vendor = kwargs.get('vendor')
 
        include_keys = kwargs.get('include_keys', False)
 
        products = []
 

	
 
        # don't bother if we're given empty value
 
        if not value:
 
            return products
 

	
 
        # TODO: most of the function calls below are only good for "at
 
        # most one" result.  in some cases there may be more than one
 
        # match in fact, which will raise an error.  need to refactor
 
        # somehow to account for that..for now just pass `only` param
 
        # to avoid the problematic keys for your situation.
 

	
 
        # maybe look for 'uuid' match
 
        if not only or 'uuid' in only:
 
            product = session.query(model.Product).get(value)
 
            if product:
 
                products.append(('uuid', product))
 

	
 
        # maybe look for 'upc' match
 
        if not only or 'upc' in only:
 

	
 
            # if value is a GPC we kind of only have one thing to try
 
            if isinstance(value, GPC):
 
                product = get_product_by_upc(session, value)
 
                if product:
 
                    products.append(('upc', product))
 

	
 
            else: # not GPC, so must convert
 

	
 
                if value.isdigit():
 

	
 
                    # we first assume the value *does* include check digit
 
                    provided = GPC(value, calc_check_digit=False)
 
                    product = get_product_by_upc(session, provided)
 
                    if product:
 
                        products.append(('upc', product))
 

	
 
                    # but we can also calculate a check digit and try that
 
                    checked = GPC(value, calc_check_digit='upc')
 
                    product = get_product_by_upc(session, checked)
 
                    if product:
 
                        products.append(('upc', product))
 

	
 
                    # one last trick is to expand UPC-E to UPC-A and then reattempt
 
                    # the lookup, *with* check digit (since it would be known)
 
                    if len(value) in (6, 8):
 
                        checked = GPC(upce_to_upca(value), calc_check_digit='upc')
 
                        product = get_product_by_upc(session, checked)
 
                        if product:
 
                            products.append(('upc', product))
 

	
 
        # maybe look for 'item_id' match
 
        if not only or 'item_id' in only:
 
            product = get_product_by_item_id(session, value)
 
            if product:
 
                products.append(('item_id', product))
 

	
 
        # maybe look for 'scancode' match
 
        if not only or 'scancode' in only:
 
            product = get_product_by_scancode(session, value)
 
            if product:
 
                products.append(('scancode', product))
 

	
 
        # maybe look for 'altcode' match
 
        if not only or 'altcode' in only:
 
            product = get_product_by_code(session, value)
 
            if product:
 
                products.append(('altcode', product))
 

	
 
        # maybe look for 'sku' match
 
        if not only or 'sku' in only:
 
            product = get_product_by_vendor_code(session, value,
 
                                                 vendor=vendor)
 
            if product:
 
                products.append(('sku', product))
 

	
 
        # maybe strip keys out of the result
 
        if not include_keys:
 
            products = [tup[1] for tup in products]
 

	
 
        return products
 

	
 
    def get_image_url(self, product=None, upc=None, **kwargs):
 
        """
 
        Return the preferred image URL for the given UPC or product.
 
        """
 
        base_url = self.config.base_url()
 

	
 
        # we prefer the "image on file" if available
 
        if product and product.image:
 
            url = self.config.base_url()
 
            if url:
 
                return '{}/products/{}/image'.format(url, product.uuid)
 

	
 
        # fallback to the POD image, if available
 
        if product and not upc:
 
            upc = product.upc
 
        if upc:
 
            return self.get_pod_image_url(upc)
 
        if base_url and product and product.image:
 
            return '{}/products/{}/image'.format(base_url, product.uuid)
 

	
 
        # and if this product is a pack item, then we prefer the unit
 
        # item image as fallback, if available
 
        if base_url and product and product.is_pack_item():
 
            unit = product.unit
 
            if unit and unit.image:
 
                return '{}/products/{}/image'.format(base_url, unit.uuid)
 

	
 
        # fallback to the POD image, if available and so configured
 
        if self.config.getbool('tailbone', 'products.show_pod_image',
 
                               default=False):
 
            if product and not upc:
 
                upc = product.upc
 
            if upc:
 
                return self.get_pod_image_url(upc)
 

	
 
        if base_url:
 
            return '{}/tailbone/img/product.png'.format(base_url)
 

	
 
    def get_pod_image_url(self, upc, **kwargs):
 
        """
 
        Return the POD image URL for the given UPC.
 
        """
 
        if upc:
 
            return pod.get_image_url(self.config, upc)
 

	
 
    def render_price(self, price, html=False, **kwargs):
 
        """
 
        Render the given ``price`` object as text.
 

	
 
        :returns: String containing the rendered price, or ``None`` if
 
           nothing was applicable.
 
        """
 
        if price.price is not None and price.pack_price is not None:
 
            if price.multiple > 1:
 
                return "{} / {}  ({} / {})".format(
 
                    self.app.render_currency(price.price),
 
                    price.multiple,
 
                    self.app.render_currency(price.pack_price),
 
                    price.pack_multiple)
 
            return "{}  ({} / {})".format(
 
                self.app.render_currency(price.price),
 
                self.app.render_currency(price.pack_price),
 
                price.pack_multiple)
 
        if price.price is not None:
 
            if price.multiple is not None and price.multiple > 1:
 
                return "{} / {}".format(
 
                    self.app.render_currency(price.price),
 
                    price.multiple)
 
            return self.app.render_currency(price.price)
 
        if price.pack_price is not None:
 
            return "{} / {}".format(
 
                self.app.render_currency(price.pack_price),
 
                price.pack_multiple)
 

	
 
    def get_uom_sil_codes(self, session, uppercase=False, **kwargs):
 
        """
 
        This should return a dict, keys of which are UOM abbreviation strings,
 
        and values of which are corresponding SIL code strings.
 

	
 
        :param session: Reference to current Rattail DB session.
 
        :param uppercase: Set to ``True`` to cause all UOM abbreviations to be
 
           upper-cased when adding to the map.
 
        :returns: Dictionary containing all known UOM / SIL code mappings.
 
        """
 
        model = self.model
 

	
 
        def normalize(uom):
 
            if uom.sil_code:
 
                return uom.sil_code
 

	
 
        def make_key(uom, normal):
 
            key = uom.abbreviation
 
            if uppercase:
 
                key = key.upper()
 
            return key
 

	
 
        return self.cache_model(session, model.UnitOfMeasure,
 
                                normalizer=normalize,
 
                                key=make_key)
 

	
 
    def get_uom_sil_code(self, session, uom, uppercase=False, **kwargs):
 
        """
 
        This should return a SIL code which corresponds to the given UOM
 
        abbreviation string.  Useful when you just need one out of the blue,
 
        but if you need multiple codes looked up then you're probably better
 
        off using :meth:`get_uom_sil_codes()` for efficiency.
 

	
 
        :param session: Reference to current Rattail DB session.
 
        :param uppercase: Set to ``True`` to cause the UOM abbreviation to be
 
           upper-cased before performing the lookup.  This effectively makes
 
           the search case-insensitive.
 
        :param uom:  Unit of measure as abbreviated string, e.g. ``'LB'``.
 
        :returns: SIL code for the UOM, as string (e.g. ``'49'``), or ``None``
 
           if no matching code was found.
 
        """
 
        model = self.model
 
        query = session.query(model.UnitOfMeasure)
 
        if uppercase:
 
            query = query.filter(sa.func.upper(model.UnitOfMeasure.abbreviation) == uom.upper())
 
        else:
 
            query = query.filter(model.UnitOfMeasure.abbreviation == uom)
 
        try:
 
            match = query.one()
 
        except orm.exc.NoResultFound:
 
            pass
 
        else:
 
            return match.sil_code
 

	
 
    def collect_wild_uoms(self, **kwargs):
 
        """
 
        Collect all UOM abbreviations "from the wild" and ensure each is
 
        represented within the Rattail Units of Measure table.
 

	
0 comments (0 inline, 0 general)