class InvCard(Entity):
    name = Field(UnicodeText, index=True)
    set_name = Field(UnicodeText)
    box = Field(UnicodeText)
    scan_png = Field(BLOB)
    box_index = Field(Integer)
    recognition_status = Field(
        Enum('scanned', 'candidate_match', 'incorrect_match', 'verified'))
    inventory_status = Field(Enum('present', 'temporarily_out',
                                  'permanently_gone'),
                             index=True)
    is_foil = Field(Boolean, default=False)
    language = Field(UnicodeText, default=u'english')
    condition = Field(Enum('mint', 'near_mint', 'good', 'heavy_play'))

    inv_logs = OneToMany('InvLog')
    fix_log = OneToOne('FixLog')

    rowid = Field(Integer, primary_key=True)

    using_options(tablename='inv_cards')

    def most_recent_log(self):
        return sorted(self.inv_logs, key=lambda x: x.date)[-1]

    def __unicode__(self):
        return "<%s/%s (%s/%s)>" % (self.set_name, self.name, self.box,
                                    self.box_index)

    def __str__(self):
        return unicode(self).encode(sys.stdout.encoding)
Beispiel #2
0
class Yeast(Ingredient):

    using_options(inheritance='multi', polymorphic=True)

    TYPES = ['ALE', 'LAGER', 'WILD', 'MEAD', 'CIDER', 'WINE']

    FORMS = ['DRY', 'LIQUID']

    FLOCCULATION_VALUES = [
        'LOW', 'LOW/MEDIUM', 'MEDIUM', 'MEDIUM/HIGH', 'HIGH'
    ]

    type = Field(Enum(*TYPES, native_enum=False))
    form = Field(Enum(*FORMS, native_enum=False))
    attenuation = Field(Float())
    flocculation = Field(Enum(*FLOCCULATION_VALUES, native_enum=False))

    additions = OneToMany('RecipeAddition', inverse='yeast')

    def __json__(self):
        json = super(Yeast, self).__json__()
        json.update({
            'form': self.form.capitalize(),
            'attenuation': self.attenuation
        })
        return json
Beispiel #3
0
class Ingredient(Entity, ShallowCopyMixin):

    using_options(inheritance='multi', polymorphic=True)

    uid = Field(Unicode(32), unique=True)
    name = Field(Unicode(256))
    description = Field(UnicodeText)
    default_unit = Field(Enum(*UNITS, native_enum=False),
                         default='POUND',
                         nullable=True)

    @property
    def printed_name(self):
        return self.name

    @property
    def printed_origin(self):
        origin = self.origin
        if len(origin) > 2:
            origin = origin.title()
        return origin

    def __json__(self):
        return {
            'id': self.id,
            'class': self.__class__.__name__,
            'name': self.printed_name,
            'default_unit': self.default_unit
        }
Beispiel #4
0
class FermentationStep(Entity, DeepCopyMixin):

    STEPS = ('PRIMARY', 'SECONDARY', 'TERTIARY')

    step = Field(Enum(*STEPS, native_enum=False))
    days = Field(Integer)
    fahrenheit = Field(Float)

    recipe = ManyToOne('Recipe', inverse='fermentation_steps')

    @property
    def celsius(self):
        return round((5 / 9.0) * (self.fahrenheit - 32))

    @celsius.setter  # noqa
    def celsius(self, v):
        self.fahrenheit = ((9 / 5.0) * v) + 32

    def __json__(self):
        json = {
            'step': self.step,
            'days': self.days,
            'fahrenheit': self.fahrenheit
        }
        return json
Beispiel #5
0
class HopAddition(RecipeAddition):

    FORMS = ('LEAF', 'PELLET', 'PLUG')

    form = Field(Enum(*FORMS, native_enum=False), default='LEAF')
    alpha_acid = Field(Float())

    using_options(inheritance='multi', polymorphic=True)

    @property
    def printable_amount(self):
        unit = self.unit
        if self.amount == 0:
            unit = 'OUNCE'
        if getattr(self.recipe, 'unit_system', None) == 'METRIC':
            return UnitConvert.to_str(*to_metric(self.amount, unit))
        return UnitConvert.to_str(self.amount, unit)

    @property
    def eta(self):
        """
        The number of minutes (after the boil starts) at which to add
        the hop addition.

        For a 15 minute addition in a 60 minute boil, the `eta` would be 45m
        """
        offset = self.recipe.boil_minutes - self.minutes
        return '%sm' % offset

    def __json__(self):
        json = super(HopAddition, self).__json__()
        json.update({'form': self.form, 'alpha_acid': self.alpha_acid})
        return json
Beispiel #6
0
class Extra(Ingredient):

    using_options(inheritance='multi', polymorphic=True)

    TYPES = ['SPICE', 'FINING', 'WATER AGENT', 'HERB', 'FLAVOR', 'OTHER']

    additions = OneToMany('RecipeAddition', inverse='extra')

    type = Field(Enum(*TYPES, native_enum=False))
    liquid = Field(Boolean)
Beispiel #7
0
class ScannedImage(Entity):
    #this is listed as ManyToOne, though there will only ever be
    #a single scanned image per card
    card = ManyToOne('models.Card')
    status = Field(Enum('unprocessed', 'candidate_match', 'confident_match'),
                   index=True)
    scan_png = Field(BLOB)

    using_options(tablename='scanned_images')
    using_table_options(schema='scanned_images')
Beispiel #8
0
class Fermentable(Ingredient):

    using_options(inheritance='multi', polymorphic=True)

    TYPES = ['MALT', 'GRAIN', 'ADJUNCT', 'EXTRACT', 'SUGAR']

    type = Field(Enum(*TYPES, native_enum=False))
    ppg = Field(Integer)
    lovibond = Field(Float)
    origin = Field(Enum(*ORIGINS, native_enum=False))

    additions = OneToMany('RecipeAddition', inverse='fermentable')

    @property
    def printed_name(self):
        return '%s (%s)' % (self.name, self.printed_origin)

    @property
    def printed_type(self):
        if self.type is None:
            return 'Grain'
        value = self.type.capitalize()
        if value == 'Malt':
            value = 'Grain'
        return value

    @property
    def percent_yield(self):
        return round((self.ppg / 46.00) * 100)

    def __json__(self):
        json = super(Fermentable, self).__json__()
        json.update({
            'ppg': self.ppg,
            'lovibond': self.lovibond,
            'printed_type': self.printed_type
        })
        return json
Beispiel #9
0
class Hop(Ingredient):

    using_options(inheritance='multi', polymorphic=True)

    alpha_acid = Field(Float())
    origin = Field(Enum(*ORIGINS, native_enum=False))

    additions = OneToMany('RecipeAddition', inverse='hop')

    @property
    def printed_name(self):
        return '%s (%s)' % (self.name, self.printed_origin)

    def __json__(self):
        json = super(Hop, self).__json__()
        json.update({'alpha_acid': self.alpha_acid})
        return json
Beispiel #10
0
class Contrib(Entity):
    """Mapper for the `contrib' entity
    """
    using_options(shortnames=True)

    title = Field(String(256))
    content = Field(String(600))
    original = Field(String(600), default='')
    status = Field(Boolean, default=False)
    enabled = Field(Boolean, default=True)
    parent = Field(Integer, default=0)
    moderation = Field(Boolean, default=False)
    user = ManyToOne('User')
    creation_date = Field(DateTime, default=datetime.now)
    children = ManyToMany('Contrib')
    theme = Field(Enum(
        u'cuidado', u'familia', u'emergencia',
        u'medicamentos', u'regional'
    ))
Beispiel #11
0
class Buzz(Entity):
    """Mapper for the `buzz' entity
    """
    using_options(shortnames=True)

    owner_nick = Field(Unicode(64))
    owner_email = Field(Unicode(128))
    owner_avatar = Field(Unicode(256))
    content = Field(Unicode(512))
    status = Field(Enum(u'inserted', u'approved', u'selected', u'published'),
                   default=u'inserted')
    date_published = Field(DateTime)
    creation_date = Field(DateTime, default=datetime.now)
    audience_id = Field(Integer)
    type_ = ManyToOne('BuzzType')
    user = ManyToOne('User')

    face_msg_id = Field(Unicode(50), unique=True)
    """
    ALTER TABLE `buzz` ADD COLUMN `face_msg_id` VARCHAR(50) NULL
    , ADD UNIQUE INDEX `face_msg_id_UNIQUE` (`face_msg_id` ASC) ;
    """

    def __str__(self):
        return '<%s "%s">' % (self.__class__.__name__, self.content)

    def to_dict(self, deep=None):
        """Just a shortcut for `super.to_dict' passing the `type_' field
        to the `deep' attribute by default.
        """
        if deep is None:
            deep = { 'type_': {}, 'user': {} }
        base = super(Buzz, self).to_dict(deep=deep)
        base['avatar'] = self.avatar
        if base['user'] and self.user:
            base['user'] = self.user.public_dict()
        return base

    @property
    def avatar(self):
        """Returns the avatar of the user that posted a notice"""
        return self.owner_avatar or self.user.avatar_url
class InvLog(Entity):
    card = ManyToOne('InvCard')
    direction = Field(Enum('added', 'removed'))
    reason = Field(UnicodeText)
    date = Field(DateTime)
    #I wish I had a ActiveRecord-like 'timestamps' here

    rowid = Field(Integer, primary_key=True)

    using_options(tablename='inv_logs')

    def __repr__(self):
        if self.direction == u'added':
            dir_text = 'added to'
        else:
            dir_text = 'removed_from'
        card_repr = "%s/%s(%d)" % (self.card.set_name, self.card.name,
                                   self.card.rowid)

        return "<%s: %s %s %s. \"%s\">" % (self.date, card_repr, dir_text,
                                           self.card.box, self.reason)
Beispiel #13
0
class RecipeAddition(Entity, DeepCopyMixin):

    USES = ('MASH', 'FIRST WORT', 'BOIL', 'POST-BOIL', 'FLAME OUT', 'PRIMARY',
            'SECONDARY', 'TERTIARY')

    using_options(inheritance='multi', polymorphic=True)

    amount = Field(Float)

    #
    # At the database level, only certain units are actually stored.
    # We do this for the sake of uniformity (to make calculations easier).
    #
    unit = Field(Enum(
        *['POUND', 'OUNCE', 'TEASPOON', 'TABLESPOON', 'GALLON', 'LITER'],
        native_enum=False),
                 nullable=True)

    use = Field(Enum(*USES, native_enum=False))
    duration = Field(Interval)

    recipe = ManyToOne('Recipe', inverse='additions')
    fermentable = ManyToOne('Fermentable', inverse='additions')
    hop = ManyToOne('Hop', inverse='additions')
    yeast = ManyToOne('Yeast', inverse='additions')
    extra = ManyToOne('Extra', inverse='extra')

    @property
    def printable_amount(self):
        if getattr(self.recipe, 'unit_system', None) == 'METRIC':
            return UnitConvert.to_str(*to_metric(self.amount, self.unit))

        return UnitConvert.to_str(self.amount, self.unit)

    @property
    def ingredient(self):
        for ingredient in ('fermentable', 'hop', 'yeast', 'extra'):
            match = getattr(self, ingredient, None)
            if match is not None:
                return match

    @property
    def pounds(self):
        if self.unit == 'POUND':
            return self.amount
        if self.unit == 'OUNCE':
            return self.amount / 16.0
        raise InvalidUnitException('Could not convert `%s` to pounds.' %
                                   self.unit)

    @property
    def minutes(self):
        if self.duration is None:
            return 0
        return self.duration.seconds / 60

    @property
    def sortable_minutes(self):
        if self.use == 'FIRST WORT':
            return maxint

        if self.use in ('POST BOIL', 'FLAME-OUT'):
            return -1

        return self.minutes

    @property
    def step(self):
        return ({
            'MASH': 'mash',
            'FIRST WORT': 'boil',
            'BOIL': 'boil',
            'POST-BOIL': 'boil',
            'FLAME OUT': 'boil',
            'PRIMARY': 'fermentation',
            'SECONDARY': 'fermentation',
            'TERTIARY': 'fermentation'
        })[self.use]

    @property
    def percentage(self):
        additions = getattr(self.recipe, self.step)
        return self.recipe._percent(additions).get(self, 0)

    def to_xml(self):
        from draughtcraft.lib.beerxml import export

        if self.hop:

            kw = {
                'name': self.hop.name,
                'alpha': self.hop.alpha_acid,
                'amount': to_kg(self.amount, self.unit),
                'time': self.minutes,
                'notes': self.hop.description,
                'form': self.form.capitalize(),
                'origin': self.hop.printed_origin
            }

            kw['use'] = {
                'MASH': 'Mash',
                'FIRST WORT': 'First Wort',
                'BOIL': 'Boil',
                'POST-BOIL': 'Aroma',
                'FLAME OUT': 'Aroma',
                'PRIMARY': 'Dry Hop',
                'SECONDARY': 'Dry Hop',
                'TERTIARY': 'Dry Hop'
            }.get(self.use)

            return export.Hop(**kw)

        if self.fermentable:
            kw = {
                'name': self.fermentable.name,
                'amount': to_kg(self.amount, self.unit),
                'yield': self.fermentable.percent_yield,
                'color': self.fermentable.lovibond,
                'add_after_boil': self.step == 'fermentation',
                'origin': self.fermentable.printed_origin,
                'notes': self.fermentable.description
            }

            kw['type'] = {
                'MALT': 'Grain',
                'GRAIN': 'Grain',
                'ADJUNCT': 'Adjunct',
                'EXTRACT': 'Extract',
                'SUGAR': 'Sugar'
            }.get(self.fermentable.type)

            if self.fermentable.type == 'EXTRACT' and \
                    'DME' in self.fermentable.name:
                kw['type'] = 'Dry Extract'

            return export.Fermentable(**kw)

        if self.yeast:
            kw = {
                'name': self.yeast.name,
                'form': self.yeast.form.capitalize(),
                'attenuation': self.yeast.attenuation * 100.00,
                'notes': self.yeast.description
            }

            # Map types as appropriately as possible to BeerXML <TYPE>'s.
            kw['type'] = {
                'ALE': 'Ale',
                'LAGER': 'Lager',
                'WILD': 'Ale',
                'MEAD': 'Wine',
                'CIDER': 'Wine',
                'WINE': 'Wine'
            }.get(self.yeast.type)

            if self.yeast.form == 'LIQUID':
                #
                # If the yeast is liquid, it's probably a activator/vial.  For
                # simplicity, we'll assume Wyeast's volume, 125ml.
                #
                kw['amount'] = 0.125
            else:
                #
                # If the yeast is dry, it's probably a small packet.  For
                # simplicity, we'll assume a standard weight of 11.5g.
                #
                kw['amount'] = 0.0115
                kw['amount_is_weight'] = True

            if self.use in ('SECONDARY', 'TERTIARY'):
                kw['add_to_secondary'] = True

            return export.Yeast(**kw)

        if self.extra:
            kw = {
                'name': self.extra.name,
                'type': string.capwords(self.extra.type),
                'time': self.minutes,
                'notes': self.extra.description
            }

            kw['use'] = {
                'MASH': 'Mash',
                'FIRST WORT': 'Boil',
                'BOIL': 'Boil',
                'POST-BOIL': 'Boil',
                'FLAME OUT': 'Boil',
                'PRIMARY': 'Primary',
                'SECONDARY': 'Secondary',
                'TERTIARY': 'Secondary'
            }.get(self.use)

            if self.unit is None:
                #
                # If there's no unit (meaning it's just one "unit"), assume
                # a weight of 15 grams.
                #
                kw['amount'] = 0.015
                kw['amount_is_weight'] = True
            elif self.extra.liquid:
                kw['amount'] = to_l(self.amount, self.unit)
            else:
                kw['amount'] = to_kg(self.amount, self.unit)
                kw['amount_is_weight'] = True

            return export.Misc(**kw)

    def __json__(self):
        return {
            'amount': 1 if self.yeast else self.amount,
            'unit': self.unit,
            'use': self.use,
            'minutes': self.minutes,
            'ingredient': self.ingredient
        }
Beispiel #14
0
class Recipe(Entity, DeepCopyMixin, ShallowCopyMixin):

    TYPES = ('MASH', 'EXTRACT', 'EXTRACTSTEEP', 'MINIMASH')

    MASH_METHODS = ('SINGLESTEP', 'TEMPERATURE', 'DECOCTION', 'MULTISTEP')

    STATES = ('DRAFT', 'PUBLISHED')

    type = Field(Enum(*TYPES, native_enum=False), default='MASH', index=True)
    name = Field(Unicode(256), index=True)
    gallons = Field(Float, default=5)
    boil_minutes = Field(Integer, default=60)
    notes = Field(UnicodeText)
    creation_date = Field(DateTime, default=datetime.utcnow)
    last_updated = Field(DateTime, default=datetime.utcnow, index=True)

    # Cached statistics
    _og = Field(Float, colname='og')
    _fg = Field(Float, colname='fg')
    _abv = Field(Float, colname='abv')
    _srm = Field(Integer, colname='srm')
    _ibu = Field(Integer, colname='ibu')

    mash_method = Field(Enum(*MASH_METHODS, native_enum=False),
                        default='SINGLESTEP')
    mash_instructions = Field(UnicodeText)

    state = Field(Enum(*STATES, native_enum=False), default='DRAFT')
    current_draft = ManyToOne('Recipe', inverse='published_version')
    published_version = OneToOne('Recipe',
                                 inverse='current_draft',
                                 order_by='creation_date')

    copied_from = ManyToOne('Recipe', inverse='copies')
    copies = OneToMany('Recipe',
                       inverse='copied_from',
                       order_by='creation_date')

    views = OneToMany('RecipeView',
                      inverse='recipe',
                      cascade='all, delete-orphan')
    additions = OneToMany('RecipeAddition',
                          inverse='recipe',
                          cascade='all, delete-orphan')
    fermentation_steps = OneToMany('FermentationStep',
                                   inverse='recipe',
                                   order_by='step',
                                   cascade='all, delete-orphan')
    slugs = OneToMany('RecipeSlug',
                      inverse='recipe',
                      order_by='id',
                      cascade='all, delete-orphan')
    style = ManyToOne('Style', inverse='recipes')
    author = ManyToOne('User', inverse='recipes')

    __ignored_properties__ = ('current_draft', 'published_version', 'copies',
                              'views', 'creation_date', 'state', '_og', '_fg',
                              '_abv', '_srm', '_ibu')

    def __init__(self, **kwargs):
        super(Recipe, self).__init__(**kwargs)
        if kwargs.get('name') and not kwargs.get('slugs'):
            self.slugs.append(entities.RecipeSlug(name=kwargs['name']))

    def duplicate(self, overrides={}):
        """
        Used to duplicate a recipe.

        An optional hash of `overrides` can be specified to override the
        default copied values, e.g.,

        dupe = user.recipes[0].duplicate({'author': otheruser})
        assert dupe.author == otheruser
        """
        # Make a deep copy of the instance
        copy = deepcopy(self)

        # For each override...
        for k, v in overrides.items():

            # If the key is already defined, and is a list (i.e., a ManyToOne)
            if isinstance(getattr(copy, k, None), list):

                #
                # Delete each existing entity, because we're about to
                # override the value.
                #
                for i in getattr(copy, k):
                    i.delete()

            # Set the new (overridden) value
            setattr(copy, k, v)

        return copy

    def draft(self):
        """
        Used to create a new, unpublished draft of a recipe.
        """
        if self.current_draft:
            self.current_draft.delete()
            self.current_draft = None

        return self.duplicate({'published_version': self})

    def publish(self):
        """
        Used to publish an orphan draft as a new recipe.
        """
        assert self.state == 'DRAFT', "Only drafts can be published."

        # If this recipe is a draft of another, merge the changes back in
        if self.published_version:
            return self.merge()

        # Otherwise, just set the state to PUBLISHED
        self.state = 'PUBLISHED'

        # Store cached values
        self._og = self.calculations.og
        self._fg = self.calculations.fg
        self._abv = self.calculations.abv
        self._srm = self.calculations.srm
        self._ibu = self.calculations.ibu

        # Generate a new slug if the existing one hasn't changed.
        existing = [slug.slug for slug in self.slugs]
        if entities.RecipeSlug.to_slug(self.name) not in existing:
            self.slugs.append(entities.RecipeSlug(name=self.name))

    def merge(self):
        """
        Used to merge a drafted recipe's changes back into its source.
        """

        # Make sure this is a draft with a source recipe
        assert self.state == 'DRAFT', "Only drafts can be merged."
        source = self.published_version
        assert source is not None, \
            "This recipe doesn't have a `published_version`."

        # Clone the draft onto the published version
        self.__copy_target__ = self.published_version
        deepcopy(self)

        # Delete the draft
        self.delete()

    @property
    def calculations(self):
        return Calculations(self)

    @property
    def efficiency(self):
        if self.author:
            return self.author.settings['brewhouse_efficiency']
        return .75

    @property
    def unit_system(self):
        if request.context['metric'] is True:
            return 'METRIC'
        return 'US'

    @property
    def metric(self):
        return self.unit_system == 'METRIC'

    @property
    def liters(self):
        liters = to_metric(*(self.gallons, "GALLON"))[0]
        return round(liters, 3)

    @liters.setter  # noqa
    def liters(self, v):
        gallons = to_us(*(v, "LITER"))[0]
        self.gallons = gallons

    def _partition(self, additions):
        """
        Partition a set of recipe additions
        by ingredient type, e.g.,:

        _partition([grain, grain2, hop])
        {'Fermentable': [grain, grain2], 'Hop': [hop]}
        """
        p = {}
        for a in additions:
            p.setdefault(a.ingredient.__class__, []).append(a)
        return p

    def _percent(self, partitions):
        """
        Calculate percentage of additions by amount
        within a set of recipe partitions.
        e.g.,

        _percent({'Fermentable': [grain, grain2], 'Hop': [hop]})
        {grain : .75, grain2 : .25, hop : 1}
        """

        percentages = {}
        for type, additions in partitions.items():
            total = sum([addition.amount for addition in additions])
            for addition in additions:
                if total:
                    percentages[addition] = float(
                        addition.amount) / float(total)
                else:
                    percentages[addition] = 0

        return percentages

    @property
    def mash(self):
        return self._partition([a for a in self.additions if a.step == 'mash'])

    @property
    def boil(self):
        return self._partition([a for a in self.additions if a.step == 'boil'])

    @property
    def fermentation(self):
        return self._partition(
            [a for a in self.additions if a.step == 'fermentation'])

    def contains(self, ingredient, step):
        if step not in ('mash', 'boil', 'fermentation'):
            return False

        additions = getattr(self, step)
        for a in sum(additions.values(), []):
            if a.ingredient == ingredient:
                return True

        return False

    @property
    def next_fermentation_step(self):
        """
        The next available fermentation step for a recipe.
        e.g., if temperature/length is already defined for "PRIMARY" a
        fermentation period, returns "SECONDARY".  If "SECONDARY" is already
        defined, returns "TERTIARY".

        Always returns one of `model.FermentationStep.STEPS`.
        """

        total = len(self.fermentation_steps)

        return {1: 'SECONDARY', 2: 'TERTIARY'}.get(total, None)

    def url(self, public=True):
        """
        The URL for a recipe.
        """
        return '/recipes/%s/%s/%s' % (
            ('%x' % self.id).lower(), self.slugs[-1].slug,
            '' if public else 'builder')

    @property
    def printable_type(self):
        return {
            'MASH': 'All Grain',
            'EXTRACT': 'Extract',
            'EXTRACTSTEEP': 'Extract w/ Steeped Grains',
            'MINIMASH': 'Mini-Mash'
        }[self.type]

    def touch(self):
        self.last_updated = datetime.utcnow()

    @property
    def og(self):
        return self._og

    @property
    def fg(self):
        return self._fg

    @property
    def abv(self):
        return self._abv

    @property
    def srm(self):
        return self._srm

    @property
    def ibu(self):
        return self._ibu

    def to_xml(self):
        from draughtcraft.lib.beerxml import export
        kw = {
            'name': self.name,
            'type': {
                'MASH': 'All Grain',
                'MINIMASH': 'Partial Mash'
            }.get(self.type, 'Extract'),
            'brewer': self.author.printed_name if self.author else 'Unknown',
            'batch_size': self.liters,
            'boil_size': self.liters * 1.25,
            'boil_time': self.boil_minutes,
            'notes': self.notes,
            'fermentation_stages': len(self.fermentation_steps),
        }

        hops = [a.to_xml() for a in self.additions if a.hop]
        fermentables = [a.to_xml() for a in self.additions if a.fermentable]
        yeast = [a.to_xml() for a in self.additions if a.yeast]
        extras = [a.to_xml() for a in self.additions if a.extra]

        kw['hops'] = hops
        kw['fermentables'] = fermentables
        kw['yeasts'] = yeast
        kw['miscs'] = extras

        kw['mash'] = []
        kw['waters'] = []

        if self.style is None:
            kw['style'] = export.Style(name='',
                                       category='No Style Chosen',
                                       type='None',
                                       category_number=0,
                                       style_letter='',
                                       og_min=0,
                                       og_max=0,
                                       ibu_min=0,
                                       ibu_max=0,
                                       color_min=0,
                                       color_max=0,
                                       fg_min=0,
                                       fg_max=0)
        else:
            kw['style'] = self.style.to_xml()

        if self.type != 'EXTRACT':
            kw['efficiency'] = self.efficiency * 100.00

        for stage in self.fermentation_steps:
            if stage.step == 'PRIMARY':
                kw['primary_age'] = stage.days
                kw['primary_temp'] = stage.celsius
            if stage.step == 'SECONDARY':
                kw['secondary_age'] = stage.days
                kw['secondary_temp'] = stage.celsius
            if stage.step == 'TERTIARY':
                kw['tertiary_age'] = stage.days
                kw['tertiary_temp'] = stage.celsius

        return export.Recipe(**kw).render()

    def __json__(self):
        from draughtcraft.templates.helpers import alphanum_key

        def inventory(cls, types=[]):
            return sorted([
                f.__json__() for f in cls.query.all()
                if not types or (types and f.type in types)
            ],
                          key=lambda f: alphanum_key(f['name']))

        #
        # Attempt to look up the preferred calculation method for the
        # recipe's author.
        #
        ibu_method = 'tinseth'
        user = self.author
        if user:
            ibu_method = user.settings.get('default_ibu_formula', 'tinseth')

        return {
            # Basic attributes
            'name':
            self.name,
            'author':
            self.author.username if self.author else '',
            'style':
            self.style.id if self.style else None,
            'gallons':
            self.gallons,

            # Ingredients
            'mash':
            filter(lambda a: a.step == 'mash', self.additions),
            'boil':
            filter(lambda a: a.step == 'boil', self.additions),
            'fermentation':
            filter(lambda a: a.step == 'fermentation', self.additions),
            'ibu_method':
            ibu_method,
            'efficiency':
            self.efficiency,

            # Inventory
            'inventory': {
                'malts':
                inventory(entities.Fermentable,
                          ('MALT', 'GRAIN', 'ADJUNCT', 'SUGAR')),
                'extracts':
                inventory(entities.Fermentable, ('EXTRACT', )),
                'hops':
                inventory(entities.Hop),
                'yeast':
                inventory(entities.Yeast),
                'extras':
                inventory(entities.Extra)
            },

            # Extras
            'mash_method':
            self.mash_method,
            'mash_instructions':
            self.mash_instructions,
            'boil_minutes':
            self.boil_minutes,
            'fermentation_steps':
            self.fermentation_steps,
            'notes':
            self.notes,
            'metric':
            self.metric
        }
Beispiel #15
0
class Style(Entity, ShallowCopyMixin):

    TYPES = [
        'LAGER',
        'ALE',
        'MEAD',
        'CIDER'
    ]

    uid = Field(Unicode(32), unique=True)
    name = Field(Unicode(256), index=True)
    url = Field(Unicode(256))

    # Gravities
    min_og = Field(Float)
    max_og = Field(Float)
    min_fg = Field(Float)
    max_fg = Field(Float)

    # IBU
    min_ibu = Field(Integer)
    max_ibu = Field(Integer)

    # SRM
    min_srm = Field(Integer)
    max_srm = Field(Integer)

    # ABV
    min_abv = Field(Float)
    max_abv = Field(Float)

    category = Field(Unicode(64))
    category_number = Field(Integer)
    style_letter = Field(Unicode(1))

    type = Field(Enum(*TYPES, native_enum=False))

    recipes = OneToMany('Recipe', inverse='style')

    def defined(self, statistic):
        if statistic not in (
            'og',
            'fg',
            'abv',
            'srm',
            'ibu'
        ):
            raise InvalidStatistic('Invalid statistic, %s' % statistic)

        minimum = getattr(self, 'min_%s' % statistic)
        maximum = getattr(self, 'max_%s' % statistic)

        return minimum is not None and maximum is not None

    def matches(self, recipe, statistic):
        if statistic not in (
            'og',
            'fg',
            'abv',
            'srm',
            'ibu'
        ):
            raise InvalidStatistic('Invalid statistic, %s' % statistic)

        minimum = getattr(self, 'min_%s' % statistic)
        maximum = getattr(self, 'max_%s' % statistic)

        if minimum is None or maximum is None:
            return False

        actual = getattr(recipe.calculations, statistic)

        if actual <= maximum and actual >= minimum:
            return True

        return False

    def to_xml(self):
        from draughtcraft.lib.beerxml import export
        kw = {
            'name': self.name,
            'category': self.category,
            'category_number': self.category_number,
            'style_letter': self.style_letter,
            'style_guide': 'BJCP',
            'type': self.type.capitalize(),
            'og_min': self.min_og,
            'og_max': self.max_og,
            'fg_min': self.min_fg,
            'fg_max': self.max_fg,
            'ibu_min': self.min_ibu,
            'ibu_max': self.max_ibu,
            'color_min': self.min_srm,
            'color_max': self.max_srm,
            'abv_min': self.min_abv,
            'abv_max': self.max_abv
        }
        return export.Style(**kw)