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)
class process(Entity): display = Field(String) pid = Field(String) name = Field(String) tabtitle = Field(String) hostip = Field(String) port = Field(String) protocol = Field(String) command = Field(String) starttime = Field(String) outputfile = Field(String) output = OneToOne('process_output', inverse='process') status = Field(String) closed = Field(String) def __init__(self, pid, name, tabtitle, hostip, port, protocol, command, starttime, outputfile, status, processOutputId): self.display = 'True' self.pid = pid self.name = name self.tabtitle = tabtitle self.hostip = hostip self.port = port self.protocol = protocol self.command = command self.starttime = starttime self.outputfile = outputfile self.output = processOutputId self.status = status self.closed = 'False'
class Card(Entity): scanned_image = OneToOne('ScannedImage') box = ManyToOne('Box') box_index = Field(Integer) card_id = Field(UnicodeText) notes = Field(UnicodeText) using_options(tablename='cards') using_table_options(schema='inventory')
class Strain(Entity): """ 2009-9-22 add 'replicate' into the unique constraint. change type of replicate from boolean to integer """ ecotype = ManyToOne("Ecotype", colname='ecotypeid', ondelete='CASCADE', onupdate='CASCADE') extraction = ManyToOne("Extraction", colname='extractionid', ondelete='CASCADE', onupdate='CASCADE') seqinfo1 = ManyToOne("SeqInfo", colname='seqinfoid1', ondelete='CASCADE', onupdate='CASCADE') seqinfo2 = ManyToOne("SeqInfo", colname='seqinfoid2', ondelete='CASCADE', onupdate='CASCADE') seqinfo3 = ManyToOne("SeqInfo", colname='seqinfoid3', ondelete='CASCADE', onupdate='CASCADE') seqinfo4 = ManyToOne("SeqInfo", colname='seqinfoid4', ondelete='CASCADE', onupdate='CASCADE') plateid = Field(String(25)) wellid = Field(String(3)) replicate = Field(Integer) contaminant_type = ManyToOne("%s.ContaminantType" % __name__, colname='contaminant_type_id', ondelete='CASCADE', onupdate='CASCADE') call_qc_ls = OneToMany("%s.CallQC" % __name__) ecotypeid_strainid2tg_ecotypeid = OneToOne("EcotypeIDStrainID2TGEcotypeID", inverse="strain") created_by = Field(String(128)) updated_by = Field(String(128)) date_created = Field(DateTime, default=datetime.now) date_updated = Field(DateTime) using_options(tablename='strain', metadata=__metadata__, session=__session__) using_table_options(mysql_engine='InnoDB') using_table_options( UniqueConstraint('ecotypeid', 'plateid', 'wellid', 'replicate'))
class DataUser(Entity): """Label mapper """ using_options(tablename='user') # VARCHAR(binary=True) here is a hack to make MySQL case sensitive # like the other DBMS. # No consequences on regular databases. username = Field( VARCHAR(255, binary=True), unique=True, primary_key=True, nullable=False) source = Field(Unicode(255), nullable=False, primary_key=True) fullname = Field(Unicode(255), nullable=False) email = Field(Unicode(255), nullable=True, unique=True, index=True) picture = Field(Unicode(255), nullable=True) language = Field(Unicode(255), default=u"en", nullable=True) email_to_confirm = Field(Unicode(255)) _salt = Field(Unicode(255), colname='salt', nullable=False) _password = Field(Unicode(255), colname='password', nullable=True) registration_date = Field(DateTime, nullable=False) last_login = Field(DateTime, nullable=True) last_board = OneToOne('DataBoard', inverse='last_users') # history = OneToMany('DataHistory') def __init__(self, username, password, fullname, email, source=u'application', picture=None, **kw): """Create a new user with an unconfirmed email""" super(DataUser, self).__init__(username=username, fullname=fullname, email=None, email_to_confirm=email, source=source, picture=picture, registration_date=datetime.datetime.utcnow(), **kw) # Create password if source is local if source == "application": self.change_password(password) else: # External authentication self.change_password('passwd') self.email_to_confirm = None @property def id(self): return self.username def update(self, fullname, email, picture=None): self.fullname = fullname if email: self.email = email self.picture = picture def check_password(self, clear_password): """Check the user password. Return True if the password is valid for this user""" encrypted_password = self._encrypt_password(self._salt, clear_password) return encrypted_password == self._password def change_password(self, clear_password): """Change the user password""" self._salt = self._create_random_salt() self._password = self._encrypt_password(self._salt, clear_password) def set_email_to_confirm(self, email_to_confirm): if email_to_confirm: self.email_to_confirm = email_to_confirm def is_validated(self): return self.email_to_confirm is None def confirm_email(self): """Called when a user confirms his email address""" # already confirmed if self.email_to_confirm is None: return self.email = self.email_to_confirm self.email_to_confirm = None def get_picture(self): return self.picture @classmethod def get_confirmed_users(cls): return cls.query.filter(cls.email is not None) @staticmethod def _create_random_salt(length=32): allowed_chars = string.ascii_letters + string.digits return u''.join(random.choice(allowed_chars) for _ in range(length)) @staticmethod def _encrypt_password(salt, password): secret = "NzlSszmvDNY2e2lVMwiKJwgWjNGFCP1a" secret_salt = hashlib.sha512(secret + salt).hexdigest() utf8_password = password.encode('utf-8') return unicode(hashlib.sha512(secret_salt + utf8_password).hexdigest()) @classmethod def get_unconfirmed_users(cls, before_date=None): q = cls.query.filter(cls.email is None) if before_date: q = q.filter(cls.registration_date < before_date) return q @classmethod def get_by_username(cls, username): return cls.get_by(username=username) @classmethod def get_by_email(cls, email): return cls.get_by(email=email) @classmethod def search(cls, value): return cls.query.filter(cls.fullname.ilike('%' + value + '%') | cls.email.ilike('%' + value + '%') | cls.email_to_confirm.ilike('%' + value + '%'))
class DataUser(Entity): """Label mapper """ using_options(tablename='user') # VARCHAR(binary=True) here is a hack to make MySQL case sensitive # like the other DBMS. # No consequences on regular databases. username = Field(VARCHAR(255, binary=True), unique=True, primary_key=True, nullable=False) source = Field(Unicode(255), nullable=False, primary_key=True) fullname = Field(Unicode(255), nullable=False) email = Field(Unicode(255), nullable=True) picture = Field(Unicode(255), nullable=True) language = Field(Unicode(255), default=u"en", nullable=True) email_to_confirm = Field(Unicode(255)) _salt = Field(Unicode(255), colname='salt', nullable=False) _password = Field(Unicode(255), colname='password', nullable=True) registration_date = Field(DateTime, nullable=False) last_login = Field(DateTime, nullable=True) display_week_numbers = Field(Boolean, default=False) board_members = OneToMany('DataBoardMember') boards = AssociationProxy( 'board_members', 'board', creator=lambda board: DataBoardMember(board=board)) board_managers = OneToMany('DataBoardManager') managed_boards = AssociationProxy( 'board_managers', 'board', creator=lambda board: DataBoardManager(board=board)) last_board = OneToOne('DataBoard', inverse='last_users') cards = ManyToMany('DataCard', inverse='members', lazy='dynamic') history = OneToMany('DataHistory') def __init__(self, username, password, fullname, email, source=u'application', picture=None, **kw): """Create a new user with an unconfirmed email""" super(DataUser, self).__init__(username=username, fullname=fullname, email=None, email_to_confirm=email, source=source, picture=picture, registration_date=datetime.datetime.utcnow(), **kw) # Create password if source is local if source == "application": self.change_password(password) else: # External authentication self.change_password('passwd') self.email_to_confirm = None @property def id(self): return self.username def update(self, fullname, email, picture=None): self.fullname = fullname if email: self.email = email self.picture = picture def check_password(self, clear_password): """Check the user password. Return True if the password is valid for this user""" encrypted_password = self._encrypt_password(self._salt, clear_password) return encrypted_password == self._password def change_password(self, clear_password): """Change the user password""" self._salt = self._create_random_salt() self._password = self._encrypt_password(self._salt, clear_password) def set_email_to_confirm(self, email_to_confirm): if email_to_confirm: self.email_to_confirm = email_to_confirm def is_validated(self): return self.email_to_confirm is None def confirm_email(self): """Called when a user confirms his email address""" # already confirmed if self.email_to_confirm is None: return self.email = self.email_to_confirm self.email_to_confirm = None def add_board(self, board, role="member"): """Add board to user's board lists In: - ``board`` -- DataBoard instance to add - ``role`` -- user is member or manager """ boards = set(dbm.board for dbm in self.board_members) if board not in boards: self.board_members.append(DataBoardMember(board=board)) if role == "manager" and board not in self.managed_boards: self.managed_boards.append(board) def get_picture(self): return self.picture @classmethod def get_confirmed_users(cls): return cls.query.filter(cls.email is not None) @staticmethod def _create_random_salt(length=32): allowed_chars = string.ascii_letters + string.digits return u''.join(random.choice(allowed_chars) for _ in range(length)) @staticmethod def _encrypt_password(salt, password): secret = "NzlSszmvDNY2e2lVMwiKJwgWjNGFCP1a" secret_salt = hashlib.sha512(secret + salt).hexdigest() utf8_password = password.encode('utf-8') return unicode(hashlib.sha512(secret_salt + utf8_password).hexdigest()) @classmethod def get_unconfirmed_users(cls, before_date=None): q = cls.query.filter(cls.email is None) if before_date: q = q.filter(cls.registration_date < before_date) return q @classmethod def get_by_username(cls, username): return cls.get_by(username=username) @classmethod def get_by_email(cls, email): return cls.get_by(email=email) @classmethod def search(cls, value): return cls.query.filter( cls.fullname.ilike('%' + value + '%') | cls.email.ilike('%' + value + '%')) def best_friends(self, exclude_list=(), size=None): from kansha.board.models import DataBoard cls = self.__class__ bm2 = aliased(DataBoardMember) cnt = func.count(DataBoardMember.board_id) query = database.session.query(cls, cnt) query = query.join( (DataBoardMember, and_(DataBoardMember.user_source == cls.source, DataBoardMember.user_username == cls.username))) query = query.join( (DataBoard, DataBoard.id == DataBoardMember.board_id)) query = query.join((bm2, bm2.board_id == DataBoard.id)) query = query.filter(bm2.member == self) if exclude_list: query = query.filter(~cls.email.in_(exclude_list)) query = query.group_by(cls) query = query.order_by(cnt.desc(), cls.fullname) if size: query = query.limit(size) return [res[0] for res in query]
class DataCard(Entity): """Card mapper """ using_options(tablename='card') title = Field(UnicodeText) description = Field(UnicodeText, default=u'') votes = OneToMany('DataVote') index = Field(Integer) column = ManyToOne('DataColumn') labels = ManyToMany('DataLabel') comments = OneToMany('DataComment', order_by="-creation_date") assets = OneToMany('DataAsset', order_by="-creation_date") checklists = OneToMany('DataChecklist', order_by="index") members = ManyToMany('DataUser') cover = OneToOne('DataAsset', inverse="cover") author = ManyToOne('DataUser', inverse="my_cards") creation_date = Field(DateTime) due_date = Field(Date) history = OneToMany('DataHistory') weight = Field(Unicode(255), default=u'') def delete_history(self): for event in self.history: session.delete(event) session.flush() @classmethod def create_card(cls, column, title, user): """Create new column In: - ``column`` -- DataColumn, father of the column - ``title`` -- title of the card - ``user`` -- DataUser, author of the card Return: - created DataCard instance """ new_card = cls(title=title, author=user, creation_date=datetime.datetime.utcnow()) column.cards.append(new_card) return new_card @classmethod def delete_card(cls, card): """Delete card Delete a given card and re-index other cards In: - ``card`` -- DataCard instance to delete """ index = card.index column = card.column card.delete() session.flush() # legacy databases may be broken… if index is None: return q = cls.query q = q.filter(cls.index >= index) q = q.filter(cls.column == column) q.update({'index': cls.index - 1}) @classmethod def get_all(cls): query = cls.query.options(subqueryload('labels'), subqueryload('comments')) return query def make_cover(self, asset): """ """ DataCard.get(self.id).cover = asset.data def remove_cover(self): """ """ DataCard.get(self.id).cover = None def remove_board_member(self, member): """Remove member from board""" if member.get_user_data() in self.members: self.members.remove(member.get_user_data()) session.flush()
class DataBoard(Entity): """Board mapper - ``title`` -- board title - ``is_template`` -- is this a real board or a template? - ``columns`` -- list of board columns - ``labels`` -- list of labels for cards - ``comments_allowed`` -- who can comment ? (0 nobody, 1 board members only , 2 all application users) - ``votes_allowed`` -- who can vote ? (0 nobody, 1 board members only , 2 all application users) - ``description`` -- board description - ``visibility`` -- board visibility [0 Private, 1 Public (anyone with the URL can view), 2 Shared (anyone can view it from her home page)] - ``uri`` -- board URI (Universally Unique IDentifier) - ``last_users`` -- list of last users - ``pending`` -- invitations pending for new members (use token) - ``archive`` -- display archive column ? (0 false, 1 true) - ``archived`` -- is board archived ? """ using_options(tablename='board') title = Field(Unicode(255)) is_template = Field(Boolean, default=False) columns = OneToMany('DataColumn', order_by="index", cascade='delete', lazy='subquery') # provisional labels = OneToMany('DataLabel', order_by='index') comments_allowed = Field(Integer, default=1) votes_allowed = Field(Integer, default=1) description = Field(UnicodeText, default=u'') visibility = Field(Integer, default=0) version = Field(Integer, default=0, server_default='0') # provisional board_members = OneToMany('DataMembership', lazy='subquery', order_by=('manager')) uri = Field(Unicode(255), index=True, unique=True) last_users = ManyToOne('DataUser', order_by=('fullname', 'email')) pending = OneToMany('DataToken', order_by='username') history = OneToMany('DataHistory') background_image = Field(Unicode(255)) background_position = Field(Unicode(255)) title_color = Field(Unicode(255)) show_archive = Field(Integer, default=0) archived = Field(Boolean, default=False) # provisional weight_config = OneToOne('DataBoardWeightConfig') def __init__(self, *args, **kwargs): """Initialization. Create board and uri of the board """ super(DataBoard, self).__init__(*args, **kwargs) self.uri = unicode(uuid.uuid4()) @property def template_title(self): manager = self.get_first_manager() if not manager or self.visibility == 0: return self.title return u'{0} ({1})'.format(self.title, manager.fullname) def get_first_manager(self): if not self.board_members: return None potential_manager = self.board_members[-1] return potential_manager.user if potential_manager.manager else None def copy(self): new_data = DataBoard(title=self.title, description=self.description, background_position=self.background_position, title_color=self.title_color, comments_allowed=self.comments_allowed, votes_allowed=self.votes_allowed) # TODO: move to board extension new_data.weight_config = DataBoardWeightConfig( weighting_cards=self.weighting_cards, weights=self.weights) session.add(new_data) session.flush() # TODO: move to board extension for label in self.labels: new_data.labels.append(label.copy()) session.flush() return new_data def get_label_by_title(self, title): return (l for l in self.labels if l.title == title).next() def delete_history(self): for event in self.history: session.delete(event) session.flush() def increase_version(self): self.version += 1 if self.version > 2147483600: self.version = 1 @property def url(self): return "%s/%s" % (urllib.quote_plus( self.title.encode('ascii', 'ignore').replace('/', '_')), self.uri) @classmethod def get_by_id(cls, id): return cls.get(id) @classmethod def get_by_uri(cls, uri): return cls.get_by(uri=uri) def set_background_image(self, image): self.background_image = image or u'' @classmethod def get_all_boards(cls, user): """Return all boards the user is member of.""" query = session.query(cls).join(DataMembership) query = query.filter(cls.is_template == False, DataMembership.user == user) return query.order_by(cls.title) @classmethod def get_shared_boards(cls): query = session.query(cls).filter(cls.visibility == BOARD_SHARED) return query.order_by(cls.title) @classmethod def get_templates_for(cls, user, public_value): q = cls.query q = q.filter(cls.archived == False) q = q.filter(cls.is_template == True) q = q.order_by(cls.title) q1 = q.filter(cls.visibility == public_value) q2 = q.join(DataMembership) q2 = q2.filter(DataMembership.user == user) q2 = q2.filter(cls.visibility != public_value) return q1, q2 def create_column(self, index, title, nb_cards=None, archive=False): return DataColumn.create_column(self, index, title, nb_cards, archive) def delete_column(self, column): if column in self.columns: self.columns.remove(column) def create_label(self, title, color): label = DataLabel(title=title, color=color) self.labels.append(label) session.flush() return label ############# Membership management; those functions belong to a board extension def delete_members(self): DataMembership.delete_members(self) def has_member(self, user): """Return True if user is member of the board In: - ``user`` -- user to test (DataUser instance) Return: - True if user is member of the board """ return DataMembership.has_member(self, user) def has_manager(self, user): """Return True if user is manager of the board In: - ``user`` -- user to test (DataUser instance) Return: - True if user is manager of the board """ return DataMembership.has_member(self, user, manager=True) def remove_member(self, user): DataMembership.remove_member(board=self, user=user) def change_role(self, user, new_role): DataMembership.change_role(self, user, new_role == 'manager') def add_member(self, user, role='member'): """ Add new member to the board In: - ``new_member`` -- user to add (DataUser instance) - ``role`` -- role's member (manager or member) """ DataMembership.add_member(self, user, role == 'manager') ############# Weight configuration, those functions belong to an extension @property def weights(self): return self.weight_config.weights @weights.setter def weights(self, value): self.weight_config.weights = value @property def weighting_cards(self): return self.weight_config.weighting_cards @weighting_cards.setter def weighting_cards(self, value): self.weight_config.weighting_cards = value def reset_card_weights(self): self.weight_config.reset_card_weights() def total_weight(self): return self.weight_config.total_weight()
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 }