Example #1
0
class Repository(db.Model):
    """
    Represents a VCS repository that Changes will watch for new commits.
    """
    __tablename__ = 'repository'

    id = Column(GUID, primary_key=True, default=uuid4)
    url = Column(String(200), nullable=False, unique=True)
    backend = Column(EnumType(RepositoryBackend),
                     default=RepositoryBackend.unknown,
                     nullable=False)
    status = Column(EnumType(RepositoryStatus),
                    default=RepositoryStatus.inactive,
                    nullable=False,
                    server_default='1')
    date_created = Column(DateTime, default=datetime.utcnow)

    last_update = Column(DateTime)
    last_update_attempt = Column(DateTime)

    def __init__(self, **kwargs):
        super(Repository, self).__init__(**kwargs)
        if not self.id:
            self.id = uuid4()
        if not self.date_created:
            self.date_created = datetime.utcnow()

    def get_vcs(self):
        from changes.models import ItemOption
        from changes.vcs.git import GitVcs
        from changes.vcs.hg import MercurialVcs

        options = dict(
            db.session.query(ItemOption.name, ItemOption.value).filter(
                ItemOption.item_id == self.id,
                ItemOption.name.in_([
                    'auth.username',
                ])))

        kwargs = {
            'path': os.path.join(current_app.config['REPO_ROOT'], self.id.hex),
            'url': self.url,
            'username': options.get('auth.username'),
        }

        if self.backend == RepositoryBackend.git:
            return GitVcs(**kwargs)
        elif self.backend == RepositoryBackend.hg:
            return MercurialVcs(**kwargs)
        else:
            return None

    @classmethod
    def get(cls, id):
        result = cls.query.filter_by(url=id).first()
        if result is None and len(id) == 32:
            result = cls.query.get(id)
        return result
Example #2
0
class Command(db.Model):
    __tablename__ = 'command'
    __table_args__ = (UniqueConstraint('jobstep_id',
                                       'order',
                                       name='unq_command_order'), )

    id = Column(GUID, primary_key=True, default=uuid.uuid4)
    jobstep_id = Column(GUID,
                        ForeignKey('jobstep.id', ondelete="CASCADE"),
                        nullable=False)
    label = Column(String(128), nullable=False)
    status = Column(EnumType(Status), nullable=False, default=Status.unknown)
    return_code = Column(Integer, nullable=True)
    script = Column(Text(), nullable=False)
    env = Column(JSONEncodedDict, nullable=True)
    cwd = Column(String(256), nullable=True)
    artifacts = Column(ARRAY(String(256)), nullable=True)
    date_started = Column(DateTime)
    date_finished = Column(DateTime)
    date_created = Column(DateTime, default=datetime.utcnow)
    data = Column(JSONEncodedDict)
    order = Column(Integer, default=0, server_default='0', nullable=False)
    type = Column(EnumType(CommandType),
                  nullable=False,
                  default=CommandType.default,
                  server_default='0')

    jobstep = relationship('JobStep',
                           backref=backref('commands',
                                           order_by='Command.order'))

    __repr__ = model_repr('jobstep_id', 'script')

    def __init__(self, **kwargs):
        super(Command, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid.uuid4()
        if self.status is None:
            self.status = Status.unknown
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if self.data is None:
            self.data = {}

    @property
    def duration(self):
        """
        Return the duration (in milliseconds) that this item was in-progress.
        """
        if self.date_started and self.date_finished:
            duration = (self.date_finished -
                        self.date_started).total_seconds() * 1000
        else:
            duration = None
        return duration
Example #3
0
class Plan(db.Model):
    """
    Represents one of N build plans for a project.
    """
    id = Column(GUID, primary_key=True, default=uuid4)
    project_id = Column(GUID,
                        ForeignKey('project.id', ondelete="CASCADE"),
                        nullable=False)
    label = Column(String(128), nullable=False)
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)
    date_modified = Column(DateTime, default=datetime.utcnow, nullable=False)
    data = Column(JSONEncodedDict)
    status = Column(EnumType(PlanStatus),
                    default=PlanStatus.inactive,
                    nullable=False,
                    server_default='1')
    avg_build_time = Column(Integer)

    project = relationship('Project', backref=backref('plans'))

    __repr__ = model_repr('label')
    __tablename__ = 'plan'

    def __init__(self, **kwargs):
        super(Plan, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid4()
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if self.date_modified is None:
            self.date_modified = self.date_created
Example #4
0
class Plan(db.Model):
    """
    What work should we do for our new revision? A project may have multiple
    plans, e.g. whenever a diff comes in, test it on both mac and windows
    (each being its own plan.) In theory, a plan consists of a sequence of
    steps; in practice, a plan is just a wrapper around a single step.
    """
    id = Column(GUID, primary_key=True, default=uuid4)
    project_id = Column(GUID,
                        ForeignKey('project.id', ondelete="CASCADE"),
                        nullable=False)
    label = Column(String(128), nullable=False)
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)
    date_modified = Column(DateTime, default=datetime.utcnow, nullable=False)
    data = Column(JSONEncodedDict)
    status = Column(EnumType(PlanStatus),
                    default=PlanStatus.inactive,
                    nullable=False,
                    server_default='1')
    # If not None, use snapshot from another plan. This allows us to share
    # a single snapshot between multiple plans.
    #
    # This plan must be a plan from the same project (or else jobstep_details
    # will fail) but this is not enforced by the database schema because we do
    # not use a composite key.
    snapshot_plan_id = Column(GUID,
                              ForeignKey('plan.id', ondelete="SET NULL"),
                              nullable=True)
    avg_build_time = Column(Integer)

    project = relationship('Project', backref=backref('plans'))
    snapshot_plan = relationship('Plan', remote_side=[id])

    __repr__ = model_repr('label')
    __tablename__ = 'plan'

    def __init__(self, **kwargs):
        super(Plan, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid4()
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if self.date_modified is None:
            self.date_modified = self.date_created

    def get_item_options(self):
        from changes.models.option import ItemOption
        options_query = db.session.query(ItemOption.name,
                                         ItemOption.value).filter(
                                             ItemOption.item_id == self.id, )
        options = dict()
        for opt_name, opt_value in options_query:
            options[opt_name] = opt_value
        return options

    def autogenerated(self):
        return self.get_item_options().get('bazel.autogenerate', '0') == '1'
Example #5
0
class TestArtifact(db.Model):
    """
    Represents any artifacts generated by a single run of a single test. used
    e.g. in server-selenium to store screenshots and large log dumps for
    later debugging.

    """
    __tablename__ = 'testartifact'
    __tableargs__ = (
        Index('idx_test_id', 'test_id'),
    )

    id = Column(GUID, nullable=False, primary_key=True, default=uuid.uuid4)
    test_id = Column(GUID, ForeignKey('test.id', ondelete="CASCADE"), nullable=False)
    name = Column('name', String(length=256), nullable=False)
    type = Column(EnumType(TestArtifactType),
                  default=TestArtifactType.unknown,
                  nullable=False, server_default='0')
    file = Column(FileStorage(**TESTARTIFACT_STORAGE_OPTIONS))
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)

    test = relationship('TestCase', backref='artifacts')

    __repr__ = model_repr('name', 'type', 'file')

    def __init__(self, **kwargs):
        super(TestArtifact, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid.uuid4()
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if isinstance(self.type, str):
            self.type = TestArtifactType[self.type]
        if self.file is None:
            # TODO(dcramer): this is super hacky but not sure a better way to
            # do it with SQLAlchemy
            self.file = FileData({}, TESTARTIFACT_STORAGE_OPTIONS)

    def save_base64_content(self, base64):
        content = b64decode(base64)
        self.file.save(
            StringIO(content),
            '{0}/{1}_{2}'.format(
                self.test_id, self.id.hex, self.name
            ),
            self._get_content_type()
        )

    def _get_content_type(self):
        content_type, encoding = mimetypes.guess_type(self.name)
        if content_type == 'text/html':
            # upload html artifacts as plain text so the browser doesn't try to
            # render them when viewing them raw
            content_type = 'text/plain'
        return content_type
Example #6
0
class Snapshot(db.Model):
    """
    Represents a snapshot used as a base in builds.

    This is primarily used to indicate status and contains a collection of
    SnapshotImage's.
    """

    __tablename__ = 'snapshot'

    id = Column(GUID, primary_key=True, default=uuid4)
    project_id = Column(GUID,
                        ForeignKey('project.id', ondelete="CASCADE"),
                        nullable=False)
    build_id = Column(GUID, ForeignKey('build.id'), unique=True)
    source_id = Column(GUID, ForeignKey('source.id'))
    status = Column(EnumType(SnapshotStatus),
                    default=SnapshotStatus.unknown,
                    nullable=False,
                    server_default='0')
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)

    build = relationship('Build')
    project = relationship('Project', innerjoin=True)
    source = relationship('Source')

    def __init__(self, **kwargs):
        super(Snapshot, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid4()

    @classmethod
    def get_current(self, project_id):
        from changes.models import ProjectOption

        current_id = db.session.query(ProjectOption.value).filter(
            ProjectOption.project_id == project_id,
            ProjectOption.name == 'snapshot.current',
        ).scalar()
        if not current_id:
            return

        return Snapshot.query.get(current_id)
Example #7
0
class SnapshotImage(db.Model):
    """
    Represents an individual image within a snapshot.

    An image is bound to a (snapshot, plan) and represents the low level base
    image that a snapshottable-job should be based on.
    """

    __tablename__ = 'snapshot_image'
    __table_args__ = (UniqueConstraint('snapshot_id',
                                       'plan_id',
                                       name='unq_snapshotimage_plan'), )

    id = Column(GUID, primary_key=True, default=uuid4)
    snapshot_id = Column(GUID,
                         ForeignKey('snapshot.id', ondelete="CASCADE"),
                         nullable=False)
    plan_id = Column(GUID,
                     ForeignKey('plan.id', ondelete="CASCADE"),
                     nullable=False)
    job_id = Column(GUID,
                    ForeignKey('job.id', ondelete="CASCADE"),
                    unique=True)
    status = Column(EnumType(SnapshotStatus),
                    default=SnapshotStatus.unknown,
                    nullable=False,
                    server_default='0')
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)

    snapshot = relationship('Snapshot',
                            backref=backref(
                                'images',
                                order_by='SnapshotImage.date_created'))
    plan = relationship('Plan')
    job = relationship('Job')

    def __init__(self, **kwargs):
        super(SnapshotImage, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid4()
Example #8
0
class Snapshot(db.Model):
    """
    A snapshot is a set of LXC container images (up to one for each plan in a project).

    Each project can have an arbitrary number of snapshots, but only up
    to one "current snapshot" is actually used by builds (stored as
    ProjectOption) at any time.

    Snapshots are used in the Mesos and Jenkins-LXC environments.
    Snapshots are currently only used with changes-client.

    When running a build, the images of the current snapshot are used
    for individual jobs that are part of a build.  A snapshot image
    can be shared between multiple plans by setting snapshot_plan_id
    of a Plan.  By default, there is a separate image for each plan
    of a build.

    The status of a snapshot indicates whether it *can* be used for
    builds; it doesn't indicate whether the snapshot is actually used
    for builds right now. A snapshot is active if and only if all the
    corresponding snapshot images are active.

    A snapshot is generated by a slightly special snapshot build that
    uploads a snapshot at the end of the build.
    """

    __tablename__ = 'snapshot'

    id = Column(GUID, primary_key=True, default=uuid4)
    project_id = Column(
        GUID, ForeignKey('project.id', ondelete="CASCADE"), nullable=False)
    build_id = Column(GUID, ForeignKey('build.id'), unique=True)
    source_id = Column(GUID, ForeignKey('source.id'))
    # Most importantly, this tells if this snapshot can be used for builds (i.e., are all
    # component snapshot images active)?
    status = Column(EnumType(SnapshotStatus),
                    default=SnapshotStatus.unknown,
                    nullable=False, server_default='0')
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)

    # The build that generated this snapshot.
    build = relationship('Build', backref=backref('snapshot', uselist=False))
    project = relationship('Project', innerjoin=True)

    # The source that was used to generate the snapshot.
    source = relationship('Source')

    def __init__(self, **kwargs):
        super(Snapshot, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid4()

    @classmethod
    def get_current(cls, project_id):
        """Return the current Snapshot for a project (or None if one is not set)."""
        from changes.models import ProjectOption

        current_id = db.session.query(ProjectOption.value).filter(
            ProjectOption.project_id == project_id,
            ProjectOption.name == 'snapshot.current',
        ).scalar()
        if not current_id:
            return

        return Snapshot.query.get(current_id)
Example #9
0
class SnapshotImage(db.Model):
    """
    Represents an individual image within a snapshot.

    An image is bound to a (snapshot, plan) and represents the low level base
    image that a snapshottable-job should be based on.

    Note that a project with multiple plans may have multiple, independent
    images per snapshot, as images aren't always shared between plans.
    """

    __tablename__ = 'snapshot_image'
    __table_args__ = (
        UniqueConstraint('snapshot_id', 'plan_id', name='unq_snapshotimage_plan'),
    )

    # The snapshot image id is used by changes-client to store and retrieve snapshots.
    # New ids are created by changes and passed on to changes-client.
    id = Column(GUID, primary_key=True, default=uuid4)
    snapshot_id = Column(
        GUID, ForeignKey('snapshot.id', ondelete="CASCADE"), nullable=False)
    plan_id = Column(
        GUID, ForeignKey('plan.id', ondelete="CASCADE"), nullable=False)
    job_id = Column(GUID, ForeignKey('job.id', ondelete="CASCADE"), unique=True)
    status = Column(EnumType(SnapshotStatus),
                    default=SnapshotStatus.unknown,
                    nullable=False, server_default='0')
    date_created = Column(DateTime, default=datetime.utcnow, nullable=False)

    snapshot = relationship('Snapshot', backref=backref('images',
                                                        order_by='SnapshotImage.date_created'))
    plan = relationship('Plan')
    # The job that was used to create this snapshot.
    job = relationship('Job')

    def __init__(self, **kwargs):
        super(SnapshotImage, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid4()

    def change_status(self, status):
        """
        The status field of snapshot is a redundant field that has to
        be maintained. Its essentially a cached aggregate over
        snapshot_image.status. This means that whenever we update
        snapshot_image.status we have to update snapshot.status.

        This method updates the current status to the new status given
        as a parameter.

        TODO we should probably verify that if the current status is
        active and we are tring to move it to a new status that is
        not "invalidated" we should give some error.

        XXX(jhance)
        Is this a sign of a defective schema? Computing snapshot.status
        should be possible in-query although I'm not sure to do it from
        within sqlalchemy.
        """
        self.status = status

        db.session.add(self)

        # We need to update the current database with the status of the
        # new image, but we don't commit completely until we have found
        # the status of the overall status and update it atomically
        db.session.flush()

        inactive_image_query = SnapshotImage.query.filter(
            SnapshotImage.status != SnapshotStatus.active,
            SnapshotImage.snapshot_id == self.snapshot.id,
        ).exists()
        if not db.session.query(inactive_image_query).scalar():
            # If the snapshot status isn't pending for whatever reason, then we
            # refuse to update its status to active because clearly some other
            # error occurred elsewhere in the pipeline (for example, the
            # snapshot build itself failing)
            if self.snapshot.status == SnapshotStatus.pending:
                self.snapshot.status = SnapshotStatus.active
        elif self.snapshot.status == SnapshotStatus.active:
            self.snapshot.status = SnapshotStatus.invalidated

        db.session.commit()

    @classmethod
    def get(cls, plan, snapshot_id):
        """Return the SnapshotImage for a plan or None if one is not set.
        """

        # This plan might be configured to be dependent on another plan's snapshot
        snapshot_plan = plan
        if plan.snapshot_plan is not None:
            snapshot_plan = plan.snapshot_plan

        snapshot = Snapshot.query.filter(Snapshot.id == snapshot_id).scalar()
        if snapshot is not None:
            return SnapshotImage.query.filter(
                        SnapshotImage.snapshot_id == snapshot.id,
                        SnapshotImage.plan_id == snapshot_plan.id,
                   ).scalar()
        return None
Example #10
0
class Build(db.Model):
    """
    Represents the work we do (e.g. running tests) for one diff or commit (an
    entry in the source table) in one particular project

    Each Build contains many Jobs (usually linked to a JobPlan).
    """
    __tablename__ = 'build'
    __table_args__ = (
        Index('idx_buildfamily_project_id', 'project_id'),
        Index('idx_buildfamily_author_id', 'author_id'),
        Index('idx_buildfamily_source_id', 'source_id'),
        UniqueConstraint('project_id', 'number', name='unq_build_number'),
    )

    id = Column(GUID, primary_key=True, default=uuid.uuid4)
    number = Column(Integer)
    project_id = Column(GUID,
                        ForeignKey('project.id', ondelete="CASCADE"),
                        nullable=False)
    # A unqiue identifier for a group of related Builds, such as all Builds created by a particular
    # action. Used primarily for aggregation in result reporting.
    # Note that this may be None for Builds that aren't grouped, and all such Builds should NOT
    # be treated as a collection.
    collection_id = Column(GUID)
    source_id = Column(GUID, ForeignKey('source.id', ondelete="CASCADE"))
    author_id = Column(GUID, ForeignKey('author.id', ondelete="CASCADE"))
    cause = Column(EnumType(Cause), nullable=False, default=Cause.unknown)
    # label is a short description, typically from the title of the change that triggered the build.
    label = Column(String(128), nullable=False)
    # short indicator of what is being built, typically the sha or the Phabricator revision ID like 'D90885'.
    target = Column(String(128))
    tags = Column(ARRAY(String(16)), nullable=True)
    status = Column(EnumType(Status), nullable=False, default=Status.unknown)
    result = Column(EnumType(Result), nullable=False, default=Result.unknown)
    message = Column(Text)
    duration = Column(Integer)
    priority = Column(EnumType(BuildPriority),
                      nullable=False,
                      default=BuildPriority.default,
                      server_default='0')
    date_started = Column(DateTime)
    date_finished = Column(DateTime)
    date_created = Column(DateTime, default=datetime.utcnow)
    date_modified = Column(DateTime, default=datetime.utcnow)
    data = Column(JSONEncodedDict)

    project = relationship('Project', innerjoin=True)
    source = relationship('Source', innerjoin=True)
    author = relationship('Author')
    stats = relationship('ItemStat',
                         primaryjoin='Build.id == ItemStat.item_id',
                         foreign_keys=[id],
                         uselist=True)

    __repr__ = model_repr('label', 'target')

    def __init__(self, **kwargs):
        super(Build, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid.uuid4()
        if self.result is None:
            self.result = Result.unknown
        if self.status is None:
            self.status = Status.unknown
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if self.date_modified is None:
            self.date_modified = self.date_created
        if self.date_started and self.date_finished and not self.duration:
            self.duration = (self.date_finished -
                             self.date_started).total_seconds() * 1000
        if self.number is None and self.project:
            self.number = select([func.next_item_value(self.project.id.hex)])
Example #11
0
class Command(db.Model):
    """
    The information of the script run on one node within a jobstep: the contents
    of the script are included, and later the command can be updated
    with status/return code.

    changes-client has no real magic beyond running commands, so the list
    of commands it ran basically tells you everything that happened.

    Looks like only mesos/lxc builds (DefaultBuildStep)
    """
    __tablename__ = 'command'
    __table_args__ = (UniqueConstraint('jobstep_id',
                                       'order',
                                       name='unq_command_order'), )

    id = Column(GUID, primary_key=True, default=uuid.uuid4)
    jobstep_id = Column(GUID,
                        ForeignKey('jobstep.id', ondelete="CASCADE"),
                        nullable=False)
    label = Column(String(128), nullable=False)
    status = Column(EnumType(Status), nullable=False, default=Status.unknown)
    return_code = Column(Integer, nullable=True)
    script = Column(Text(), nullable=False)
    env = Column(JSONEncodedDict, nullable=True)
    cwd = Column(String(256), nullable=True)
    artifacts = Column(ARRAY(String(256)), nullable=True)
    date_started = Column(DateTime)
    date_finished = Column(DateTime)
    date_created = Column(DateTime, default=datetime.utcnow)
    data = Column(JSONEncodedDict)
    order = Column(Integer, default=0, server_default='0', nullable=False)
    type = Column(EnumType(CommandType),
                  nullable=False,
                  default=CommandType.default,
                  server_default='0')

    jobstep = relationship('JobStep',
                           backref=backref('commands',
                                           order_by='Command.order'))

    __repr__ = model_repr('jobstep_id', 'script')

    def __init__(self, **kwargs):
        super(Command, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid.uuid4()
        if self.status is None:
            self.status = Status.unknown
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if self.data is None:
            self.data = {}

    @property
    def duration(self):
        """
        Return the duration (in milliseconds) that this item was in-progress.
        """
        if self.date_started and self.date_finished:
            duration = (self.date_finished -
                        self.date_started).total_seconds() * 1000
        else:
            duration = None
        return duration
Example #12
0
class Build(db.Model):
    """
    Represents a collection of builds for a single target, as well as the sum
    of their results.

    Each Build contains many Jobs (usually linked to a JobPlan).
    """
    __tablename__ = 'build'
    __table_args__ = (
        Index('idx_buildfamily_project_id', 'project_id'),
        Index('idx_buildfamily_author_id', 'author_id'),
        Index('idx_buildfamily_source_id', 'source_id'),
        UniqueConstraint('project_id', 'number', name='unq_build_number'),
    )

    id = Column(GUID, primary_key=True, default=uuid.uuid4)
    number = Column(Integer)
    project_id = Column(GUID,
                        ForeignKey('project.id', ondelete="CASCADE"),
                        nullable=False)
    collection_id = Column(GUID)
    source_id = Column(GUID, ForeignKey('source.id', ondelete="CASCADE"))
    author_id = Column(GUID, ForeignKey('author.id', ondelete="CASCADE"))
    cause = Column(EnumType(Cause), nullable=False, default=Cause.unknown)
    label = Column(String(128), nullable=False)
    target = Column(String(128))
    tags = Column(ARRAY(String(16)), nullable=True)
    status = Column(EnumType(Status), nullable=False, default=Status.unknown)
    result = Column(EnumType(Result), nullable=False, default=Result.unknown)
    message = Column(Text)
    duration = Column(Integer)
    priority = Column(EnumType(BuildPriority),
                      nullable=False,
                      default=BuildPriority.default,
                      server_default='0')
    date_started = Column(DateTime)
    date_finished = Column(DateTime)
    date_created = Column(DateTime, default=datetime.utcnow)
    date_modified = Column(DateTime, default=datetime.utcnow)
    data = Column(JSONEncodedDict)

    project = relationship('Project', innerjoin=True)
    source = relationship('Source', innerjoin=True)
    author = relationship('Author')
    stats = relationship('ItemStat',
                         primaryjoin='Build.id == ItemStat.item_id',
                         foreign_keys=[id],
                         uselist=True)

    __repr__ = model_repr('label', 'target')

    def __init__(self, **kwargs):
        super(Build, self).__init__(**kwargs)
        if self.id is None:
            self.id = uuid.uuid4()
        if self.result is None:
            self.result = Result.unknown
        if self.status is None:
            self.status = Status.unknown
        if self.date_created is None:
            self.date_created = datetime.utcnow()
        if self.date_modified is None:
            self.date_modified = self.date_created
        if self.date_started and self.date_finished and not self.duration:
            self.duration = (self.date_finished -
                             self.date_started).total_seconds() * 1000
        if self.number is None and self.project:
            self.number = select([func.next_item_value(self.project.id.hex)])