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)
Esempio n. 2
0
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'
Esempio n. 3
0
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')
Esempio n. 4
0
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'))
Esempio n. 5
0
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 + '%'))
Esempio n. 6
0
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]
Esempio n. 7
0
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()
Esempio n. 8
0
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()
Esempio n. 9
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
        }