Initial fork from gertty -> boartty
Change-Id: I8c0ce5550f2287f77fb31c790c3923d3d1b80481
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
*.pyc
|
||||
*.egg*
|
||||
gertty-env
|
||||
boartty-env
|
||||
.tox
|
||||
doc/build
|
||||
|
@ -1,4 +1,4 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/gertty.git
|
||||
project=openstack/boartty.git
|
||||
|
@ -1,10 +1,10 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
To browse the latest code, see: https://git.openstack.org/cgit/stackforge/gertty/tree/
|
||||
To clone the latest code, use `git clone git://git.openstack.org/stackforge/gertty`
|
||||
To browse the latest code, see: https://git.openstack.org/cgit/openstack/boartty/tree/
|
||||
To clone the latest code, use `git clone git://git.openstack.org/openstack/boartty`
|
||||
|
||||
Bugs are handled at: https://storyboard.openstack.org/#!/project/698
|
||||
Bugs are handled at: https://storyboard.openstack.org/
|
||||
|
||||
Code reviews are handled by gerrit at: https://review.openstack.org
|
||||
|
||||
@ -18,26 +18,19 @@ that links to your launchpad account). Example::
|
||||
Philosophy
|
||||
----------
|
||||
|
||||
Gertty is based on the following precepts which should inform changes
|
||||
Boartty is based on the following precepts which should inform changes
|
||||
to the program:
|
||||
|
||||
* Support large numbers of review requests across large numbers of
|
||||
projects. Help the user prioritize those reviews.
|
||||
* Support large numbers of stories across large numbers of projects.
|
||||
|
||||
* Adopt a news/mailreader-like workflow in support of the above.
|
||||
Being able to subscribe to projects, mark reviews as "read" without
|
||||
reviewing, etc, are all useful concepts to support a heavy review
|
||||
load (they have worked extremely well in supporting people who
|
||||
read/write a lot of mail/news).
|
||||
|
||||
* Support off-line use. Gertty should be completely usable off-line
|
||||
with reliable syncing between local data and Gerrit when a
|
||||
* Support off-line use. Boartty should be completely usable off-line
|
||||
with reliable syncing between local data and Storyboard when a
|
||||
connection is available (just like git or mail or news).
|
||||
|
||||
* Ample use of color. Unlike a web interface, a good text interface
|
||||
relies mostly on color and precise placement rather than whitespace
|
||||
and decoration to indicate to the user the purpose of a given piece
|
||||
of information. Gertty should degrade well to 16 colors, but more
|
||||
of information. Boartty should degrade well to 16 colors, but more
|
||||
(88 or 256) may be used.
|
||||
|
||||
* Keyboard navigation (with easy-to-remember commands) should be
|
||||
@ -51,10 +44,9 @@ to the program:
|
||||
messages or comments) and navigating back intuitive (it matches
|
||||
expectations set by the web browsers).
|
||||
|
||||
* Support a wide variety of Gerrit installations. The initial
|
||||
development of Gertty is against the OpenStack project's Gerrit, and
|
||||
many of the features are intended to help its developers with their
|
||||
workflow, however, those features should be implemented in a generic
|
||||
way so that the system does not require a specific Gerrit
|
||||
configuration.
|
||||
|
||||
* Support a wide variety of Storyboard installations. The initial
|
||||
development of Boartty is against the OpenStack project's
|
||||
Storyboard, and many of the features are intended to help its
|
||||
developers with their workflow, however, those features should be
|
||||
implemented in a generic way so that the system does not require a
|
||||
specific Storyboard configuration.
|
||||
|
177
README.rst
177
README.rst
@ -1,137 +1,84 @@
|
||||
Gertty
|
||||
======
|
||||
Boartty
|
||||
=======
|
||||
|
||||
Gertty is a console-based interface to the Gerrit Code Review system.
|
||||
Boartty is a console-based interface to the Storyboard task-tracking
|
||||
system.
|
||||
|
||||
As compared to the web interface, the main advantages are:
|
||||
|
||||
* Workflow -- the interface is designed to support a workflow similar
|
||||
to reading network news or mail. In particular, it is designed to
|
||||
deal with a large number of review requests across a large number
|
||||
of projects.
|
||||
deal with a large number of stories across a large number of
|
||||
projects.
|
||||
|
||||
* Offline Use -- Gertty syncs information about changes in subscribed
|
||||
projects to a local database and local git repos. All review
|
||||
operations are performed against that database and then synced back
|
||||
to Gerrit.
|
||||
* Offline Use -- Boartty syncs information about changes in
|
||||
subscribed projects to a local database. All review operations are
|
||||
performed against that database and then synced back to Storyboard.
|
||||
|
||||
* Speed -- user actions modify locally cached content and need not
|
||||
wait for server interaction.
|
||||
|
||||
* Convenience -- because Gertty downloads all changes to local git
|
||||
repos, a single command instructs it to checkout a change into that
|
||||
repo for detailed examination or testing of larger changes.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Debian
|
||||
~~~~~~
|
||||
|
||||
Gertty is packaged in Debian and is currently available in:
|
||||
|
||||
* unstable
|
||||
* testing
|
||||
* stable
|
||||
|
||||
You can install it with::
|
||||
|
||||
apt-get install gertty
|
||||
|
||||
Fedora
|
||||
~~~~~~
|
||||
|
||||
Gertty is packaged starting in Fedora 21. You can install it with::
|
||||
|
||||
yum install python-gertty
|
||||
|
||||
openSUSE
|
||||
~~~~~~~~
|
||||
|
||||
Gertty is packaged for openSUSE 13.1 onwards. You can install it via
|
||||
`1-click install from the Open Build Service <http://software.opensuse.org/package/python-gertty>`_.
|
||||
|
||||
Gentoo
|
||||
~~~~~~
|
||||
|
||||
Gertty is available in the main Gentoo repository. You can install it with::
|
||||
|
||||
emerge gertty
|
||||
|
||||
Arch Linux
|
||||
~~~~~~~~~~
|
||||
|
||||
Gertty packages are available in the Arch User Repository packages. You
|
||||
can get the package from::
|
||||
|
||||
https://aur.archlinux.org/packages/python2-gertty/
|
||||
|
||||
Source
|
||||
~~~~~~
|
||||
|
||||
When installing from source, it is recommended (but not required) to
|
||||
install Gertty in a virtualenv. To set one up::
|
||||
install Boartty in a virtualenv. To set one up::
|
||||
|
||||
virtualenv gertty-env
|
||||
source gertty-env/bin/activate
|
||||
virtualenv boartty-env
|
||||
source boartty-env/bin/activate
|
||||
|
||||
To install the latest version from the cheeseshop::
|
||||
|
||||
pip install gertty
|
||||
pip install boartty
|
||||
|
||||
To install from a git checkout::
|
||||
|
||||
pip install .
|
||||
|
||||
Gertty uses a YAML based configuration file that it looks for at
|
||||
``~/.gertty.yaml``. Several sample configuration files are included.
|
||||
Boartty uses a YAML based configuration file that it looks for at
|
||||
``~/.boartty.yaml``. Several sample configuration files are included.
|
||||
You can find them in the examples/ directory of the
|
||||
`source distribution <https://git.openstack.org/cgit/openstack/gertty/tree/examples>`_
|
||||
or the share/gertty/examples directory after installation.
|
||||
`source distribution <https://git.openstack.org/cgit/openstack/boartty/tree/examples>`_
|
||||
or the share/boartty/examples directory after installation.
|
||||
|
||||
Select one of the sample config files, copy it to ~/.gertty.yaml and
|
||||
Select one of the sample config files, copy it to ~/.boartty.yaml and
|
||||
edit as necessary. Search for ``CHANGEME`` to find parameters that
|
||||
need to be supplied. The sample config files are as follows:
|
||||
|
||||
**minimal-gertty.yaml**
|
||||
Only contains the parameters required for Gertty to actually run.
|
||||
**minimal-boartty.yaml**
|
||||
Only contains the parameters required for Boartty to actually run.
|
||||
|
||||
**reference-gertty.yaml**
|
||||
**reference-boartty.yaml**
|
||||
An exhaustive list of all supported options with examples.
|
||||
|
||||
**openstack-gertty.yaml**
|
||||
**openstack-boartty.yaml**
|
||||
A configuration designed for use with OpenStack's installation of
|
||||
Gerrit.
|
||||
|
||||
**googlesource-gertty.yaml**
|
||||
A configuration designed for use with installations of Gerrit
|
||||
running on googlesource.com.
|
||||
You will need a Storyboard authentication token which you can generate
|
||||
or retrieve by navigating to ``Profile``, then ``Tokens`` (the "key"
|
||||
icon), or visiting the `/#!/profile/tokens` URI in your Storyboard
|
||||
installation. Issue a new token if you have not done so before, and
|
||||
give it a sufficiently long lifetime (for example, one decade). Copy
|
||||
and paste the resulting token in your ``~/.boartty.yaml`` file.
|
||||
|
||||
You will need your Gerrit password which you can generate or retrieve
|
||||
by navigating to ``Settings``, then ``HTTP Password``.
|
||||
|
||||
Gertty uses local git repositories to perform much of its work. These
|
||||
can be the same git repositories that you use when developing a
|
||||
project. Gertty will not alter the working directory or index unless
|
||||
you request it to (and even then, the usual git safeguards against
|
||||
accidentally losing work remain in place). You will need to supply
|
||||
the name of a directory where Gertty will find or clone git
|
||||
repositories for your projects as the ``git-root`` parameter.
|
||||
|
||||
The config file is designed to support multiple Gerrit instances. The
|
||||
first one is used by default, but others can be specified by supplying
|
||||
the name on the command line.
|
||||
The config file is designed to support multiple Storyboard instances.
|
||||
The first one is used by default, but others can be specified by
|
||||
supplying the name on the command line.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
After installing Gertty, you should be able to run it by invoking
|
||||
``gertty``. If you installed it in a virtualenv, you can invoke it
|
||||
without activating the virtualenv with ``/path/to/venv/bin/gertty``
|
||||
which you may wish to add to your shell aliases. Use ``gertty
|
||||
After installing Boartty, you should be able to run it by invoking
|
||||
``boartty``. If you installed it in a virtualenv, you can invoke it
|
||||
without activating the virtualenv with ``/path/to/venv/bin/boartty``
|
||||
which you may wish to add to your shell aliases. Use ``boartty
|
||||
--help`` to see a list of command line options available.
|
||||
|
||||
Once Gertty is running, you will need to start by subscribing to some
|
||||
Once Boartty is running, you will need to start by subscribing to some
|
||||
projects. Use 'L' to list all of the projects and then 's' to
|
||||
subscribe to the ones you are interested in. Hit 'L' again to shrink
|
||||
the list to your subscribed projects.
|
||||
@ -139,37 +86,27 @@ the list to your subscribed projects.
|
||||
In general, pressing the F1 key will show help text on any screen, and
|
||||
ESC will take you to the previous screen.
|
||||
|
||||
Gertty works seamlessly offline or online. All of the actions that it
|
||||
performs are first recorded in a local database (in ``~/.gertty.db``
|
||||
by default), and are then transmitted to Gerrit. If Gertty is unable
|
||||
to contact Gerrit for any reason, it will continue to operate against
|
||||
the local database, and once it re-establishes contact, it will
|
||||
process any pending changes.
|
||||
Boartty works seamlessly offline or online. All of the actions that
|
||||
it performs are first recorded in a local database (in
|
||||
``~/.boartty.db`` by default), and are then transmitted to Storyboard.
|
||||
If Boartty is unable to contact Storyboard for any reason, it will
|
||||
continue to operate against the local database, and once it
|
||||
re-establishes contact, it will process any pending changes.
|
||||
|
||||
The status bar at the top of the screen displays the current number of
|
||||
outstanding tasks that Gertty must perform in order to be fully up to
|
||||
outstanding tasks that Boartty must perform in order to be fully up to
|
||||
date. Some of these tasks are more complicated than others, and some
|
||||
of them will end up creating new tasks (for instance, one task may be
|
||||
to search for new changes in a project which will then produce 5 new
|
||||
tasks if there are 5 new changes).
|
||||
to search for new stories in a project which will then produce 5 new
|
||||
tasks if there are 5 new stories).
|
||||
|
||||
If Gertty is offline, it will so indicate in the status bar. It will
|
||||
If Boartty is offline, it will so indicate in the status bar. It will
|
||||
retry requests if needed, and will switch between offline and online
|
||||
mode automatically.
|
||||
|
||||
If you review a change while offline with a positive vote, and someone
|
||||
else leaves a negative vote on that change in the same category before
|
||||
Gertty is able to upload your review, Gertty will detect the situation
|
||||
and mark the change as "held" so that you may re-inspect the change
|
||||
and any new comments before uploading the review. The status bar will
|
||||
alert you to any held changes and direct you to a list of them (the
|
||||
`F12` key by default). When viewing a change, the "held" flag may be
|
||||
toggled with the exclamation key (`!`). Once held, a change must be
|
||||
explicitly un-held in this manner for your review to be uploaded.
|
||||
|
||||
If Gertty encounters an error, this will also be indicated in the
|
||||
status bar. You may wish to examine ~/.gertty.log to see what the
|
||||
error was. In many cases, Gertty can continue after encountering an
|
||||
If Boartty encounters an error, this will also be indicated in the
|
||||
status bar. You may wish to examine ~/.boartty.log to see what the
|
||||
error was. In many cases, Boartty can continue after encountering an
|
||||
error. The error flag will be cleared when you leave the current
|
||||
screen.
|
||||
|
||||
@ -180,28 +117,28 @@ Terminal Integration
|
||||
--------------------
|
||||
|
||||
If you use rxvt-unicode, you can add something like the following to
|
||||
``.Xresources`` to make Gerrit URLs that are displayed in your
|
||||
``.Xresources`` to make Storyboard URLs that are displayed in your
|
||||
terminal (perhaps in an email or irc client) clickable links that open
|
||||
in Gertty::
|
||||
in Boartty::
|
||||
|
||||
URxvt.perl-ext: default,matcher
|
||||
URxvt.url-launcher: sensible-browser
|
||||
URxvt.keysym.C-Delete: perl:matcher:last
|
||||
URxvt.keysym.M-Delete: perl:matcher:list
|
||||
URxvt.matcher.button: 1
|
||||
URxvt.matcher.pattern.1: https:\/\/review.example.org/(\\#\/c\/)?(\\d+)[\w]*
|
||||
URxvt.matcher.launcher.1: gertty --open $0
|
||||
URxvt.matcher.pattern.1: https:\/\/storyboard.example.org/#!/story/(\\d+)[\w]*
|
||||
URxvt.matcher.launcher.1: boartty --open $0
|
||||
|
||||
You will want to adjust the pattern to match the review site you are
|
||||
interested in; multiple patterns may be added as needed.
|
||||
You will want to adjust the pattern to match the Storyboard site you
|
||||
are interested in; multiple patterns may be added as needed.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
For information on how to contribute to Gertty, please see the
|
||||
For information on how to contribute to Boartty, please see the
|
||||
contents of the CONTRIBUTING.rst file.
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
Bugs are handled at: https://storyboard.openstack.org/#!/project/698
|
||||
Bugs are handled at: https://storyboard.openstack.org/
|
||||
|
@ -20,7 +20,7 @@ script_location = alembic
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
sqlalchemy.url = sqlite:////tmp/gertty.db
|
||||
sqlalchemy.url = sqlite:////tmp/boartty.db
|
||||
|
||||
|
||||
# Logging configuration
|
@ -15,8 +15,8 @@ config = context.config
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
import gertty.db
|
||||
target_metadata = gertty.db.metadata
|
||||
import boartty.db
|
||||
target_metadata = boartty.db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
193
boartty/alembic/versions/183755ac91df_initial_schema.py
Normal file
193
boartty/alembic/versions/183755ac91df_initial_schema.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""initial schema
|
||||
|
||||
Revision ID: 183755ac91df
|
||||
Revises: None
|
||||
Create Date: 2016-10-31 08:54:59.399741
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '183755ac91df'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('comment',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('parent_comment_key', sa.Integer(), nullable=True),
|
||||
sa.Column('content', sa.Text(), nullable=True),
|
||||
sa.Column('draft', sa.Boolean(), nullable=False),
|
||||
sa.Column('pending', sa.Boolean(), nullable=False),
|
||||
sa.Column('pending_delete', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['parent_comment_key'], ['comment.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_comment_draft'), 'comment', ['draft'], unique=False)
|
||||
op.create_index(op.f('ix_comment_id'), 'comment', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_comment_pending'), 'comment', ['pending'], unique=False)
|
||||
op.create_index(op.f('ix_comment_pending_delete'), 'comment', ['pending_delete'], unique=False)
|
||||
op.create_table('project',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('subscribed', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('updated', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_project_name'), 'project', ['name'], unique=False)
|
||||
op.create_index(op.f('ix_project_subscribed'), 'project', ['subscribed'], unique=False)
|
||||
op.create_index(op.f('ix_project_updated'), 'project', ['updated'], unique=False)
|
||||
op.create_table('sync_query',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('updated', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_sync_query_name'), 'sync_query', ['name'], unique=True)
|
||||
op.create_index(op.f('ix_sync_query_updated'), 'sync_query', ['updated'], unique=False)
|
||||
op.create_table('system',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_table('tag',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_tag_id'), 'tag', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_tag_name'), 'tag', ['name'], unique=False)
|
||||
op.create_table('topic',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('sequence', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_topic_name'), 'topic', ['name'], unique=False)
|
||||
op.create_index(op.f('ix_topic_sequence'), 'topic', ['sequence'], unique=True)
|
||||
op.create_table('user',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('email', sa.String(length=255), nullable=True),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=False)
|
||||
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_user_name'), 'user', ['name'], unique=False)
|
||||
op.create_table('project_topic',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('project_key', sa.Integer(), nullable=True),
|
||||
sa.Column('topic_key', sa.Integer(), nullable=True),
|
||||
sa.Column('sequence', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
|
||||
sa.ForeignKeyConstraint(['topic_key'], ['topic.key'], ),
|
||||
sa.PrimaryKeyConstraint('key'),
|
||||
sa.UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const')
|
||||
)
|
||||
op.create_index(op.f('ix_project_topic_project_key'), 'project_topic', ['project_key'], unique=False)
|
||||
op.create_index(op.f('ix_project_topic_topic_key'), 'project_topic', ['topic_key'], unique=False)
|
||||
op.create_table('story',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('user_key', sa.Integer(), nullable=True),
|
||||
sa.Column('status', sa.String(length=16), nullable=False),
|
||||
sa.Column('hidden', sa.Boolean(), nullable=False),
|
||||
sa.Column('subscribed', sa.Boolean(), nullable=False),
|
||||
sa.Column('title', sa.String(length=255), nullable=True),
|
||||
sa.Column('private', sa.Boolean(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated', sa.DateTime(), nullable=True),
|
||||
sa.Column('last_seen', sa.DateTime(), nullable=True),
|
||||
sa.Column('outdated', sa.Boolean(), nullable=False),
|
||||
sa.Column('pending', sa.Boolean(), nullable=False),
|
||||
sa.Column('pending_delete', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_key'], ['user.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_story_created'), 'story', ['created'], unique=False)
|
||||
op.create_index(op.f('ix_story_hidden'), 'story', ['hidden'], unique=False)
|
||||
op.create_index(op.f('ix_story_id'), 'story', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_story_last_seen'), 'story', ['last_seen'], unique=False)
|
||||
op.create_index(op.f('ix_story_outdated'), 'story', ['outdated'], unique=False)
|
||||
op.create_index(op.f('ix_story_pending'), 'story', ['pending'], unique=False)
|
||||
op.create_index(op.f('ix_story_pending_delete'), 'story', ['pending_delete'], unique=False)
|
||||
op.create_index(op.f('ix_story_status'), 'story', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_story_subscribed'), 'story', ['subscribed'], unique=False)
|
||||
op.create_index(op.f('ix_story_title'), 'story', ['title'], unique=False)
|
||||
op.create_index(op.f('ix_story_updated'), 'story', ['updated'], unique=False)
|
||||
op.create_index(op.f('ix_story_user_key'), 'story', ['user_key'], unique=False)
|
||||
op.create_table('event',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('type', sa.String(length=255), nullable=False),
|
||||
sa.Column('user_key', sa.Integer(), nullable=True),
|
||||
sa.Column('story_key', sa.Integer(), nullable=True),
|
||||
sa.Column('created', sa.DateTime(), nullable=True),
|
||||
sa.Column('comment_key', sa.Integer(), nullable=True),
|
||||
sa.Column('info', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['comment_key'], ['comment.key'], ),
|
||||
sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
|
||||
sa.ForeignKeyConstraint(['user_key'], ['user.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_event_created'), 'event', ['created'], unique=False)
|
||||
op.create_index(op.f('ix_event_id'), 'event', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_event_type'), 'event', ['type'], unique=False)
|
||||
op.create_index(op.f('ix_event_user_key'), 'event', ['user_key'], unique=False)
|
||||
op.create_table('story_tag',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('story_key', sa.Integer(), nullable=True),
|
||||
sa.Column('tag_key', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
|
||||
sa.ForeignKeyConstraint(['tag_key'], ['tag.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_story_tag_story_key'), 'story_tag', ['story_key'], unique=False)
|
||||
op.create_index(op.f('ix_story_tag_tag_key'), 'story_tag', ['tag_key'], unique=False)
|
||||
op.create_table('task',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=True),
|
||||
sa.Column('title', sa.String(length=255), nullable=True),
|
||||
sa.Column('status', sa.String(length=16), nullable=True),
|
||||
sa.Column('creator_user_key', sa.Integer(), nullable=True),
|
||||
sa.Column('story_key', sa.Integer(), nullable=True),
|
||||
sa.Column('project_key', sa.Integer(), nullable=True),
|
||||
sa.Column('assignee_user_key', sa.Integer(), nullable=True),
|
||||
sa.Column('priority', sa.String(length=16), nullable=True),
|
||||
sa.Column('link', sa.Text(), nullable=True),
|
||||
sa.Column('created', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated', sa.DateTime(), nullable=True),
|
||||
sa.Column('pending', sa.Boolean(), nullable=False),
|
||||
sa.Column('pending_delete', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assignee_user_key'], ['user.key'], ),
|
||||
sa.ForeignKeyConstraint(['creator_user_key'], ['user.key'], ),
|
||||
sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
|
||||
sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_task_assignee_user_key'), 'task', ['assignee_user_key'], unique=False)
|
||||
op.create_index(op.f('ix_task_created'), 'task', ['created'], unique=False)
|
||||
op.create_index(op.f('ix_task_creator_user_key'), 'task', ['creator_user_key'], unique=False)
|
||||
op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_task_pending'), 'task', ['pending'], unique=False)
|
||||
op.create_index(op.f('ix_task_pending_delete'), 'task', ['pending_delete'], unique=False)
|
||||
op.create_index(op.f('ix_task_project_key'), 'task', ['project_key'], unique=False)
|
||||
op.create_index(op.f('ix_task_status'), 'task', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_task_story_key'), 'task', ['story_key'], unique=False)
|
||||
op.create_index(op.f('ix_task_title'), 'task', ['title'], unique=False)
|
||||
op.create_index(op.f('ix_task_updated'), 'task', ['updated'], unique=False)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -35,28 +35,27 @@ from six.moves.urllib import parse as urlparse
|
||||
import sqlalchemy.exc
|
||||
import urwid
|
||||
|
||||
from gertty import db
|
||||
from gertty import config
|
||||
from gertty import gitrepo
|
||||
from gertty import keymap
|
||||
from gertty import mywid
|
||||
from gertty import palette
|
||||
from gertty import sync
|
||||
from gertty import search
|
||||
from gertty import requestsexceptions
|
||||
from gertty.view import change_list as view_change_list
|
||||
from gertty.view import project_list as view_project_list
|
||||
from gertty.view import change as view_change
|
||||
import gertty.view
|
||||
import gertty.version
|
||||
from boartty import db
|
||||
from boartty import config
|
||||
from boartty import keymap
|
||||
from boartty import mywid
|
||||
from boartty import palette
|
||||
from boartty import sync
|
||||
from boartty import search
|
||||
from boartty import requestsexceptions
|
||||
from boartty.view import story_list as view_story_list
|
||||
from boartty.view import project_list as view_project_list
|
||||
from boartty.view import story as view_story
|
||||
import boartty.view
|
||||
import boartty.version
|
||||
|
||||
WELCOME_TEXT = """\
|
||||
Welcome to Gertty!
|
||||
Welcome to Boartty!
|
||||
|
||||
To get started, you should subscribe to some projects. Press the "L"
|
||||
key (shift-L) to list all the projects, navigate to the ones you are
|
||||
interested in, and then press "s" to subscribe to them. Gertty will
|
||||
automatically sync changes in your subscribed projects.
|
||||
interested in, and then press "s" to subscribe to them. Boardtty will
|
||||
automatically sync stories in your subscribed projects.
|
||||
|
||||
Press the F1 key anywhere to get help. Your terminal emulator may
|
||||
require you to press function-F1 or alt-F1 instead.
|
||||
@ -233,8 +232,8 @@ class ProjectCache(object):
|
||||
def get(self, project):
|
||||
if project.key not in self.projects:
|
||||
self.projects[project.key] = dict(
|
||||
unreviewed_changes = len(project.unreviewed_changes),
|
||||
open_changes = len(project.open_changes),
|
||||
active_stories = len(project.active_stories),
|
||||
stories = len(project.stories),
|
||||
)
|
||||
return self.projects[project.key]
|
||||
|
||||
@ -272,14 +271,14 @@ class App(object):
|
||||
req_logger.setLevel(level)
|
||||
else:
|
||||
req_logger.setLevel(req_level_name)
|
||||
self.log = logging.getLogger('gertty.App')
|
||||
self.log = logging.getLogger('boartty.App')
|
||||
self.log.debug("Starting")
|
||||
|
||||
self.lock_fd = open(self.config.lock_file, 'w')
|
||||
try:
|
||||
fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except IOError:
|
||||
print("error: another instance of gertty is running for: %s" % self.config.server['name'])
|
||||
print("error: another instance of boartty is running for: %s" % self.config.server['name'])
|
||||
sys.exit(1)
|
||||
|
||||
self.project_cache = ProjectCache()
|
||||
@ -291,6 +290,7 @@ class App(object):
|
||||
self.config.keymap.updateCommandMap()
|
||||
self.search = search.SearchCompiler(self.config.username)
|
||||
self.db = db.Database(self, self.config.dburi, self.search)
|
||||
self.initSystemData()
|
||||
self.sync = sync.Sync(self, disable_background_sync)
|
||||
|
||||
self.status = StatusHeader(self)
|
||||
@ -405,6 +405,7 @@ class App(object):
|
||||
if not self.screens:
|
||||
return
|
||||
while self.screens:
|
||||
self.log.debug("screens %s" % (target_widget,))
|
||||
widget = self.screens.pop()
|
||||
if (not target_widget) or (widget is target_widget):
|
||||
break
|
||||
@ -415,9 +416,9 @@ class App(object):
|
||||
self.frame.body = widget
|
||||
self.refresh(force=True)
|
||||
|
||||
def findChangeList(self):
|
||||
def findStoryList(self):
|
||||
for widget in reversed(self.screens):
|
||||
if isinstance(widget, view_change_list.ChangeListView):
|
||||
if isinstance(widget, view_story_list.StoryListView):
|
||||
return widget
|
||||
return None
|
||||
|
||||
@ -450,6 +451,7 @@ class App(object):
|
||||
self.status.refresh()
|
||||
|
||||
def updateStatusQueries(self):
|
||||
return # TODO: storyboard
|
||||
with self.db.getSession() as session:
|
||||
held = len(session.getHeld())
|
||||
self.status.update(held=held)
|
||||
@ -515,6 +517,7 @@ class App(object):
|
||||
lambda button: self.backScreen())
|
||||
self.popup(dialog, min_width=76, min_height=len(lines)+4)
|
||||
|
||||
#storyboard
|
||||
def _syncOneChangeFromQuery(self, query):
|
||||
number = changeid = restid = None
|
||||
if query.startswith("change:"):
|
||||
@ -574,7 +577,7 @@ class App(object):
|
||||
with self.db.getSession() as session:
|
||||
try:
|
||||
changes = session.getChanges(query)
|
||||
except gertty.search.SearchSyntaxError as e:
|
||||
except boartty.search.SearchSyntaxError as e:
|
||||
return self.error(e.message)
|
||||
except sqlalchemy.exc.OperationalError as e:
|
||||
return self.error(e.message)
|
||||
@ -587,9 +590,9 @@ class App(object):
|
||||
if change_key:
|
||||
view = view_change.ChangeView(self, change_key)
|
||||
else:
|
||||
view = view_change_list.ChangeListView(self, query)
|
||||
view = view_story_list.StoryListView(self, query)
|
||||
self.changeScreen(view)
|
||||
except gertty.view.DisplayError as e:
|
||||
except boartty.view.DisplayError as e:
|
||||
return self.error(e.message)
|
||||
|
||||
def searchDialog(self, default):
|
||||
@ -677,15 +680,17 @@ class App(object):
|
||||
self.help()
|
||||
elif keymap.QUIT in commands:
|
||||
self.quit()
|
||||
elif keymap.CHANGE_SEARCH in commands:
|
||||
elif keymap.STORY_SEARCH in commands:
|
||||
self.searchDialog('')
|
||||
elif keymap.NEW_STORY in commands:
|
||||
self.newStory()
|
||||
elif keymap.LIST_HELD in commands:
|
||||
self.doSearch("is:held")
|
||||
elif key in self.config.dashboards:
|
||||
d = self.config.dashboards[key]
|
||||
view = view_change_list.ChangeListView(self, d['query'], d['name'],
|
||||
sort_by=d.get('sort-by'),
|
||||
reverse=d.get('reverse'))
|
||||
view = view_story_list.StoryListView(self, d['query'], d['name'],
|
||||
sort_by=d.get('sort-by'),
|
||||
reverse=d.get('reverse'))
|
||||
self.changeScreen(view)
|
||||
elif keymap.FURTHER_INPUT in commands:
|
||||
self.input_buffer.append(key)
|
||||
@ -711,6 +716,8 @@ class App(object):
|
||||
self.loop.screen.clear()
|
||||
|
||||
def time(self, dt):
|
||||
if dt is None:
|
||||
return None
|
||||
utc = dt.replace(tzinfo=dateutil.tz.tzutc())
|
||||
if self.config.utc:
|
||||
return utc
|
||||
@ -753,6 +760,7 @@ class App(object):
|
||||
else:
|
||||
self.log.error("Unable to parse command %s with data %s" % (command, data))
|
||||
|
||||
#storyboard
|
||||
def toggleHeldChange(self, change_key):
|
||||
with self.db.getSession() as session:
|
||||
change = session.getChange(change_key)
|
||||
@ -767,88 +775,54 @@ class App(object):
|
||||
self.updateStatusQueries()
|
||||
return ret
|
||||
|
||||
def localCheckoutCommit(self, project_name, commit_sha):
|
||||
repo = gitrepo.get_repo(project_name, self.config)
|
||||
try:
|
||||
repo.checkout(commit_sha)
|
||||
dialog = mywid.MessageDialog('Checkout', 'Change checked out in %s' % repo.path)
|
||||
min_height=8
|
||||
except gitrepo.GitCheckoutError as e:
|
||||
dialog = mywid.MessageDialog('Error', e.msg)
|
||||
min_height=12
|
||||
urwid.connect_signal(dialog, 'close',
|
||||
lambda button: self.backScreen())
|
||||
self.popup(dialog, min_height=min_height)
|
||||
def newStory(self):
|
||||
dialog = view_story.NewStoryDialog(self)
|
||||
urwid.connect_signal(dialog, 'save',
|
||||
lambda button: self.saveNewStory(dialog))
|
||||
urwid.connect_signal(dialog, 'cancel',
|
||||
lambda button: self.cancelNewStory(dialog))
|
||||
self.popup(dialog,
|
||||
relative_width=50, relative_height=25,
|
||||
min_width=60, min_height=8)
|
||||
|
||||
def localCherryPickCommit(self, project_name, commit_sha):
|
||||
repo = gitrepo.get_repo(project_name, self.config)
|
||||
try:
|
||||
repo.cherryPick(commit_sha)
|
||||
dialog = mywid.MessageDialog('Cherry-Pick', 'Change cherry-picked in %s' % repo.path)
|
||||
min_height=8
|
||||
except gitrepo.GitCheckoutError as e:
|
||||
dialog = mywid.MessageDialog('Error', e.msg)
|
||||
min_height=12
|
||||
urwid.connect_signal(dialog, 'close',
|
||||
lambda button: self.backScreen())
|
||||
self.popup(dialog, min_height=min_height)
|
||||
def cancelNewStory(self, dialog):
|
||||
self.backScreen()
|
||||
|
||||
def saveReviews(self, revision_keys, approvals, message, upload, submit):
|
||||
message_keys = []
|
||||
def saveNewStory(self, dialog):
|
||||
with self.db.getSession() as session:
|
||||
account = session.getAccountByUsername(self.config.username)
|
||||
for revision_key in revision_keys:
|
||||
k = self._saveReview(session, account, revision_key,
|
||||
approvals, message, upload, submit)
|
||||
if k:
|
||||
message_keys.append(k)
|
||||
return message_keys
|
||||
story = session.createStory(
|
||||
title=dialog.title_field.edit_text,
|
||||
description=dialog.description_field.edit_text,
|
||||
pending=True)
|
||||
task = story.addTask(
|
||||
project=session.getProjectByID(dialog.project_button.key),
|
||||
title=dialog.title_field.edit_text,
|
||||
pending=True)
|
||||
|
||||
def _saveReview(self, session, account, revision_key,
|
||||
approvals, message, upload, submit):
|
||||
message_key = None
|
||||
revision = session.getRevision(revision_key)
|
||||
change = revision.change
|
||||
draft_approvals = {}
|
||||
for approval in change.draft_approvals:
|
||||
draft_approvals[approval.category] = approval
|
||||
self.sync.submitTask(
|
||||
sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY))
|
||||
self.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
self.backScreen()
|
||||
|
||||
categories = set()
|
||||
for label in change.permitted_labels:
|
||||
categories.add(label.category)
|
||||
for category in categories:
|
||||
value = approvals.get(category, 0)
|
||||
approval = draft_approvals.get(category)
|
||||
if not approval:
|
||||
approval = change.createApproval(account, category, 0, draft=True)
|
||||
draft_approvals[category] = approval
|
||||
approval.value = value
|
||||
draft_message = revision.getPendingMessage()
|
||||
if not draft_message:
|
||||
draft_message = revision.getDraftMessage()
|
||||
if not draft_message:
|
||||
if message or upload:
|
||||
draft_message = revision.createMessage(None, account,
|
||||
datetime.datetime.utcnow(),
|
||||
'', draft=True)
|
||||
if draft_message:
|
||||
draft_message.created = datetime.datetime.utcnow()
|
||||
draft_message.message = message
|
||||
draft_message.pending = upload
|
||||
message_key = draft_message.key
|
||||
if upload:
|
||||
change.reviewed = True
|
||||
self.project_cache.clear(change.project)
|
||||
if submit:
|
||||
change.status = 'SUBMITTED'
|
||||
change.pending_status = True
|
||||
change.pending_status_message = None
|
||||
return message_key
|
||||
def initSystemData(self):
|
||||
with self.db.getSession() as session:
|
||||
system = session.getSystem()
|
||||
if system is None:
|
||||
self.user_id = None
|
||||
else:
|
||||
self.user_id = system.user_id
|
||||
|
||||
def setUserID(self, user_id):
|
||||
with self.db.getSession() as session:
|
||||
system = session.getSystem()
|
||||
if system is None:
|
||||
system = session.createSystem()
|
||||
system.user_id = self.user_id = user_id
|
||||
|
||||
|
||||
def version():
|
||||
return "Gertty version: %s" % gertty.version.version_info.release_string()
|
||||
return "Boardtty version: %s" % boartty.version.version_info.release_string()
|
||||
|
||||
class PrintKeymapAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
@ -900,10 +874,10 @@ def main():
|
||||
help='print the palette attribute names to stdout')
|
||||
parser.add_argument('--open', nargs=1, action=OpenChangeAction,
|
||||
metavar='URL',
|
||||
help='open the given URL in a running Gertty')
|
||||
help='open the given URL in a running Boardtty')
|
||||
parser.add_argument('--version', dest='version', action='version',
|
||||
version=version(),
|
||||
help='show Gertty\'s version')
|
||||
help='show Boardtty\'s version')
|
||||
parser.add_argument('-p', dest='palette', default='default',
|
||||
help='color palette to use')
|
||||
parser.add_argument('-k', dest='keymap', default='default',
|
@ -22,7 +22,7 @@ import re
|
||||
import six
|
||||
import urwid
|
||||
|
||||
from gertty import mywid
|
||||
from boartty import mywid
|
||||
|
||||
try:
|
||||
OrderedDict = collections.OrderedDict
|
@ -26,27 +26,24 @@ import yaml
|
||||
from six.moves.urllib import parse as urlparse
|
||||
import voluptuous as v
|
||||
|
||||
import gertty.commentlink
|
||||
import gertty.palette
|
||||
import gertty.keymap
|
||||
import boartty.commentlink
|
||||
import boartty.palette
|
||||
import boartty.keymap
|
||||
|
||||
try:
|
||||
OrderedDict = collections.OrderedDict
|
||||
except AttributeError:
|
||||
OrderedDict = ordereddict.OrderedDict
|
||||
|
||||
DEFAULT_CONFIG_PATH='~/.gertty.yaml'
|
||||
DEFAULT_CONFIG_PATH='~/.boartty.yaml'
|
||||
|
||||
class ConfigSchema(object):
|
||||
server = {v.Required('name'): str,
|
||||
v.Required('url'): str,
|
||||
v.Required('username'): str,
|
||||
'password': str,
|
||||
v.Required('token'): str,
|
||||
'verify-ssl': bool,
|
||||
'ssl-ca-path': str,
|
||||
'dburi': str,
|
||||
v.Required('git-root'): str,
|
||||
'git-url': str,
|
||||
'log-file': str,
|
||||
'socket': str,
|
||||
'auth-type': v.Any('basic', 'digest', 'form'),
|
||||
@ -100,7 +97,7 @@ class ConfigSchema(object):
|
||||
|
||||
hide_comments = [hide_comment]
|
||||
|
||||
change_list_options = {'sort-by': sort_by,
|
||||
story_list_options = {'sort-by': sort_by,
|
||||
'reverse': bool}
|
||||
|
||||
keymap = {v.Required('name'): str,
|
||||
@ -117,14 +114,13 @@ class ConfigSchema(object):
|
||||
'commentlinks': self.commentlinks,
|
||||
'dashboards': self.dashboards,
|
||||
'reviewkeys': self.reviewkeys,
|
||||
'change-list-query': str,
|
||||
'story-list-query': str,
|
||||
'diff-view': str,
|
||||
'hide-comments': self.hide_comments,
|
||||
'thread-changes': bool,
|
||||
'display-times-in-utc': bool,
|
||||
'handle-mouse': bool,
|
||||
'breadcrumbs': bool,
|
||||
'change-list-options': self.change_list_options,
|
||||
'story-list-options': self.story_list_options,
|
||||
'expire-age': str,
|
||||
})
|
||||
return schema
|
||||
@ -149,21 +145,17 @@ class Config(object):
|
||||
self.url = url
|
||||
result = urlparse.urlparse(url)
|
||||
self.hostname = result.netloc
|
||||
self.username = server['username']
|
||||
self.password = server.get('password')
|
||||
if self.password is None:
|
||||
self.password = getpass.getpass("Password for %s (%s): "
|
||||
% (self.url, self.username))
|
||||
else:
|
||||
# Ensure file is only readable by user as password is stored in
|
||||
# file.
|
||||
mode = os.stat(self.path).st_mode & 0o0777
|
||||
if not mode == 0o600:
|
||||
print (
|
||||
"Error: Config file '{}' contains a password and does "
|
||||
"not have permissions set to 0600.\n"
|
||||
"Permissions are: {}".format(self.path, oct(mode)))
|
||||
exit(1)
|
||||
self.token = server['token']
|
||||
self.username = '' # TODO: storyboard
|
||||
# Ensure file is only readable by user as password is stored in
|
||||
# file.
|
||||
mode = os.stat(self.path).st_mode & 0o0777
|
||||
if not mode == 0o600:
|
||||
print (
|
||||
"Error: Config file '{}' contains an api key and does "
|
||||
"not have permissions set to 0600.\n"
|
||||
"Permissions are: {}".format(self.path, oct(mode)))
|
||||
exit(1)
|
||||
self.auth_type = server.get('auth-type', 'digest')
|
||||
self.verify_ssl = server.get('verify-ssl', True)
|
||||
if not self.verify_ssl:
|
||||
@ -171,54 +163,49 @@ class Config(object):
|
||||
self.ssl_ca_path = server.get('ssl-ca-path', None)
|
||||
if self.ssl_ca_path is not None:
|
||||
self.ssl_ca_path = os.path.expanduser(self.ssl_ca_path)
|
||||
# Gertty itself uses the Requests library
|
||||
# Boardtty itself uses the Requests library
|
||||
os.environ['REQUESTS_CA_BUNDLE'] = self.ssl_ca_path
|
||||
# And this is to allow Git callouts
|
||||
os.environ['GIT_SSL_CAINFO'] = self.ssl_ca_path
|
||||
self.git_root = os.path.expanduser(server['git-root'])
|
||||
git_url = server.get('git-url', self.url + 'p/')
|
||||
if not git_url.endswith('/'):
|
||||
git_url += '/'
|
||||
self.git_url = git_url
|
||||
self.dburi = server.get('dburi',
|
||||
'sqlite:///' + os.path.expanduser('~/.gertty.db'))
|
||||
socket_path = server.get('socket', '~/.gertty.sock')
|
||||
'sqlite:///' + os.path.expanduser('~/.boartty.db'))
|
||||
socket_path = server.get('socket', '~/.boartty.sock')
|
||||
self.socket_path = os.path.expanduser(socket_path)
|
||||
log_file = server.get('log-file', '~/.gertty.log')
|
||||
log_file = server.get('log-file', '~/.boartty.log')
|
||||
self.log_file = os.path.expanduser(log_file)
|
||||
lock_file = server.get('lock-file', '~/.gertty.%s.lock' % server['name'])
|
||||
lock_file = server.get('lock-file', '~/.boartty.%s.lock' % server['name'])
|
||||
self.lock_file = os.path.expanduser(lock_file)
|
||||
|
||||
self.palettes = {'default': gertty.palette.Palette({}),
|
||||
'light': gertty.palette.Palette(gertty.palette.LIGHT_PALETTE),
|
||||
self.palettes = {'default': boartty.palette.Palette({}),
|
||||
'light': boartty.palette.Palette(boartty.palette.LIGHT_PALETTE),
|
||||
}
|
||||
for p in self.config.get('palettes', []):
|
||||
if p['name'] not in self.palettes:
|
||||
self.palettes[p['name']] = gertty.palette.Palette(p)
|
||||
self.palettes[p['name']] = boartty.palette.Palette(p)
|
||||
else:
|
||||
self.palettes[p['name']].update(p)
|
||||
self.palette = self.palettes[self.config.get('palette', palette)]
|
||||
|
||||
self.keymaps = {'default': gertty.keymap.KeyMap({}),
|
||||
'vi': gertty.keymap.KeyMap(gertty.keymap.VI_KEYMAP)}
|
||||
self.keymaps = {'default': boartty.keymap.KeyMap({}),
|
||||
'vi': boartty.keymap.KeyMap(boartty.keymap.VI_KEYMAP)}
|
||||
for p in self.config.get('keymaps', []):
|
||||
if p['name'] not in self.keymaps:
|
||||
self.keymaps[p['name']] = gertty.keymap.KeyMap(p)
|
||||
self.keymaps[p['name']] = boartty.keymap.KeyMap(p)
|
||||
else:
|
||||
self.keymaps[p['name']].update(p)
|
||||
self.keymap = self.keymaps[self.config.get('keymap', keymap)]
|
||||
|
||||
self.commentlinks = [gertty.commentlink.CommentLink(c)
|
||||
self.commentlinks = [boartty.commentlink.CommentLink(c)
|
||||
for c in self.config.get('commentlinks', [])]
|
||||
self.commentlinks.append(
|
||||
gertty.commentlink.CommentLink(dict(
|
||||
boartty.commentlink.CommentLink(dict(
|
||||
match="(?P<url>https?://\\S*)",
|
||||
replacements=[
|
||||
dict(link=dict(
|
||||
text="{url}",
|
||||
url="{url}"))])))
|
||||
|
||||
self.project_change_list_query = self.config.get('change-list-query', 'status:open')
|
||||
self.project_story_list_query = self.config.get('story-list-query', '')
|
||||
|
||||
self.diff_view = self.config.get('diff-view', 'side-by-side')
|
||||
|
||||
@ -235,15 +222,14 @@ class Config(object):
|
||||
for h in self.config.get('hide-comments', []):
|
||||
self.hide_comments.append(re.compile(h['author']))
|
||||
|
||||
self.thread_changes = self.config.get('thread-changes', True)
|
||||
self.utc = self.config.get('display-times-in-utc', False)
|
||||
self.breadcrumbs = self.config.get('breadcrumbs', True)
|
||||
self.handle_mouse = self.config.get('handle-mouse', True)
|
||||
|
||||
change_list_options = self.config.get('change-list-options', {})
|
||||
self.change_list_options = {
|
||||
'sort-by': change_list_options.get('sort-by', 'number'),
|
||||
'reverse': change_list_options.get('reverse', False)}
|
||||
story_list_options = self.config.get('story-list-options', {})
|
||||
self.story_list_options = {
|
||||
'sort-by': story_list_options.get('sort-by', 'number'),
|
||||
'reverse': story_list_options.get('reverse', False)}
|
||||
|
||||
self.expire_age = self.config.get('expire-age', '2 months')
|
||||
|
||||
@ -254,11 +240,11 @@ class Config(object):
|
||||
return None
|
||||
|
||||
def printSample(self):
|
||||
filename = 'share/gertty/examples'
|
||||
print("""Gertty requires a configuration file at ~/.gertty.yaml
|
||||
filename = 'share/boartty/examples'
|
||||
print("""Boardtty requires a configuration file at ~/.boartty.yaml
|
||||
If the file contains a password then permissions must be set to 0600.
|
||||
|
||||
Several sample configuration files were installed with Gertty and are
|
||||
Several sample configuration files were installed with Boardtty and are
|
||||
available in %s in the root of the installation.
|
||||
|
||||
For more information, please see the README.
|
676
boartty/db.py
Normal file
676
boartty/db.py
Normal file
@ -0,0 +1,676 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import alembic
|
||||
import alembic.config
|
||||
import six
|
||||
import sqlalchemy
|
||||
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text, UniqueConstraint
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.orm import mapper, sessionmaker, relationship, scoped_session, joinedload
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.sql import exists
|
||||
from sqlalchemy.sql.expression import and_
|
||||
|
||||
metadata = MetaData()
|
||||
system_table = Table(
|
||||
'system', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('user_id', Integer),
|
||||
)
|
||||
project_table = Table(
|
||||
'project', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('name', String(255), index=True, nullable=False),
|
||||
Column('subscribed', Boolean, index=True, default=False),
|
||||
Column('description', Text),
|
||||
Column('updated', DateTime, index=True),
|
||||
)
|
||||
topic_table = Table(
|
||||
'topic', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('name', String(255), index=True, nullable=False),
|
||||
Column('sequence', Integer, index=True, unique=True, nullable=False),
|
||||
)
|
||||
project_topic_table = Table(
|
||||
'project_topic', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('project_key', Integer, ForeignKey("project.key"), index=True),
|
||||
Column('topic_key', Integer, ForeignKey("topic.key"), index=True),
|
||||
Column('sequence', Integer, nullable=False),
|
||||
UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
|
||||
)
|
||||
story_table = Table(
|
||||
'story', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('user_key', Integer, ForeignKey("user.key"), index=True),
|
||||
Column('status', String(16), index=True, nullable=False),
|
||||
Column('hidden', Boolean, index=True, nullable=False),
|
||||
Column('subscribed', Boolean, index=True, nullable=False),
|
||||
Column('title', String(255), index=True),
|
||||
Column('private', Boolean, nullable=False),
|
||||
Column('description', Text),
|
||||
Column('created', DateTime, index=True),
|
||||
# TODO: make sure updated is never null in storyboard
|
||||
Column('updated', DateTime, index=True),
|
||||
Column('last_seen', DateTime, index=True),
|
||||
Column('outdated', Boolean, index=True, nullable=False),
|
||||
Column('pending', Boolean, index=True, nullable=False),
|
||||
Column('pending_delete', Boolean, index=True, nullable=False),
|
||||
)
|
||||
tag_table = Table(
|
||||
'tag', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('name', String(255), index=True, nullable=False),
|
||||
)
|
||||
story_tag_table = Table(
|
||||
'story_tag', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('story_key', Integer, ForeignKey("story.key"), index=True),
|
||||
Column('tag_key', Integer, ForeignKey("tag.key"), index=True),
|
||||
)
|
||||
task_table = Table(
|
||||
'task', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('title', String(255), index=True),
|
||||
Column('status', String(16), index=True),
|
||||
Column('creator_user_key', Integer, ForeignKey("user.key"), index=True),
|
||||
Column('story_key', Integer, ForeignKey("story.key"), index=True),
|
||||
Column('project_key', Integer, ForeignKey("project.key"), index=True),
|
||||
Column('assignee_user_key', Integer, ForeignKey("user.key"), index=True),
|
||||
Column('priority', String(16)),
|
||||
Column('link', Text),
|
||||
Column('created', DateTime, index=True),
|
||||
# TODO: make sure updated is never null in storyboard
|
||||
Column('updated', DateTime, index=True),
|
||||
Column('pending', Boolean, index=True, nullable=False),
|
||||
Column('pending_delete', Boolean, index=True, nullable=False),
|
||||
)
|
||||
event_table = Table(
|
||||
'event', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('type', String(255), index=True, nullable=False),
|
||||
Column('user_key', Integer, ForeignKey("user.key"), index=True),
|
||||
Column('story_key', Integer, ForeignKey('story.key'), nullable=True),
|
||||
#Column('worklist_key', Integer, ForeignKey('worklist.key'), nullable=True),
|
||||
#Column('board_key', Integer, ForeignKey('board.key'), nullable=True),
|
||||
Column('created', DateTime, index=True),
|
||||
Column('comment_key', Integer, ForeignKey('comment.key'), nullable=True),
|
||||
Column('user_key', ForeignKey('user.key'), nullable=True),
|
||||
Column('info', Text),
|
||||
)
|
||||
comment_table = Table(
|
||||
'comment', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('parent_comment_key', Integer, ForeignKey('comment.key'), nullable=True),
|
||||
Column('content', Text),
|
||||
Column('draft', Boolean, index=True, nullable=False),
|
||||
Column('pending', Boolean, index=True, nullable=False),
|
||||
Column('pending_delete', Boolean, index=True, nullable=False),
|
||||
)
|
||||
user_table = Table(
|
||||
'user', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('id', Integer, index=True),
|
||||
Column('name', String(255), index=True),
|
||||
Column('email', String(255), index=True),
|
||||
)
|
||||
sync_query_table = Table(
|
||||
'sync_query', metadata,
|
||||
Column('key', Integer, primary_key=True),
|
||||
Column('name', String(255), index=True, unique=True, nullable=False),
|
||||
Column('updated', DateTime, index=True),
|
||||
)
|
||||
|
||||
class System(object):
|
||||
def __init__(self, user_id=None):
|
||||
self.user_id = user_id
|
||||
|
||||
class User(object):
|
||||
def __init__(self, id, name=None, email=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.email = email
|
||||
|
||||
class Project(object):
|
||||
def __init__(self, id, name, subscribed=False, description=''):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.subscribed = subscribed
|
||||
self.description = description
|
||||
|
||||
def createChange(self, *args, **kw):
|
||||
session = Session.object_session(self)
|
||||
args = [self] + list(args)
|
||||
c = Change(*args, **kw)
|
||||
self.changes.append(c)
|
||||
session.add(c)
|
||||
session.flush()
|
||||
return c
|
||||
|
||||
def createBranch(self, *args, **kw):
|
||||
session = Session.object_session(self)
|
||||
args = [self] + list(args)
|
||||
b = Branch(*args, **kw)
|
||||
self.branches.append(b)
|
||||
session.add(b)
|
||||
session.flush()
|
||||
return b
|
||||
|
||||
class ProjectTopic(object):
|
||||
def __init__(self, project, topic, sequence):
|
||||
self.project_key = project.key
|
||||
self.topic_key = topic.key
|
||||
self.sequence = sequence
|
||||
|
||||
class Topic(object):
|
||||
def __init__(self, name, sequence):
|
||||
self.name = name
|
||||
self.sequence = sequence
|
||||
|
||||
def addProject(self, project):
|
||||
session = Session.object_session(self)
|
||||
seq = max([x.sequence for x in self.project_topics] + [0])
|
||||
pt = ProjectTopic(project, self, seq+1)
|
||||
self.project_topics.append(pt)
|
||||
self.projects.append(project)
|
||||
session.add(pt)
|
||||
session.flush()
|
||||
|
||||
def removeProject(self, project):
|
||||
session = Session.object_session(self)
|
||||
for pt in self.project_topics:
|
||||
if pt.project_key == project.key:
|
||||
self.project_topics.remove(pt)
|
||||
session.delete(pt)
|
||||
self.projects.remove(project)
|
||||
session.flush()
|
||||
|
||||
def format_name(self):
|
||||
name = 'Anonymous Coward'
|
||||
if self.creator:
|
||||
if self.creator.name:
|
||||
name = self.creator.name
|
||||
elif self.creator.email:
|
||||
name = self.creator.email
|
||||
return name
|
||||
|
||||
class Story(object):
|
||||
def __init__(self, id=None, creator=None, created=None, title=None,
|
||||
description=None, pending=False):
|
||||
self.id = id
|
||||
self.creator = creator
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.status = 'active'
|
||||
self.created = created
|
||||
self.private = False
|
||||
self.outdated = False
|
||||
self.hidden = False
|
||||
self.subscribed = False
|
||||
self.pending = pending
|
||||
self.pending_delete = False
|
||||
|
||||
@property
|
||||
def creator_name(self):
|
||||
return format_name(self)
|
||||
|
||||
def addEvent(self, *args, **kw):
|
||||
session = Session.object_session(self)
|
||||
e = Event(*args, **kw)
|
||||
e.story_key = self.key
|
||||
self.events.append(e)
|
||||
session.add(e)
|
||||
session.flush()
|
||||
return e
|
||||
|
||||
def addTask(self, *args, **kw):
|
||||
session = Session.object_session(self)
|
||||
t = Task(*args, **kw)
|
||||
t.story_key = self.key
|
||||
self.tasks.append(t)
|
||||
session.add(t)
|
||||
session.flush()
|
||||
return t
|
||||
|
||||
def getDraftCommentEvent(self, parent):
|
||||
for event in self.events:
|
||||
if (event.comment and event.comment.draft and
|
||||
event.comment.parent==parent):
|
||||
return event
|
||||
return None
|
||||
|
||||
def setDraftComment(self, creator, parent, content):
|
||||
event = self.getDraftCommentEvent(parent)
|
||||
if event is None:
|
||||
event = self.addEvent(type='user_comment', creator=creator)
|
||||
event.addComment()
|
||||
event.comment.content = content
|
||||
event.comment.draft = True
|
||||
event.comment.parent = parent
|
||||
return event
|
||||
|
||||
class Tag(object):
|
||||
def __init__(self, id, name):
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
||||
class StoryTag(object):
|
||||
def __init__(self, story, tag):
|
||||
self.story_key = story.key
|
||||
self.tag_key = tag.key
|
||||
|
||||
class Task(object):
|
||||
def __init__(self, id=None, title=None, status=None, creator=None,
|
||||
created=None, pending=False, pending_delete=False,
|
||||
project=None):
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.status = status
|
||||
self.pending = pending
|
||||
self.pending_delete = pending_delete
|
||||
self.creator = creator
|
||||
self.created = created
|
||||
self.project = project
|
||||
|
||||
class Event(object):
|
||||
def __init__(self, id=None, type=None, creator=None, created=None, info=None):
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.creator = creator
|
||||
if created is None:
|
||||
created = datetime.datetime.utcnow()
|
||||
self.created = created
|
||||
self.info = info
|
||||
|
||||
@property
|
||||
def creator_name(self):
|
||||
return format_name(self)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return re.sub('_', ' ', self.type)
|
||||
|
||||
def addComment(self, *args, **kw):
|
||||
session = Session.object_session(self)
|
||||
c = Comment(*args, **kw)
|
||||
session.add(c)
|
||||
session.flush()
|
||||
self.comment_key = c.key
|
||||
return c
|
||||
|
||||
class Comment(object):
|
||||
def __init__(self, id=None, content=None, parent=None, draft=False,
|
||||
pending=False, pending_delete=False):
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.parent = parent
|
||||
self.pending = pending
|
||||
self.pending_delete = pending_delete
|
||||
self.draft = draft
|
||||
|
||||
class SyncQuery(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
mapper(System, system_table)
|
||||
mapper(User, user_table)
|
||||
mapper(Project, project_table, properties=dict(
|
||||
topics=relationship(Topic,
|
||||
secondary=project_topic_table,
|
||||
order_by=topic_table.c.name,
|
||||
viewonly=True),
|
||||
active_stories=relationship(Story,
|
||||
secondary=task_table,
|
||||
primaryjoin=and_(project_table.c.key==task_table.c.project_key,
|
||||
story_table.c.key==task_table.c.story_key,
|
||||
story_table.c.status=='active'),
|
||||
order_by=story_table.c.id,
|
||||
),
|
||||
stories=relationship(Story,
|
||||
secondary=task_table,
|
||||
order_by=story_table.c.id,
|
||||
),
|
||||
))
|
||||
mapper(Topic, topic_table, properties=dict(
|
||||
projects=relationship(Project,
|
||||
secondary=project_topic_table,
|
||||
order_by=project_table.c.name,
|
||||
viewonly=True),
|
||||
project_topics=relationship(ProjectTopic),
|
||||
))
|
||||
mapper(ProjectTopic, project_topic_table)
|
||||
mapper(Story, story_table, properties=dict(
|
||||
creator=relationship(User),
|
||||
tags=relationship(Tag,
|
||||
secondary=story_tag_table,
|
||||
order_by=tag_table.c.name,
|
||||
#viewonly=True
|
||||
),
|
||||
tasks=relationship(Task, backref='story',
|
||||
cascade='all, delete-orphan'),
|
||||
events=relationship(Event, backref='story',
|
||||
cascade='all, delete-orphan'),
|
||||
))
|
||||
mapper(Tag, tag_table)
|
||||
mapper(StoryTag, story_tag_table)
|
||||
mapper(Task, task_table, properties=dict(
|
||||
project=relationship(Project),
|
||||
assignee=relationship(User, foreign_keys=task_table.c.assignee_user_key),
|
||||
creator=relationship(User, foreign_keys=task_table.c.creator_user_key),
|
||||
))
|
||||
mapper(Event, event_table, properties=dict(
|
||||
creator=relationship(User),
|
||||
comment=relationship(Comment, backref='event'),
|
||||
))
|
||||
mapper(Comment, comment_table, properties=dict(
|
||||
parent=relationship(Comment, remote_side=[comment_table.c.key],backref='children'),
|
||||
))
|
||||
mapper(SyncQuery, sync_query_table)
|
||||
|
||||
def match(expr, item):
|
||||
if item is None:
|
||||
return False
|
||||
return re.match(expr, item) is not None
|
||||
|
||||
@sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "connect")
|
||||
def add_sqlite_match(dbapi_connection, connection_record):
|
||||
dbapi_connection.create_function("matches", 2, match)
|
||||
|
||||
class Database(object):
|
||||
def __init__(self, app, dburi, search):
|
||||
self.log = logging.getLogger('boartty.db')
|
||||
self.dburi = dburi
|
||||
self.search = search
|
||||
self.engine = create_engine(self.dburi)
|
||||
#metadata.create_all(self.engine)
|
||||
self.migrate(app)
|
||||
# If we want the objects returned from query() to be usable
|
||||
# outside of the session, we need to expunge them from the session,
|
||||
# and since the DatabaseSession always calls commit() on the session
|
||||
# when the context manager exits, we need to inform the session to
|
||||
# expire objects when it does so.
|
||||
self.session_factory = sessionmaker(bind=self.engine,
|
||||
expire_on_commit=False,
|
||||
autoflush=False)
|
||||
self.session = scoped_session(self.session_factory)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def getSession(self):
|
||||
return DatabaseSession(self)
|
||||
|
||||
def migrate(self, app):
|
||||
conn = self.engine.connect()
|
||||
context = alembic.migration.MigrationContext.configure(conn)
|
||||
current_rev = context.get_current_revision()
|
||||
self.log.debug('Current migration revision: %s' % current_rev)
|
||||
|
||||
has_table = self.engine.dialect.has_table(conn, "project")
|
||||
|
||||
config = alembic.config.Config()
|
||||
config.set_main_option("script_location", "boartty:alembic")
|
||||
config.set_main_option("sqlalchemy.url", self.dburi)
|
||||
config.boartty_app = app
|
||||
|
||||
if current_rev is None and has_table:
|
||||
self.log.debug('Stamping database as initial revision')
|
||||
alembic.command.stamp(config, "44402069e137")
|
||||
alembic.command.upgrade(config, 'head')
|
||||
|
||||
class DatabaseSession(object):
|
||||
def __init__(self, database):
|
||||
self.database = database
|
||||
self.session = database.session
|
||||
self.search = database.search
|
||||
|
||||
def __enter__(self):
|
||||
self.database.lock.acquire()
|
||||
self.start = time.time()
|
||||
return self
|
||||
|
||||
def __exit__(self, etype, value, tb):
|
||||
if etype:
|
||||
self.session().rollback()
|
||||
else:
|
||||
self.session().commit()
|
||||
self.session().close()
|
||||
self.session = None
|
||||
end = time.time()
|
||||
self.database.log.debug("Database lock held %s seconds" % (end-self.start,))
|
||||
self.database.lock.release()
|
||||
|
||||
def abort(self):
|
||||
self.session().rollback()
|
||||
|
||||
def commit(self):
|
||||
self.session().commit()
|
||||
|
||||
def delete(self, obj):
|
||||
self.session().delete(obj)
|
||||
|
||||
def vacuum(self):
|
||||
self.session().execute("VACUUM")
|
||||
|
||||
def getProjects(self, subscribed=False, active=False, topicless=False):
|
||||
"""Retrieve projects.
|
||||
|
||||
:param subscribed: If True limit to only subscribed projects.
|
||||
:param active: If True limit to only projects with active
|
||||
stories.
|
||||
:param topicless: If True limit to only projects without topics.
|
||||
"""
|
||||
query = self.session().query(Project)
|
||||
if subscribed:
|
||||
query = query.filter_by(subscribed=subscribed)
|
||||
if active:
|
||||
query = query.filter(exists().where(Project.active_stories))
|
||||
if topicless:
|
||||
query = query.filter_by(topics=None)
|
||||
return query.order_by(Project.name).all()
|
||||
|
||||
def getTopics(self):
|
||||
return self.session().query(Topic).order_by(Topic.sequence).all()
|
||||
|
||||
def getProject(self, key):
|
||||
try:
|
||||
return self.session().query(Project).filter_by(key=key).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getProjectByName(self, name):
|
||||
try:
|
||||
return self.session().query(Project).filter_by(name=name).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getProjectByID(self, id):
|
||||
try:
|
||||
return self.session().query(Project).filter_by(id=id).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getTopic(self, key):
|
||||
try:
|
||||
return self.session().query(Topic).filter_by(key=key).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getTopicByName(self, name):
|
||||
try:
|
||||
return self.session().query(Topic).filter_by(name=name).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getSyncQueryByName(self, name):
|
||||
try:
|
||||
return self.session().query(SyncQuery).filter_by(name=name).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return self.createSyncQuery(name)
|
||||
|
||||
def getStory(self, key):
|
||||
query = self.session().query(Story).filter_by(key=key)
|
||||
try:
|
||||
return query.one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getStoryByID(self, id):
|
||||
try:
|
||||
return self.session().query(Story).filter_by(id=id).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getStories(self, query, active, sort_by='number'):
|
||||
self.database.log.debug("Search query: %s sort: %s" % (query, sort_by))
|
||||
q = self.session().query(Story)
|
||||
if query:
|
||||
q = q.filter(self.search.parse(query))
|
||||
if active:
|
||||
q = q.filter(story_table.c.hidden==False, story_table.c.status=='active')
|
||||
if sort_by == 'updated':
|
||||
q = q.order_by(story_table.c.updated)
|
||||
elif sort_by == 'last-seen':
|
||||
q = q.order_by(story_table.c.last_seen)
|
||||
else:
|
||||
q = q.order_by(story_table.c.id)
|
||||
self.database.log.debug("Search SQL: %s" % q)
|
||||
try:
|
||||
return q.all()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return []
|
||||
|
||||
def getTagByID(self, id):
|
||||
try:
|
||||
return self.session().query(Tag).filter_by(id=id).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getTask(self, key):
|
||||
try:
|
||||
return self.session().query(Task).filter_by(key=key).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getTaskByID(self, id):
|
||||
try:
|
||||
return self.session().query(Task).filter_by(id=id).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getComment(self, key):
|
||||
try:
|
||||
return self.session().query(Comment).filter_by(key=key).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getCommentByID(self, id):
|
||||
try:
|
||||
return self.session().query(Comment).filter_by(id=id).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getHeld(self):
|
||||
return self.session().query(Story).filter_by(held=True).all()
|
||||
|
||||
def getOutdated(self):
|
||||
return self.session().query(Story).filter_by(outdated=True).all()
|
||||
|
||||
def getPendingStories(self):
|
||||
return self.session().query(Story).filter_by(pending=True).all()
|
||||
|
||||
def getPendingTasks(self):
|
||||
return self.session().query(Task).filter_by(pending=True).all()
|
||||
|
||||
def getUsers(self):
|
||||
return self.session().query(User).all()
|
||||
|
||||
def getUser(self, key):
|
||||
try:
|
||||
return self.session().query(User).filter_by(key=key).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getUserByID(self, id):
|
||||
try:
|
||||
return self.session().query(User).filter_by(id=id).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getSystem(self):
|
||||
try:
|
||||
return self.session().query(System).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def getEvent(self, key):
|
||||
try:
|
||||
return self.session().query(Event).filter_by(key=key).one()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def createProject(self, *args, **kw):
|
||||
o = Project(*args, **kw)
|
||||
self.session().add(o)
|
||||
self.session().flush()
|
||||
return o
|
||||
|
||||
def createStory(self, *args, **kw):
|
||||
s = Story(*args, **kw)
|
||||
self.session().add(s)
|
||||
self.session().flush()
|
||||
return s
|
||||
|
||||
def createUser(self, *args, **kw):
|
||||
a = User(*args, **kw)
|
||||
self.session().add(a)
|
||||
self.session().flush()
|
||||
return a
|
||||
|
||||
def createSyncQuery(self, *args, **kw):
|
||||
o = SyncQuery(*args, **kw)
|
||||
self.session().add(o)
|
||||
self.session().flush()
|
||||
return o
|
||||
|
||||
def createTopic(self, *args, **kw):
|
||||
o = Topic(*args, **kw)
|
||||
self.session().add(o)
|
||||
self.session().flush()
|
||||
return o
|
||||
|
||||
def createTag(self, *args, **kw):
|
||||
o = Tag(*args, **kw)
|
||||
self.session().add(o)
|
||||
self.session().flush()
|
||||
return o
|
||||
|
||||
def createSystem(self, *args, **kw):
|
||||
o = System(*args, **kw)
|
||||
self.session().add(o)
|
||||
self.session().flush()
|
||||
return o
|
||||
|
@ -135,17 +135,7 @@ def sqlite_drop_columns(table_name, drop_columns):
|
||||
for key in meta.tables[table_name].foreign_keys:
|
||||
# If this is a single column constraint for a dropped column,
|
||||
# don't copy it.
|
||||
if isinstance(key.constraint.columns, sqlalchemy.sql.base.ColumnCollection):
|
||||
# This is needed for SQLAlchemy >= 1.0.4
|
||||
columns = [c.name for c in key.constraint.columns]
|
||||
else:
|
||||
# This is needed for SQLAlchemy <= 0.9.9. This is
|
||||
# backwards compat code just in case someone updates
|
||||
# Gertty without updating SQLAlchemy. This is simple
|
||||
# enough to check and will hopefully avoid leaving the
|
||||
# user's db in an inconsistent state. Remove this after
|
||||
# Gertty 1.2.0.
|
||||
columns = key.constraint.columns
|
||||
columns = [c.name for c in key.constraint.columns]
|
||||
if (len(columns)==1 and columns[0] in drop_columns):
|
||||
continue
|
||||
# Otherwise, recreate the constraint.
|
@ -30,7 +30,7 @@ CURSOR_PAGE_DOWN = urwid.CURSOR_PAGE_DOWN
|
||||
CURSOR_MAX_LEFT = urwid.CURSOR_MAX_LEFT
|
||||
CURSOR_MAX_RIGHT = urwid.CURSOR_MAX_RIGHT
|
||||
ACTIVATE = urwid.ACTIVATE
|
||||
# Global gertty commands:
|
||||
# Global boartty commands:
|
||||
KILL = 'kill'
|
||||
YANK = 'yank'
|
||||
YANK_POP = 'yank pop'
|
||||
@ -38,37 +38,31 @@ PREV_SCREEN = 'previous screen'
|
||||
TOP_SCREEN = 'top screen'
|
||||
HELP = 'help'
|
||||
QUIT = 'quit'
|
||||
CHANGE_SEARCH = 'change search'
|
||||
REFINE_CHANGE_SEARCH = 'refine change search'
|
||||
LIST_HELD = 'list held changes'
|
||||
# Change screen:
|
||||
TOGGLE_REVIEWED = 'toggle reviewed'
|
||||
STORY_SEARCH = 'story search'
|
||||
REFINE_STORY_SEARCH = 'refine story search'
|
||||
LIST_HELD = 'list held stories'
|
||||
NEW_STORY = 'new story'
|
||||
# Story screen:
|
||||
TOGGLE_HIDDEN = 'toggle hidden'
|
||||
TOGGLE_STARRED = 'toggle starred'
|
||||
TOGGLE_HELD = 'toggle held'
|
||||
TOGGLE_MARK = 'toggle process mark'
|
||||
REVIEW = 'review'
|
||||
DIFF = 'diff'
|
||||
LOCAL_CHECKOUT = 'local checkout'
|
||||
LOCAL_CHERRY_PICK = 'local cherry pick'
|
||||
LEAVE_COMMENT = 'leave comment'
|
||||
SEARCH_RESULTS = 'search results'
|
||||
NEXT_CHANGE = 'next change'
|
||||
PREV_CHANGE = 'previous change'
|
||||
NEXT_STORY = 'next story'
|
||||
PREV_STORY = 'previous story'
|
||||
TOGGLE_HIDDEN_COMMENTS = 'toggle hidden comments'
|
||||
ABANDON_CHANGE = 'abandon change'
|
||||
RESTORE_CHANGE = 'restore change'
|
||||
REBASE_CHANGE = 'rebase change'
|
||||
CHERRY_PICK_CHANGE = 'cherry pick change'
|
||||
NEW_TASK = 'new task'
|
||||
DELETE_TASK = 'delete task'
|
||||
REFRESH = 'refresh'
|
||||
EDIT_TOPIC = 'edit topic'
|
||||
EDIT_COMMIT_MESSAGE = 'edit commit message'
|
||||
SUBMIT_CHANGE = 'submit change'
|
||||
EDIT_TITLE = 'edit title'
|
||||
EDIT_DESCRIPTION = 'edit description'
|
||||
SORT_BY_NUMBER = 'sort by number'
|
||||
SORT_BY_UPDATED = 'sort by updated'
|
||||
SORT_BY_LAST_SEEN = 'sort by last seen'
|
||||
SORT_BY_REVERSE = 'reverse the sort'
|
||||
# Project list screen:
|
||||
TOGGLE_LIST_REVIEWED = 'toggle list reviewed'
|
||||
TOGGLE_LIST_ACTIVE = 'toggle list active'
|
||||
TOGGLE_LIST_SUBSCRIBED = 'toggle list subscribed'
|
||||
TOGGLE_SUBSCRIBED = 'toggle subscribed'
|
||||
NEW_PROJECT_TOPIC = 'new project topic'
|
||||
@ -77,13 +71,11 @@ MOVE_PROJECT_TOPIC = 'move to project topic'
|
||||
COPY_PROJECT_TOPIC = 'copy to project topic'
|
||||
REMOVE_PROJECT_TOPIC = 'remove from project topic'
|
||||
RENAME_PROJECT_TOPIC = 'rename project topic'
|
||||
# Diff screens:
|
||||
SELECT_PATCHSETS = 'select patchsets'
|
||||
# Special:
|
||||
FURTHER_INPUT = 'further input'
|
||||
NEXT_SELECTABLE = 'next selectable'
|
||||
PREV_SELECTABLE = 'prev selectable'
|
||||
INTERACTIVE_SEARCH = 'interactive search'
|
||||
# Special:
|
||||
FURTHER_INPUT = 'further input'
|
||||
|
||||
DEFAULT_KEYMAP = {
|
||||
REDRAW_SCREEN: 'ctrl l',
|
||||
@ -104,37 +96,31 @@ DEFAULT_KEYMAP = {
|
||||
TOP_SCREEN: 'meta home',
|
||||
HELP: ['f1', '?'],
|
||||
QUIT: ['ctrl q'],
|
||||
CHANGE_SEARCH: 'ctrl o',
|
||||
REFINE_CHANGE_SEARCH: 'meta o',
|
||||
STORY_SEARCH: 'ctrl o',
|
||||
REFINE_STORY_SEARCH: 'meta o',
|
||||
LIST_HELD: 'f12',
|
||||
NEW_STORY: 'ctrl n',
|
||||
|
||||
TOGGLE_REVIEWED: 'v',
|
||||
TOGGLE_HIDDEN: 'k',
|
||||
TOGGLE_STARRED: '*',
|
||||
TOGGLE_HELD: '!',
|
||||
TOGGLE_MARK: '%',
|
||||
REVIEW: 'r',
|
||||
DIFF: 'd',
|
||||
LOCAL_CHECKOUT: 'c',
|
||||
LOCAL_CHERRY_PICK: 'x',
|
||||
LEAVE_COMMENT: 'r',
|
||||
SEARCH_RESULTS: 'u',
|
||||
NEXT_CHANGE: 'n',
|
||||
PREV_CHANGE: 'p',
|
||||
NEXT_STORY: 'n',
|
||||
PREV_STORY: 'p',
|
||||
TOGGLE_HIDDEN_COMMENTS: 't',
|
||||
ABANDON_CHANGE: 'ctrl a',
|
||||
RESTORE_CHANGE: 'ctrl e',
|
||||
REBASE_CHANGE: 'ctrl b',
|
||||
CHERRY_PICK_CHANGE: 'ctrl x',
|
||||
NEW_TASK: 'N',
|
||||
DELETE_TASK: 'delete',
|
||||
REFRESH: 'ctrl r',
|
||||
EDIT_TOPIC: 'ctrl t',
|
||||
EDIT_COMMIT_MESSAGE: 'ctrl d',
|
||||
SUBMIT_CHANGE: 'ctrl u',
|
||||
EDIT_TITLE: 'ctrl t',
|
||||
EDIT_DESCRIPTION: 'ctrl d',
|
||||
SORT_BY_NUMBER: [['S', 'n']],
|
||||
SORT_BY_UPDATED: [['S', 'u']],
|
||||
SORT_BY_LAST_SEEN: [['S', 's']],
|
||||
SORT_BY_REVERSE: [['S', 'r']],
|
||||
|
||||
TOGGLE_LIST_REVIEWED: 'l',
|
||||
TOGGLE_LIST_ACTIVE: 'l',
|
||||
TOGGLE_LIST_SUBSCRIBED: 'L',
|
||||
TOGGLE_SUBSCRIBED: 's',
|
||||
NEW_PROJECT_TOPIC: [['T', 'n']],
|
||||
@ -144,7 +130,6 @@ DEFAULT_KEYMAP = {
|
||||
REMOVE_PROJECT_TOPIC: [['T', 'D']],
|
||||
RENAME_PROJECT_TOPIC: [['T', 'r']],
|
||||
|
||||
SELECT_PATCHSETS: 'p',
|
||||
NEXT_SELECTABLE: 'tab',
|
||||
PREV_SELECTABLE: 'shift tab',
|
||||
INTERACTIVE_SEARCH: 'ctrl s',
|
@ -14,8 +14,8 @@
|
||||
|
||||
import urwid
|
||||
|
||||
from gertty import keymap
|
||||
from gertty.view import mouse_scroll_decorator
|
||||
from boartty import keymap
|
||||
from boartty.view import mouse_scroll_decorator
|
||||
|
||||
GLOBAL_HELP = (
|
||||
(keymap.HELP,
|
||||
@ -25,11 +25,11 @@ GLOBAL_HELP = (
|
||||
(keymap.TOP_SCREEN,
|
||||
"Back to project list"),
|
||||
(keymap.QUIT,
|
||||
"Quit Gertty"),
|
||||
(keymap.CHANGE_SEARCH,
|
||||
"Search for changes"),
|
||||
"Quit Boardtty"),
|
||||
(keymap.STORY_SEARCH,
|
||||
"Search for stories"),
|
||||
(keymap.LIST_HELD,
|
||||
"List held changes"),
|
||||
"List held stories"),
|
||||
(keymap.KILL,
|
||||
"Kill to end of line (editing)"),
|
||||
(keymap.YANK,
|
||||
@ -192,7 +192,8 @@ class LineEditDialog(ButtonDialog):
|
||||
|
||||
class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
|
||||
signals = ['save', 'cancel']
|
||||
def __init__(self, title, prompt, button, text, ring=None):
|
||||
def __init__(self, app, title, prompt, button, text, ring=None):
|
||||
self.app = app
|
||||
save_button = FixedButton(button)
|
||||
cancel_button = FixedButton('Cancel')
|
||||
urwid.connect_signal(save_button, 'click',
|
||||
@ -212,6 +213,16 @@ class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
|
||||
fill = urwid.Filler(pile, valign='top')
|
||||
super(TextEditDialog, self).__init__(urwid.LineBox(fill, title))
|
||||
|
||||
def keypress(self, size, key):
|
||||
if not self.app.input_buffer:
|
||||
key = super(TextEditDialog, self).keypress(size, key)
|
||||
keys = self.app.input_buffer + [key]
|
||||
commands = self.app.config.keymap.getCommands(keys)
|
||||
if keymap.PREV_SCREEN in commands:
|
||||
self._emit('cancel')
|
||||
return None
|
||||
return key
|
||||
|
||||
class MessageDialog(ButtonDialog):
|
||||
signals = ['close']
|
||||
def __init__(self, title, message):
|
||||
@ -530,3 +541,76 @@ class MyGridFlow(urwid.GridFlow):
|
||||
c.focus_position = i
|
||||
break
|
||||
return p
|
||||
|
||||
class SearchSelectInnerButton(urwid.Button):
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
||||
super(SearchSelectInnerButton, self).__init__(value)
|
||||
|
||||
class SearchSelectDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
|
||||
"""
|
||||
A dialog that allows a user to select one item from a list, and
|
||||
interactively refine the list by searching.
|
||||
"""
|
||||
|
||||
signals = ['save']
|
||||
def __init__(self, app, title, current_key, values):
|
||||
self.app = app
|
||||
|
||||
rows = []
|
||||
self.key = None
|
||||
self.value = None
|
||||
for key, value in values():
|
||||
button = SearchSelectInnerButton(key, value)
|
||||
urwid.connect_signal(button, 'click',
|
||||
lambda b:self.onSelected(b))
|
||||
rows.append(button)
|
||||
|
||||
pile = urwid.Pile(rows)
|
||||
fill = urwid.Filler(pile, valign='top')
|
||||
super(SearchSelectDialog, self).__init__(urwid.LineBox(fill, title))
|
||||
|
||||
def onSelected(self, b):
|
||||
self.key = b.key
|
||||
self.value = b.value
|
||||
self._emit('save')
|
||||
self.app.backScreen()
|
||||
|
||||
class SearchSelectButton(TextButton):
|
||||
"""
|
||||
A button that displays a value; when clicked, a SearchSelectDialog
|
||||
is opened to select a new value.
|
||||
"""
|
||||
signals = ['changed']
|
||||
def __init__(self, app, title, key, value, values):
|
||||
self.app = app
|
||||
self.title = title
|
||||
self.values = values
|
||||
urwid.connect_signal(self, 'click',
|
||||
lambda button:self.onClick())
|
||||
super(SearchSelectButton, self).__init__(u'')
|
||||
self.update(key, value)
|
||||
|
||||
def onClick(self):
|
||||
dialog = SearchSelectDialog(self.app, self.title, self.key, self.values)
|
||||
urwid.connect_signal(dialog, 'save',
|
||||
lambda d:self.onChanged(d))
|
||||
self.app.popup(dialog,
|
||||
relative_width=30, relative_height=75,
|
||||
min_width=30, min_height=20)
|
||||
|
||||
def update(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
||||
if self.value is None:
|
||||
label = u'Select'
|
||||
else:
|
||||
label = self.value
|
||||
self.text.set_text(label)
|
||||
|
||||
def onChanged(self, dialog):
|
||||
if dialog.key is None:
|
||||
return
|
||||
self.update(dialog.key, dialog.value)
|
||||
self._emit('changed')
|
103
boartty/palette.py
Normal file
103
boartty/palette.py
Normal file
@ -0,0 +1,103 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
DEFAULT_PALETTE={
|
||||
'focused': ['default,standout', ''],
|
||||
'header': ['white,bold', 'dark blue'],
|
||||
'error': ['light red', 'dark blue'],
|
||||
'table-header': ['white,bold', ''],
|
||||
'link': ['dark blue', ''],
|
||||
'focused-link': ['light blue', ''],
|
||||
'footer': ['light gray', 'dark gray'],
|
||||
'search-result': ['default,standout', ''],
|
||||
# Story view
|
||||
'story-data': ['dark cyan', ''],
|
||||
'focused-story-data': ['light cyan', ''],
|
||||
'story-header': ['light blue', ''],
|
||||
'task-id': ['dark cyan', ''],
|
||||
'task-title': ['light green', ''],
|
||||
'task-project': ['light blue', ''],
|
||||
'task-status': ['yellow', ''],
|
||||
'task-assignee': ['light cyan', ''],
|
||||
'task-note': ['default', ''],
|
||||
'focused-task-id': ['dark cyan,standout', ''],
|
||||
'focused-task-title': ['light green,standout', ''],
|
||||
'focused-task-project': ['light blue,standout', ''],
|
||||
'focused-task-status': ['yellow,standout', ''],
|
||||
'focused-task-assignee': ['dark cyan,standout', ''],
|
||||
'focused-task-note': ['default', ''],
|
||||
'story-event-name': ['yellow', ''],
|
||||
'story-event-own-name': ['light cyan', ''],
|
||||
'story-event-header': ['brown', ''],
|
||||
'story-event-own-header': ['dark cyan', ''],
|
||||
'story-event-draft': ['dark red', ''],
|
||||
'story-event-button': ['dark magenta', ''],
|
||||
'focused-story-event-button': ['light magenta', ''],
|
||||
# project list
|
||||
'active-project': ['white', ''],
|
||||
'subscribed-project': ['default', ''],
|
||||
'unsubscribed-project': ['dark gray', ''],
|
||||
'marked-project': ['light cyan', ''],
|
||||
'focused-active-project': ['white,standout', ''],
|
||||
'focused-subscribed-project': ['default,standout', ''],
|
||||
'focused-unsubscribed-project': ['dark gray,standout', ''],
|
||||
'focused-marked-project': ['light cyan,standout', ''],
|
||||
# story list
|
||||
'active-story': ['default', ''],
|
||||
'inactive-story': ['dark gray', ''],
|
||||
'focused-active-story': ['default,standout', ''],
|
||||
'focused-inactive-story': ['dark gray,standout', ''],
|
||||
'starred-story': ['light cyan', ''],
|
||||
'focused-starred-story': ['light cyan,standout', ''],
|
||||
'held-story': ['light red', ''],
|
||||
'focused-held-story': ['light red,standout', ''],
|
||||
'marked-story': ['dark cyan', ''],
|
||||
'focused-marked-story': ['dark cyan,standout', ''],
|
||||
}
|
||||
|
||||
# A delta from the default palette
|
||||
LIGHT_PALETTE = {
|
||||
'table-header': ['black,bold', ''],
|
||||
'active-project': ['black', ''],
|
||||
'subscribed-project': ['dark gray', ''],
|
||||
'unsubscribed-project': ['dark gray', ''],
|
||||
'focused-active-project': ['black,standout', ''],
|
||||
'focused-subscribed-project': ['dark gray,standout', ''],
|
||||
'focused-unsubscribed-project': ['dark gray,standout', ''],
|
||||
'story-data': ['dark blue,bold', ''],
|
||||
'focused-story-data': ['dark blue,standout', ''],
|
||||
'story-event-name': ['brown', ''],
|
||||
'story-event-own-name': ['dark blue,bold', ''],
|
||||
'story-event-header': ['black', ''],
|
||||
'story-event-own-header': ['black,bold', ''],
|
||||
'focused-link': ['dark blue,bold', ''],
|
||||
}
|
||||
|
||||
class Palette(object):
|
||||
def __init__(self, config):
|
||||
self.palette = {}
|
||||
self.palette.update(DEFAULT_PALETTE)
|
||||
self.update(config)
|
||||
|
||||
def update(self, config):
|
||||
d = config.copy()
|
||||
if 'name' in d:
|
||||
del d['name']
|
||||
self.palette.update(d)
|
||||
|
||||
def getPalette(self):
|
||||
ret = []
|
||||
for k,v in self.palette.items():
|
||||
ret.append(tuple([k]+v))
|
||||
return ret
|
@ -15,8 +15,8 @@
|
||||
import sqlalchemy.sql.expression
|
||||
from sqlalchemy.sql.expression import and_
|
||||
|
||||
from gertty.search import tokenizer, parser
|
||||
import gertty.db
|
||||
from boartty.search import tokenizer, parser
|
||||
import boartty.db
|
||||
|
||||
|
||||
class SearchSyntaxError(Exception):
|
||||
@ -35,7 +35,7 @@ class SearchCompiler(object):
|
||||
while stack:
|
||||
x = stack.pop()
|
||||
if hasattr(x, 'table'):
|
||||
if (x.table != gertty.db.change_table
|
||||
if (x.table != boartty.db.story_table
|
||||
and hasattr(x.table, 'name')):
|
||||
tables.add(x.table)
|
||||
for child in x.get_children():
|
||||
@ -47,19 +47,19 @@ class SearchCompiler(object):
|
||||
self.parser.username = self.username
|
||||
result = self.parser.parse(data, lexer=self.lexer)
|
||||
tables = self.findTables(result)
|
||||
if gertty.db.project_table in tables:
|
||||
result = and_(gertty.db.change_table.c.project_key == gertty.db.project_table.c.key,
|
||||
if boartty.db.project_table in tables:
|
||||
result = and_(boartty.db.story_table.c.project_key == boartty.db.project_table.c.key,
|
||||
result)
|
||||
tables.remove(gertty.db.project_table)
|
||||
if gertty.db.account_table in tables:
|
||||
result = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key,
|
||||
tables.remove(boartty.db.project_table)
|
||||
if boartty.db.user_table in tables:
|
||||
result = and_(boartty.db.story_table.c.user_key == boartty.db.user_table.c.key,
|
||||
result)
|
||||
tables.remove(gertty.db.account_table)
|
||||
if gertty.db.file_table in tables:
|
||||
result = and_(gertty.db.file_table.c.revision_key == gertty.db.revision_table.c.key,
|
||||
gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key,
|
||||
result)
|
||||
tables.remove(gertty.db.file_table)
|
||||
tables.remove(boartty.db.user_table)
|
||||
#if boartty.db.file_table in tables:
|
||||
# result = and_(boartty.db.file_table.c.revision_key == boartty.db.revision_table.c.key,
|
||||
# boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key,
|
||||
# result)
|
||||
# tables.remove(boartty.db.file_table)
|
||||
if tables:
|
||||
raise Exception("Unknown table in search: %s" % tables)
|
||||
return result
|
@ -18,9 +18,9 @@ import re
|
||||
import ply.yacc as yacc
|
||||
from sqlalchemy.sql.expression import and_, or_, not_, select, func
|
||||
|
||||
import gertty.db
|
||||
import gertty.search
|
||||
from gertty.search.tokenizer import tokens # NOQA
|
||||
import boartty.db
|
||||
import boartty.search
|
||||
from boartty.search.tokenizer import tokens # NOQA
|
||||
|
||||
def age_to_delta(delta, unit):
|
||||
if unit in ['seconds', 'second', 'sec', 's']:
|
||||
@ -68,7 +68,7 @@ def SearchParser():
|
||||
elif p[2].lower() == 'or':
|
||||
p[0] = or_(p[1], p[3])
|
||||
else:
|
||||
raise gertty.search.SearchSyntaxError("Boolean %s not recognized" % p[2])
|
||||
raise boartty.search.SearchSyntaxError("Boolean %s not recognized" % p[2])
|
||||
|
||||
def p_negative_expr(p):
|
||||
'''negative_expr : NOT expression
|
||||
@ -78,7 +78,7 @@ def SearchParser():
|
||||
def p_term(p):
|
||||
'''term : age_term
|
||||
| recentlyseen_term
|
||||
| change_term
|
||||
| story_term
|
||||
| owner_term
|
||||
| reviewer_term
|
||||
| commit_term
|
||||
@ -111,102 +111,107 @@ def SearchParser():
|
||||
delta = p[2]
|
||||
unit = p[3]
|
||||
delta = age_to_delta(delta, unit)
|
||||
p[0] = gertty.db.change_table.c.updated < (now-datetime.timedelta(seconds=delta))
|
||||
p[0] = boartty.db.story_table.c.updated < (now-datetime.timedelta(seconds=delta))
|
||||
|
||||
def p_recentlyseen_term(p):
|
||||
'''recentlyseen_term : OP_RECENTLYSEEN NUMBER string'''
|
||||
# A gertty extension
|
||||
# A boartty extension
|
||||
now = datetime.datetime.utcnow()
|
||||
delta = p[2]
|
||||
unit = p[3]
|
||||
delta = age_to_delta(delta, unit)
|
||||
s = select([func.datetime(func.max(gertty.db.change_table.c.last_seen), '-%s seconds' % delta)],
|
||||
s = select([func.datetime(func.max(boartty.db.story_table.c.last_seen), '-%s seconds' % delta)],
|
||||
correlate=False)
|
||||
p[0] = gertty.db.change_table.c.last_seen >= s
|
||||
p[0] = boartty.db.story_table.c.last_seen >= s
|
||||
|
||||
def p_change_term(p):
|
||||
'''change_term : OP_CHANGE CHANGE_ID
|
||||
| OP_CHANGE NUMBER'''
|
||||
def p_story_term(p):
|
||||
'''story_term : OP_STORY STORY_ID
|
||||
| OP_STORY NUMBER'''
|
||||
if type(p[2]) == int:
|
||||
p[0] = gertty.db.change_table.c.number == p[2]
|
||||
p[0] = boartty.db.story_table.c.number == p[2]
|
||||
else:
|
||||
p[0] = gertty.db.change_table.c.change_id == p[2]
|
||||
p[0] = boartty.db.story_table.c.story_id == p[2]
|
||||
|
||||
def p_owner_term(p):
|
||||
'''owner_term : OP_OWNER string'''
|
||||
if p[2] == 'self':
|
||||
username = p.parser.username
|
||||
p[0] = gertty.db.account_table.c.username == username
|
||||
p[0] = boartty.db.user_table.c.username == username
|
||||
else:
|
||||
p[0] = or_(gertty.db.account_table.c.username == p[2],
|
||||
gertty.db.account_table.c.email == p[2],
|
||||
gertty.db.account_table.c.name == p[2])
|
||||
p[0] = or_(boartty.db.user_table.c.username == p[2],
|
||||
boartty.db.user_table.c.email == p[2],
|
||||
boartty.db.user_table.c.name == p[2])
|
||||
|
||||
def p_reviewer_term(p):
|
||||
'''reviewer_term : OP_REVIEWER string
|
||||
| OP_REVIEWER NUMBER'''
|
||||
filters = []
|
||||
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
|
||||
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
|
||||
try:
|
||||
number = int(p[2])
|
||||
except:
|
||||
number = None
|
||||
if number is not None:
|
||||
filters.append(gertty.db.account_table.c.id == number)
|
||||
filters.append(boartty.db.user_table.c.id == number)
|
||||
elif p[2] == 'self':
|
||||
username = p.parser.username
|
||||
filters.append(gertty.db.account_table.c.username == username)
|
||||
filters.append(boartty.db.user_table.c.username == username)
|
||||
else:
|
||||
filters.append(or_(gertty.db.account_table.c.username == p[2],
|
||||
gertty.db.account_table.c.email == p[2],
|
||||
gertty.db.account_table.c.name == p[2]))
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
filters.append(or_(boartty.db.user_table.c.username == p[2],
|
||||
boartty.db.user_table.c.email == p[2],
|
||||
boartty.db.user_table.c.name == p[2]))
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
|
||||
def p_commit_term(p):
|
||||
'''commit_term : OP_COMMIT string'''
|
||||
filters = []
|
||||
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.revision_table.c.commit == p[2])
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.revision_table.c.commit == p[2])
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
|
||||
def p_project_term(p):
|
||||
'''project_term : OP_PROJECT string'''
|
||||
if p[2].startswith('^'):
|
||||
p[0] = func.matches(p[2], gertty.db.project_table.c.name)
|
||||
p[0] = func.matches(p[2], boartty.db.project_table.c.name)
|
||||
else:
|
||||
p[0] = gertty.db.project_table.c.name == p[2]
|
||||
p[0] = boartty.db.project_table.c.name == p[2]
|
||||
|
||||
def p_projects_term(p):
|
||||
'''projects_term : OP_PROJECTS string'''
|
||||
p[0] = gertty.db.project_table.c.name.like('%s%%' % p[2])
|
||||
p[0] = boartty.db.project_table.c.name.like('%s%%' % p[2])
|
||||
|
||||
def p_project_key_term(p):
|
||||
'''project_key_term : OP_PROJECT_KEY NUMBER'''
|
||||
p[0] = gertty.db.change_table.c.project_key == p[2]
|
||||
#p[0] = boartty.db.story_table.c.project_key == p[2]
|
||||
filters = []
|
||||
filters.append(boartty.db.task_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.task_table.c.project_key == p[2])
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
|
||||
def p_branch_term(p):
|
||||
'''branch_term : OP_BRANCH string'''
|
||||
if p[2].startswith('^'):
|
||||
p[0] = func.matches(p[2], gertty.db.change_table.c.branch)
|
||||
p[0] = func.matches(p[2], boartty.db.story_table.c.branch)
|
||||
else:
|
||||
p[0] = gertty.db.change_table.c.branch == p[2]
|
||||
p[0] = boartty.db.story_table.c.branch == p[2]
|
||||
|
||||
def p_topic_term(p):
|
||||
'''topic_term : OP_TOPIC string'''
|
||||
if p[2].startswith('^'):
|
||||
p[0] = func.matches(p[2], gertty.db.change_table.c.topic)
|
||||
p[0] = func.matches(p[2], boartty.db.story_table.c.topic)
|
||||
else:
|
||||
p[0] = gertty.db.change_table.c.topic == p[2]
|
||||
p[0] = boartty.db.story_table.c.topic == p[2]
|
||||
|
||||
def p_ref_term(p):
|
||||
'''ref_term : OP_REF string'''
|
||||
if p[2].startswith('^'):
|
||||
p[0] = func.matches(p[2], 'refs/heads/'+gertty.db.change_table.c.branch)
|
||||
p[0] = func.matches(p[2], 'refs/heads/'+boartty.db.story_table.c.branch)
|
||||
else:
|
||||
p[0] = gertty.db.change_table.c.branch == p[2][len('refs/heads/'):]
|
||||
p[0] = boartty.db.story_table.c.branch == p[2][len('refs/heads/'):]
|
||||
|
||||
label_re = re.compile(r'(?P<label>[a-zA-Z0-9_-]+([a-zA-Z]|((?<![-+])[0-9])))'
|
||||
r'(?P<operator>[<>]?=?)(?P<value>[-+]?[0-9]+)'
|
||||
@ -221,60 +226,60 @@ def SearchParser():
|
||||
user = args.group('user')
|
||||
|
||||
filters = []
|
||||
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.approval_table.c.category == label)
|
||||
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.approval_table.c.category == label)
|
||||
if op == '=':
|
||||
filters.append(gertty.db.approval_table.c.value == value)
|
||||
filters.append(boartty.db.approval_table.c.value == value)
|
||||
elif op == '>=':
|
||||
filters.append(gertty.db.approval_table.c.value >= value)
|
||||
filters.append(boartty.db.approval_table.c.value >= value)
|
||||
elif op == '<=':
|
||||
filters.append(gertty.db.approval_table.c.value <= value)
|
||||
filters.append(boartty.db.approval_table.c.value <= value)
|
||||
if user is not None:
|
||||
filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
|
||||
filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
|
||||
if user == 'self':
|
||||
filters.append(gertty.db.account_table.c.username == p.parser.username)
|
||||
filters.append(boartty.db.user_table.c.username == p.parser.username)
|
||||
else:
|
||||
filters.append(
|
||||
or_(gertty.db.account_table.c.username == user,
|
||||
gertty.db.account_table.c.email == user,
|
||||
gertty.db.account_table.c.name == user))
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
or_(boartty.db.user_table.c.username == user,
|
||||
boartty.db.user_table.c.email == user,
|
||||
boartty.db.user_table.c.name == user))
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
|
||||
def p_message_term(p):
|
||||
'''message_term : OP_MESSAGE string'''
|
||||
filters = []
|
||||
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.revision_table.c.message.like('%%%s%%' % p[2]))
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.revision_table.c.message.like('%%%s%%' % p[2]))
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
|
||||
def p_comment_term(p):
|
||||
'''comment_term : OP_COMMENT string'''
|
||||
filters = []
|
||||
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.revision_table.c.message == p[2])
|
||||
revision_select = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.revision_table.c.message == p[2])
|
||||
revision_select = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
filters = []
|
||||
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.comment_table.c.revision_key == gertty.db.revision_table.c.key)
|
||||
filters.append(gertty.db.comment_table.c.message == p[2])
|
||||
comment_select = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = or_(gertty.db.change_table.c.key.in_(comment_select),
|
||||
gertty.db.change_table.c.key.in_(revision_select))
|
||||
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.comment_table.c.revision_key == boartty.db.revision_table.c.key)
|
||||
filters.append(boartty.db.comment_table.c.message == p[2])
|
||||
comment_select = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = or_(boartty.db.story_table.c.key.in_(comment_select),
|
||||
boartty.db.story_table.c.key.in_(revision_select))
|
||||
|
||||
def p_has_term(p):
|
||||
'''has_term : OP_HAS string'''
|
||||
#TODO: implement star
|
||||
if p[2] == 'draft':
|
||||
filters = []
|
||||
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.message_table.c.revision_key == gertty.db.revision_table.c.key)
|
||||
filters.append(gertty.db.message_table.c.draft == True)
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.message_table.c.revision_key == boartty.db.revision_table.c.key)
|
||||
filters.append(boartty.db.message_table.c.draft == True)
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
else:
|
||||
raise gertty.search.SearchSyntaxError('Syntax error: has:%s is not supported' % p[2])
|
||||
raise boartty.search.SearchSyntaxError('Syntax error: has:%s is not supported' % p[2])
|
||||
|
||||
def p_is_term(p):
|
||||
'''is_term : OP_IS string'''
|
||||
@ -282,58 +287,58 @@ def SearchParser():
|
||||
username = p.parser.username
|
||||
if p[2] == 'reviewed':
|
||||
filters = []
|
||||
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.approval_table.c.value != 0)
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.approval_table.c.value != 0)
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
elif p[2] == 'open':
|
||||
p[0] = gertty.db.change_table.c.status.notin_(['MERGED', 'ABANDONED'])
|
||||
p[0] = boartty.db.story_table.c.status.notin_(['MERGED', 'ABANDONED'])
|
||||
elif p[2] == 'closed':
|
||||
p[0] = gertty.db.change_table.c.status.in_(['MERGED', 'ABANDONED'])
|
||||
p[0] = boartty.db.story_table.c.status.in_(['MERGED', 'ABANDONED'])
|
||||
elif p[2] == 'submitted':
|
||||
p[0] = gertty.db.change_table.c.status == 'SUBMITTED'
|
||||
p[0] = boartty.db.story_table.c.status == 'SUBMITTED'
|
||||
elif p[2] == 'merged':
|
||||
p[0] = gertty.db.change_table.c.status == 'MERGED'
|
||||
p[0] = boartty.db.story_table.c.status == 'MERGED'
|
||||
elif p[2] == 'abandoned':
|
||||
p[0] = gertty.db.change_table.c.status == 'ABANDONED'
|
||||
p[0] = boartty.db.story_table.c.status == 'ABANDONED'
|
||||
elif p[2] == 'owner':
|
||||
p[0] = gertty.db.account_table.c.username == username
|
||||
p[0] = boartty.db.user_table.c.username == username
|
||||
elif p[2] == 'starred':
|
||||
p[0] = gertty.db.change_table.c.starred == True
|
||||
p[0] = boartty.db.story_table.c.starred == True
|
||||
elif p[2] == 'held':
|
||||
# A gertty extension
|
||||
p[0] = gertty.db.change_table.c.held == True
|
||||
# A boartty extension
|
||||
p[0] = boartty.db.story_table.c.held == True
|
||||
elif p[2] == 'reviewer':
|
||||
filters = []
|
||||
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
|
||||
filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
|
||||
filters.append(gertty.db.account_table.c.username == username)
|
||||
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = gertty.db.change_table.c.key.in_(s)
|
||||
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
|
||||
filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
|
||||
filters.append(boartty.db.user_table.c.username == username)
|
||||
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
|
||||
p[0] = boartty.db.story_table.c.key.in_(s)
|
||||
elif p[2] == 'watched':
|
||||
p[0] = gertty.db.project_table.c.subscribed == True
|
||||
p[0] = boartty.db.project_table.c.subscribed == True
|
||||
else:
|
||||
raise gertty.search.SearchSyntaxError('Syntax error: is:%s is not supported' % p[2])
|
||||
raise boartty.search.SearchSyntaxError('Syntax error: is:%s is not supported' % p[2])
|
||||
|
||||
def p_file_term(p):
|
||||
'''file_term : OP_FILE string'''
|
||||
if p[2].startswith('^'):
|
||||
p[0] = and_(or_(func.matches(p[2], gertty.db.file_table.c.path),
|
||||
func.matches(p[2], gertty.db.file_table.c.old_path)),
|
||||
gertty.db.file_table.c.status is not None)
|
||||
p[0] = and_(or_(func.matches(p[2], boartty.db.file_table.c.path),
|
||||
func.matches(p[2], boartty.db.file_table.c.old_path)),
|
||||
boartty.db.file_table.c.status is not None)
|
||||
else:
|
||||
p[0] = and_(or_(gertty.db.file_table.c.path == p[2],
|
||||
gertty.db.file_table.c.old_path == p[2]),
|
||||
gertty.db.file_table.c.status is not None)
|
||||
p[0] = and_(or_(boartty.db.file_table.c.path == p[2],
|
||||
boartty.db.file_table.c.old_path == p[2]),
|
||||
boartty.db.file_table.c.status is not None)
|
||||
|
||||
def p_status_term(p):
|
||||
'''status_term : OP_STATUS string'''
|
||||
if p[2] == 'open':
|
||||
p[0] = gertty.db.change_table.c.status.notin_(['MERGED', 'ABANDONED'])
|
||||
p[0] = boartty.db.story_table.c.status.notin_(['MERGED', 'ABANDONED'])
|
||||
elif p[2] == 'closed':
|
||||
p[0] = gertty.db.change_table.c.status.in_(['MERGED', 'ABANDONED'])
|
||||
p[0] = boartty.db.story_table.c.status.in_(['MERGED', 'ABANDONED'])
|
||||
else:
|
||||
p[0] = gertty.db.change_table.c.status == p[2].upper()
|
||||
p[0] = boartty.db.story_table.c.status == p[2]
|
||||
|
||||
def p_limit_term(p):
|
||||
'''limit_term : OP_LIMIT NUMBER'''
|
||||
@ -341,7 +346,7 @@ def SearchParser():
|
||||
# applied to the query operation and so can not be returned as
|
||||
# part of the production here. The information would need to
|
||||
# be returned out-of-band. In the mean time, since limits are
|
||||
# not as important in gertty, make this a no-op for now so
|
||||
# not as important in boartty, make this a no-op for now so
|
||||
# that it does not produce a syntax error.
|
||||
p[0] = (True == True)
|
||||
|
||||
@ -351,9 +356,9 @@ def SearchParser():
|
||||
|
||||
def p_error(p):
|
||||
if p:
|
||||
raise gertty.search.SearchSyntaxError('Syntax error at "%s" in search string "%s" (col %s)' % (
|
||||
raise boartty.search.SearchSyntaxError('Syntax error at "%s" in search string "%s" (col %s)' % (
|
||||
p.lexer.lexdata[p.lexpos:], p.lexer.lexdata, p.lexpos))
|
||||
else:
|
||||
raise gertty.search.SearchSyntaxError('Syntax error: EOF in search string')
|
||||
raise boartty.search.SearchSyntaxError('Syntax error: EOF in search string')
|
||||
|
||||
return yacc.yacc(debug=0, write_tables=0)
|
@ -17,8 +17,8 @@ import six
|
||||
|
||||
operators = {
|
||||
'age': 'OP_AGE',
|
||||
'recentlyseen': 'OP_RECENTLYSEEN', # Gertty extension
|
||||
'change': 'OP_CHANGE',
|
||||
'recentlyseen': 'OP_RECENTLYSEEN', # Boardtty extension
|
||||
'story': 'OP_STORY',
|
||||
'owner': 'OP_OWNER',
|
||||
#'OP_OWNERIN', # needs local group membership
|
||||
'reviewer': 'OP_REVIEWER',
|
||||
@ -26,7 +26,7 @@ operators = {
|
||||
'commit': 'OP_COMMIT',
|
||||
'project': 'OP_PROJECT',
|
||||
'projects': 'OP_PROJECTS',
|
||||
'_project_key': 'OP_PROJECT_KEY', # internal gertty use only
|
||||
'_project_key': 'OP_PROJECT_KEY', # internal boartty use only
|
||||
'branch': 'OP_BRANCH',
|
||||
'topic': 'OP_TOPIC',
|
||||
'ref': 'OP_REF',
|
||||
@ -56,7 +56,7 @@ tokens = [
|
||||
'LPAREN',
|
||||
'RPAREN',
|
||||
'NUMBER',
|
||||
'CHANGE_ID',
|
||||
'STORY_ID',
|
||||
'SSTRING',
|
||||
'DSTRING',
|
||||
'USTRING',
|
||||
@ -75,7 +75,7 @@ def SearchTokenizer():
|
||||
t.type = operators.get(t.value[:-1], 'OP')
|
||||
return t
|
||||
|
||||
def t_CHANGE_ID(t):
|
||||
def t_STORY_ID(t):
|
||||
r'I[a-fA-F0-9]{7,40}'
|
||||
return t
|
||||
|
1137
boartty/sync.py
Normal file
1137
boartty/sync.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,4 +14,4 @@
|
||||
|
||||
import pbr.version
|
||||
|
||||
version_info = pbr.version.VersionInfo('gertty')
|
||||
version_info = pbr.version.VersionInfo('boartty')
|
@ -16,11 +16,13 @@
|
||||
import logging
|
||||
import urwid
|
||||
|
||||
from gertty import keymap
|
||||
from gertty import mywid
|
||||
from gertty import sync
|
||||
from gertty.view import change_list as view_change_list
|
||||
from gertty.view import mouse_scroll_decorator
|
||||
from boartty import keymap
|
||||
from boartty import mywid
|
||||
from boartty import sync
|
||||
from boartty.view import story_list as view_story_list
|
||||
from boartty.view import mouse_scroll_decorator
|
||||
|
||||
ACTIVE_COL_WIDTH = 7
|
||||
|
||||
class TopicSelectDialog(urwid.WidgetWrap):
|
||||
signals = ['ok', 'cancel']
|
||||
@ -59,7 +61,7 @@ class TopicSelectDialog(urwid.WidgetWrap):
|
||||
|
||||
class ProjectRow(urwid.Button):
|
||||
project_focus_map = {None: 'focused',
|
||||
'unreviewed-project': 'focused-unreviewed-project',
|
||||
'active-project': 'focused-active-project',
|
||||
'subscribed-project': 'focused-subscribed-project',
|
||||
'unsubscribed-project': 'focused-unsubscribed-project',
|
||||
'marked-project': 'focused-marked-project',
|
||||
@ -94,12 +96,10 @@ class ProjectRow(urwid.Button):
|
||||
self.name = mywid.SearchableText('')
|
||||
self._setName(project.name, self.indent)
|
||||
self.name.set_wrap_mode('clip')
|
||||
self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
|
||||
self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
|
||||
self.active_stories = urwid.Text(u'', align=urwid.RIGHT)
|
||||
col = urwid.Columns([
|
||||
self.name,
|
||||
('fixed', 11, self.unreviewed_changes),
|
||||
('fixed', 5, self.open_changes),
|
||||
('fixed', ACTIVE_COL_WIDTH, self.active_stories),
|
||||
])
|
||||
self.row_style = urwid.AttrMap(col, '')
|
||||
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
|
||||
@ -111,18 +111,14 @@ class ProjectRow(urwid.Button):
|
||||
def update(self, project):
|
||||
cache = self.app.project_cache.get(project)
|
||||
if project.subscribed:
|
||||
if cache['unreviewed_changes'] > 0:
|
||||
style = 'unreviewed-project'
|
||||
else:
|
||||
style = 'subscribed-project'
|
||||
style = 'subscribed-project'
|
||||
else:
|
||||
style = 'unsubscribed-project'
|
||||
self._style = style
|
||||
if self.mark:
|
||||
style = 'marked-project'
|
||||
self.row_style.set_attr_map({None: style})
|
||||
self.unreviewed_changes.set_text('%i ' % cache['unreviewed_changes'])
|
||||
self.open_changes.set_text('%i ' % cache['open_changes'])
|
||||
self.active_stories.set_text('%i ' % cache['active_stories'])
|
||||
|
||||
def toggleMark(self):
|
||||
self.mark = not self.mark
|
||||
@ -160,12 +156,10 @@ class TopicRow(urwid.Button):
|
||||
self.name = urwid.Text('')
|
||||
self._setName(topic.name)
|
||||
self.name.set_wrap_mode('clip')
|
||||
self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
|
||||
self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
|
||||
self.active_stories = urwid.Text(u'', align=urwid.RIGHT)
|
||||
col = urwid.Columns([
|
||||
self.name,
|
||||
('fixed', 11, self.unreviewed_changes),
|
||||
('fixed', 5, self.open_changes),
|
||||
('fixed', ACTIVE_COL_WIDTH, self.active_stories),
|
||||
])
|
||||
self.row_style = urwid.AttrMap(col, '')
|
||||
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
|
||||
@ -173,16 +167,12 @@ class TopicRow(urwid.Button):
|
||||
self.row_style.set_attr_map({None: self._style})
|
||||
self.update(topic)
|
||||
|
||||
def update(self, topic, unreviewed_changes=None, open_changes=None):
|
||||
def update(self, topic, active_stories=None):
|
||||
self._setName(topic.name)
|
||||
if unreviewed_changes is None:
|
||||
self.unreviewed_changes.set_text('')
|
||||
if active_stories is None:
|
||||
self.active_stories.set_text('')
|
||||
else:
|
||||
self.unreviewed_changes.set_text('%i ' % unreviewed_changes)
|
||||
if open_changes is None:
|
||||
self.open_changes.set_text('')
|
||||
else:
|
||||
self.open_changes.set_text('%i ' % open_changes)
|
||||
self.active_stories.set_text('%i ' % active_stories)
|
||||
|
||||
def toggleMark(self):
|
||||
self.mark = not self.mark
|
||||
@ -196,8 +186,7 @@ class TopicRow(urwid.Button):
|
||||
class ProjectListHeader(urwid.WidgetWrap):
|
||||
def __init__(self):
|
||||
cols = [urwid.Text(u' Project'),
|
||||
(11, urwid.Text(u'Unreviewed')),
|
||||
(5, urwid.Text(u'Open'))]
|
||||
(ACTIVE_COL_WIDTH, urwid.Text(u'Active'))]
|
||||
super(ProjectListHeader, self).__init__(urwid.Columns(cols))
|
||||
|
||||
@mouse_scroll_decorator.ScrollByWheel
|
||||
@ -206,8 +195,8 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
return [
|
||||
(keymap.TOGGLE_LIST_SUBSCRIBED,
|
||||
"Toggle whether only subscribed projects or all projects are listed"),
|
||||
(keymap.TOGGLE_LIST_REVIEWED,
|
||||
"Toggle listing of projects with unreviewed changes"),
|
||||
(keymap.TOGGLE_LIST_ACTIVE,
|
||||
"Toggle listing of projects with active changes"),
|
||||
(keymap.TOGGLE_SUBSCRIBED,
|
||||
"Toggle the subscription flag for the selected project"),
|
||||
(keymap.REFRESH,
|
||||
@ -237,10 +226,10 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
|
||||
def __init__(self, app):
|
||||
super(ProjectListView, self).__init__(urwid.Pile([]))
|
||||
self.log = logging.getLogger('gertty.view.project_list')
|
||||
self.log = logging.getLogger('boartty.view.project_list')
|
||||
self.searchInit()
|
||||
self.app = app
|
||||
self.unreviewed = True
|
||||
self.active = True
|
||||
self.subscribed = True
|
||||
self.project_rows = {}
|
||||
self.topic_rows = {}
|
||||
@ -257,10 +246,10 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
def interested(self, event):
|
||||
if not (isinstance(event, sync.ProjectAddedEvent)
|
||||
or
|
||||
isinstance(event, sync.ChangeAddedEvent)
|
||||
isinstance(event, sync.StoryAddedEvent)
|
||||
or
|
||||
(isinstance(event, sync.ChangeUpdatedEvent) and
|
||||
(event.status_changed or event.review_flag_changed))):
|
||||
(isinstance(event, sync.StoryUpdatedEvent) and
|
||||
event.status_changed)):
|
||||
self.log.debug("Ignoring refresh project list due to event %s" % (event,))
|
||||
return False
|
||||
self.log.debug("Refreshing project list due to event %s" % (event,))
|
||||
@ -326,8 +315,8 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
if self.subscribed:
|
||||
self.title = u'Subscribed projects'
|
||||
self.short_title = self.title[:]
|
||||
if self.unreviewed:
|
||||
self.title += u' with unreviewed changes'
|
||||
if self.active:
|
||||
self.title += u' with active stories'
|
||||
else:
|
||||
self.title = u'All projects'
|
||||
self.short_title = self.title[:]
|
||||
@ -335,28 +324,24 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
with self.app.db.getSession() as session:
|
||||
i = 0
|
||||
for project in session.getProjects(topicless=True,
|
||||
subscribed=self.subscribed, unreviewed=self.unreviewed):
|
||||
subscribed=self.subscribed, active=self.active):
|
||||
#self.log.debug("project: %s" % project.name)
|
||||
i = self._projectRow(i, project, None)
|
||||
for topic in session.getTopics():
|
||||
#self.log.debug("topic: %s" % topic.name)
|
||||
i = self._topicRow(i, topic)
|
||||
topic_unreviewed = 0
|
||||
topic_open = 0
|
||||
topic_active = 0
|
||||
for project in topic.projects:
|
||||
#self.log.debug(" project: %s" % project.name)
|
||||
cache = self.app.project_cache.get(project)
|
||||
topic_unreviewed += cache['unreviewed_changes']
|
||||
topic_open += cache['open_changes']
|
||||
topic_active += cache['active_stories']
|
||||
if self.subscribed:
|
||||
if not project.subscribed:
|
||||
continue
|
||||
if self.unreviewed and not cache['unreviewed_changes']:
|
||||
continue
|
||||
if topic.key in self.open_topics:
|
||||
i = self._projectRow(i, project, topic)
|
||||
topic_row = self.topic_rows.get(topic.key)
|
||||
topic_row.update(topic, topic_unreviewed, topic_open)
|
||||
topic_row.update(topic, topic_active)
|
||||
while i < len(self.listbox.body):
|
||||
current_row = self.listbox.body[i]
|
||||
self._deleteRow(current_row)
|
||||
@ -370,10 +355,11 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
|
||||
def onSelect(self, button, data):
|
||||
project_key, project_name = data
|
||||
self.app.changeScreen(view_change_list.ChangeListView(
|
||||
self.app,
|
||||
"_project_key:%s %s" % (project_key, self.app.config.project_change_list_query),
|
||||
project_name, project_key=project_key, unreviewed=True))
|
||||
self.app.changeScreen(view_story_list.StoryListView(
|
||||
self.app,
|
||||
"_project_key:%s %s" % (project_key,
|
||||
self.app.config.project_story_list_query),
|
||||
project_name, project_key=project_key, active=True))
|
||||
|
||||
def onSelectTopic(self, button, data):
|
||||
topic_key = data[0]
|
||||
@ -553,8 +539,8 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
return key
|
||||
|
||||
def handleCommands(self, commands):
|
||||
if keymap.TOGGLE_LIST_REVIEWED in commands:
|
||||
self.unreviewed = not self.unreviewed
|
||||
if keymap.TOGGLE_LIST_ACTIVE in commands:
|
||||
self.active = not self.active
|
||||
self.refresh()
|
||||
return True
|
||||
if keymap.TOGGLE_LIST_SUBSCRIBED in commands:
|
837
boartty/view/story.py
Normal file
837
boartty/view/story.py
Normal file
@ -0,0 +1,837 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2016 Red Hat, Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import logging
|
||||
try:
|
||||
import ordereddict
|
||||
except:
|
||||
pass
|
||||
import textwrap
|
||||
|
||||
from six.moves.urllib import parse as urlparse
|
||||
import urwid
|
||||
|
||||
from boartty import keymap
|
||||
from boartty import mywid
|
||||
from boartty import sync
|
||||
from boartty.view import mouse_scroll_decorator
|
||||
import boartty.view
|
||||
|
||||
try:
|
||||
OrderedDict = collections.OrderedDict
|
||||
except AttributeError:
|
||||
OrderedDict = ordereddict.OrderedDict
|
||||
|
||||
class NewStoryDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin):
|
||||
signals = ['save', 'cancel']
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
save_button = mywid.FixedButton(u'Save')
|
||||
cancel_button = mywid.FixedButton(u'Cancel')
|
||||
urwid.connect_signal(save_button, 'click',
|
||||
lambda button:self._emit('save'))
|
||||
urwid.connect_signal(cancel_button, 'click',
|
||||
lambda button:self._emit('cancel'))
|
||||
|
||||
rows = []
|
||||
buttons = [('pack', save_button),
|
||||
('pack', cancel_button)]
|
||||
buttons = urwid.Columns(buttons, dividechars=2)
|
||||
|
||||
self.project_button = ProjectButton(self.app)
|
||||
self.title_field = mywid.MyEdit(u'', edit_text=u'', ring=app.ring)
|
||||
self.description_field = mywid.MyEdit(u'', edit_text='',
|
||||
multiline=True, ring=app.ring)
|
||||
|
||||
for (label, w) in [
|
||||
(u'Title:', self.title_field),
|
||||
(u'Description:', self.description_field),
|
||||
(u'Project:', ('pack', self.project_button)),
|
||||
]:
|
||||
row = urwid.Columns([(12, urwid.Text(label)), w])
|
||||
rows.append(row)
|
||||
|
||||
rows.append(urwid.Divider())
|
||||
rows.append(buttons)
|
||||
pile = urwid.Pile(rows)
|
||||
fill = urwid.Filler(pile, valign='top')
|
||||
super(NewStoryDialog, self).__init__(urwid.LineBox(fill, 'New Story'))
|
||||
|
||||
class ProjectButton(mywid.SearchSelectButton):
|
||||
def __init__(self, app, key=None, value=None):
|
||||
self.app = app
|
||||
super(ProjectButton, self).__init__(app, 'Select Project', key, value,
|
||||
self.getValues)
|
||||
|
||||
def getValues(self):
|
||||
with self.app.db.getSession() as session:
|
||||
projects = session.getProjects()
|
||||
for project in projects:
|
||||
yield (project.key, project.name)
|
||||
|
||||
class StatusButton(mywid.SearchSelectButton):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
super(StatusButton, self).__init__(app, 'Select Status', 'todo', 'todo',
|
||||
self.getValues)
|
||||
|
||||
def getValues(self):
|
||||
return [('todo', 'todo'),
|
||||
('merged', 'merged'),
|
||||
('invalid', 'invalid'),
|
||||
('review', 'review'),
|
||||
('inprogress', 'inprogress'),
|
||||
]
|
||||
|
||||
class AssigneeButton(mywid.SearchSelectButton):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
super(AssigneeButton, self).__init__(app, 'Select Assignee', None, None,
|
||||
self.getValues)
|
||||
|
||||
def getValues(self):
|
||||
with self.app.db.getSession() as session:
|
||||
users = session.getUsers()
|
||||
for user in users:
|
||||
yield (user.key, user.name)
|
||||
|
||||
class NewTaskDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin):
|
||||
signals = ['save', 'cancel']
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
save_button = mywid.FixedButton(u'Save')
|
||||
cancel_button = mywid.FixedButton(u'Cancel')
|
||||
urwid.connect_signal(save_button, 'click',
|
||||
lambda button:self._emit('save'))
|
||||
urwid.connect_signal(cancel_button, 'click',
|
||||
lambda button:self._emit('cancel'))
|
||||
|
||||
rows = []
|
||||
buttons = [('pack', save_button),
|
||||
('pack', cancel_button)]
|
||||
buttons = urwid.Columns(buttons, dividechars=2)
|
||||
|
||||
self.project_button = ProjectButton(self.app)
|
||||
self.status_button = StatusButton(self.app)
|
||||
self.assignee_button = AssigneeButton(self.app)
|
||||
self.title_field = mywid.MyEdit(u'', edit_text=u'', ring=app.ring)
|
||||
|
||||
for (label, w) in [
|
||||
(u'Project:', ('pack', self.project_button)),
|
||||
(u'Title:', self.title_field),
|
||||
(u'Status:', ('pack', self.status_button)),
|
||||
(u'Assignee:', ('pack', self.assignee_button)),
|
||||
]:
|
||||
row = urwid.Columns([(12, urwid.Text(label)), w])
|
||||
rows.append(row)
|
||||
|
||||
rows.append(urwid.Divider())
|
||||
rows.append(buttons)
|
||||
pile = urwid.Pile(rows)
|
||||
fill = urwid.Filler(pile, valign='top')
|
||||
super(NewTaskDialog, self).__init__(urwid.LineBox(fill, 'New Task'))
|
||||
|
||||
class TaskRow(urwid.WidgetWrap):
|
||||
task_focus_map = {
|
||||
'task-title': 'focused-task-title',
|
||||
'task-project': 'focused-task-project',
|
||||
'task-status': 'focused-task-status',
|
||||
'task-assignee': 'focused-task-assignee',
|
||||
'task-note': 'focused-task-note',
|
||||
}
|
||||
|
||||
def keypress(self, size, key):
|
||||
if not self.app.input_buffer:
|
||||
key = super(TaskRow, self).keypress(size, key)
|
||||
keys = self.app.input_buffer + [key]
|
||||
commands = self.app.config.keymap.getCommands(keys)
|
||||
if keymap.DELETE_TASK in commands:
|
||||
self.delete()
|
||||
return None
|
||||
return key
|
||||
|
||||
def __init__(self, app, story_view, task):
|
||||
super(TaskRow, self).__init__(urwid.Pile([]))
|
||||
self.app = app
|
||||
self.story_view = story_view
|
||||
self.task_key = task.key
|
||||
self._note = u''
|
||||
self.taskid = mywid.TextButton(self._note)
|
||||
urwid.connect_signal(self.taskid, 'click',
|
||||
lambda b:self.editNote(b))
|
||||
self.project = ProjectButton(self.app)
|
||||
urwid.connect_signal(self.project, 'changed',
|
||||
lambda b:self.updateProject(b))
|
||||
self.status = StatusButton(self.app)
|
||||
urwid.connect_signal(self.status, 'changed',
|
||||
lambda b:self.updateStatus(b))
|
||||
self._title = u''
|
||||
self.title = mywid.TextButton(self._title)
|
||||
urwid.connect_signal(self.title, 'click',
|
||||
lambda b:self.editTitle(b))
|
||||
self.assignee = AssigneeButton(self.app)
|
||||
urwid.connect_signal(self.assignee, 'changed',
|
||||
lambda b:self.updateAssignee(b))
|
||||
self.description = urwid.Text(u'')
|
||||
self.columns = urwid.Columns([], dividechars=1)
|
||||
|
||||
for (widget, attr, packing) in [
|
||||
(self.taskid, 'task-id', ('given', 4, False)),
|
||||
(self.project, 'task-project', ('weight', 1, False)),
|
||||
(self.title, 'task-title', ('weight', 2, False)),
|
||||
(self.status, 'task-status', ('weight', 1, False)),
|
||||
(self.assignee, 'task-assignee', ('weight', 1, False)),
|
||||
]:
|
||||
w = urwid.AttrMap(urwid.Padding(widget, width='pack'), attr,
|
||||
focus_map={'focused': 'focused-'+attr})
|
||||
self.columns.contents.append((w, packing))
|
||||
self.pile = urwid.Pile([self.columns])
|
||||
self.note = urwid.Text(u'')
|
||||
self.note_visible = False
|
||||
self.note_columns = urwid.Columns([], dividechars=1)
|
||||
self.note_columns.contents.append((urwid.Text(u''), ('given', 1, False)))
|
||||
self.note_columns.contents.append((self.note, ('weight', 1, False)))
|
||||
self._w = urwid.AttrMap(self.pile, None)#, focus_map=self.task_focus_map)
|
||||
self.refresh(task)
|
||||
|
||||
def setNote(self, note):
|
||||
if note:
|
||||
self._note = note
|
||||
self.note.set_text(('task-note', self._note))
|
||||
if not self.note_visible:
|
||||
self.pile.contents.append((self.note_columns, ('weight', 1)))
|
||||
self.note_visible = True
|
||||
elif self.note_visible:
|
||||
for x in self.pile.contents[:]:
|
||||
if x[0] is self.note_columns:
|
||||
self.pile.contents.remove(x)
|
||||
self.note_visible = False
|
||||
|
||||
def refresh(self, task):
|
||||
self.taskid.text.set_text(str(task.id))
|
||||
self.project.update(task.project.key, task.project.name)
|
||||
self.status.update(task.status, task.status)
|
||||
self._title = task.title
|
||||
self.title.text.set_text(self._title)
|
||||
self.setNote(task.link)
|
||||
if task.assignee:
|
||||
self.assignee.update(task.assignee.key, task.assignee.name)
|
||||
else:
|
||||
self.assignee.update(None, 'Unassigned')
|
||||
|
||||
def updateProject(self, project_button):
|
||||
with self.app.db.getSession() as session:
|
||||
task = session.getTask(self.task_key)
|
||||
project = session.getProject(project_button.key)
|
||||
task.project = project
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
|
||||
def updateStatus(self, status_button):
|
||||
with self.app.db.getSession() as session:
|
||||
task = session.getTask(self.task_key)
|
||||
task.status = status_button.key
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
|
||||
def updateAssignee(self, assignee_button):
|
||||
with self.app.db.getSession() as session:
|
||||
task = session.getTask(self.task_key)
|
||||
user = session.getUser(assignee_button.key)
|
||||
task.assignee = user
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
|
||||
def editTitle(self, title_button):
|
||||
dialog = mywid.LineEditDialog(self.app, 'Edit Task Title', '',
|
||||
'Title: ', self._title,
|
||||
ring=self.app.ring)
|
||||
urwid.connect_signal(dialog, 'save',
|
||||
lambda button: self.updateTitle(dialog, True))
|
||||
urwid.connect_signal(dialog, 'cancel',
|
||||
lambda button: self.updateTitle(dialog, False))
|
||||
self.app.popup(dialog)
|
||||
|
||||
def updateTitle(self, dialog, save):
|
||||
if save:
|
||||
with self.app.db.getSession() as session:
|
||||
task = session.getTask(self.task_key)
|
||||
task.title = dialog.entry.edit_text
|
||||
self._title = task.title
|
||||
self.title.text.set_text(self._title)
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
|
||||
def editNote(self, note_button):
|
||||
dialog = mywid.LineEditDialog(self.app, 'Edit Task Note', '',
|
||||
'Note: ', self._note,
|
||||
ring=self.app.ring)
|
||||
urwid.connect_signal(dialog, 'save',
|
||||
lambda button: self.updateNote(dialog, True))
|
||||
urwid.connect_signal(dialog, 'cancel',
|
||||
lambda button: self.updateNote(dialog, False))
|
||||
self.app.popup(dialog)
|
||||
|
||||
def updateNote(self, dialog, save):
|
||||
if save:
|
||||
with self.app.db.getSession() as session:
|
||||
task = session.getTask(self.task_key)
|
||||
task.link = dialog.entry.edit_text or None
|
||||
self.setNote(task.link)
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
|
||||
def delete(self):
|
||||
dialog = mywid.YesNoDialog(u'Delete Task',
|
||||
u'Are you sure you want to delete this task?')
|
||||
urwid.connect_signal(dialog, 'no', lambda d: self.app.backScreen())
|
||||
urwid.connect_signal(dialog, 'yes', self.finishDelete)
|
||||
self.app.popup(dialog)
|
||||
|
||||
def finishDelete(self, dialog):
|
||||
with self.app.db.getSession() as session:
|
||||
task = session.getTask(self.task_key)
|
||||
task.pending_delete = True
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
self.story_view.refresh()
|
||||
|
||||
class StoryButton(urwid.Button):
|
||||
button_left = urwid.Text(u' ')
|
||||
button_right = urwid.Text(u' ')
|
||||
|
||||
def __init__(self, story_view, story_key, text):
|
||||
super(StoryButton, self).__init__('')
|
||||
self.set_label(text)
|
||||
self.story_view = story_view
|
||||
self.story_key = story_key
|
||||
urwid.connect_signal(self, 'click',
|
||||
lambda button: self.openStory())
|
||||
|
||||
def set_label(self, text):
|
||||
super(StoryButton, self).set_label(text)
|
||||
|
||||
def openStory(self):
|
||||
try:
|
||||
self.story_view.app.changeScreen(StoryView(self.story_view.app, self.story_key))
|
||||
except boartty.view.DisplayError as e:
|
||||
self.story_view.app.error(e.message)
|
||||
|
||||
class StoryEventBox(mywid.HyperText):
|
||||
def __init__(self, story_view, event):
|
||||
super(StoryEventBox, self).__init__(u'')
|
||||
self.story_view = story_view
|
||||
self.app = story_view.app
|
||||
self.refresh(event)
|
||||
|
||||
def formatReply(self):
|
||||
text = self.comment_text
|
||||
pgraphs = []
|
||||
pgraph_accumulator = []
|
||||
wrap = True
|
||||
for line in text.split('\n'):
|
||||
if line.startswith('> '):
|
||||
wrap = False
|
||||
line = '> ' + line
|
||||
if not line:
|
||||
if pgraph_accumulator:
|
||||
pgraphs.append((wrap, '\n'.join(pgraph_accumulator)))
|
||||
pgraph_accumulator = []
|
||||
wrap = True
|
||||
continue
|
||||
pgraph_accumulator.append(line)
|
||||
if pgraph_accumulator:
|
||||
pgraphs.append((wrap, '\n'.join(pgraph_accumulator)))
|
||||
pgraph_accumulator = []
|
||||
wrap = True
|
||||
wrapper = textwrap.TextWrapper(initial_indent='> ',
|
||||
subsequent_indent='> ')
|
||||
wrapped_pgraphs = []
|
||||
for wrap, pgraph in pgraphs:
|
||||
if wrap:
|
||||
wrapped_pgraphs.append('\n'.join(wrapper.wrap(pgraph)))
|
||||
else:
|
||||
wrapped_pgraphs.append(pgraph)
|
||||
return '\n>\n'.join(wrapped_pgraphs)
|
||||
|
||||
def reply(self):
|
||||
reply_text = self.formatReply()
|
||||
if reply_text:
|
||||
reply_text = self.event_creator + ' wrote:\n\n' + reply_text + '\n'
|
||||
self.story_view.leaveComment(reply_text=reply_text)
|
||||
|
||||
def refresh(self, event):
|
||||
self.event_id = event.id
|
||||
self.event_creator = event.creator_name
|
||||
description = event.description
|
||||
if event.comment:
|
||||
comment = event.comment.content
|
||||
else:
|
||||
comment = ''
|
||||
self.comment_text = comment
|
||||
created = self.app.time(event.created)
|
||||
lines = comment.split('\n')
|
||||
if event.creator.id == self.app.user_id:
|
||||
name_style = 'story-event-own-name'
|
||||
header_style = 'story-event-own-header'
|
||||
creator_string = event.creator.name
|
||||
else:
|
||||
name_style = 'story-event-name'
|
||||
header_style = 'story-event-header'
|
||||
if event.creator.email:
|
||||
creator_string = "%s <%s>" % (
|
||||
event.creator.name,
|
||||
event.creator.email)
|
||||
else:
|
||||
creator_string = event.creator.name
|
||||
|
||||
text = [(name_style, creator_string),
|
||||
(header_style, ': '+description),
|
||||
(header_style,
|
||||
created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))]
|
||||
if event.comment and event.comment.draft and not event.comment.pending:
|
||||
text.append(('story-event-draft', ' (draft)'))
|
||||
elif event.comment:
|
||||
link = mywid.Link('< Reply >',
|
||||
'story-event-button',
|
||||
'focused-story-event-button')
|
||||
urwid.connect_signal(link, 'selected',
|
||||
lambda link:self.reply())
|
||||
text.append(' ')
|
||||
text.append(link)
|
||||
text.append('\n')
|
||||
if lines and lines[-1]:
|
||||
lines.append('')
|
||||
comment_text = ['\n'.join(lines)]
|
||||
for commentlink in self.app.config.commentlinks:
|
||||
comment_text = commentlink.run(self.app, comment_text)
|
||||
info = event.info or ''
|
||||
if info:
|
||||
info = [info + '\n']
|
||||
else:
|
||||
info = []
|
||||
self.set_text(text+comment_text+info)
|
||||
|
||||
class DescriptionBox(mywid.HyperText):
|
||||
def __init__(self, app, description):
|
||||
self.app = app
|
||||
super(DescriptionBox, self).__init__(description)
|
||||
|
||||
def set_text(self, text):
|
||||
text = [text]
|
||||
for commentlink in self.app.config.commentlinks:
|
||||
text = commentlink.run(self.app, text)
|
||||
super(DescriptionBox, self).set_text(text)
|
||||
|
||||
@mouse_scroll_decorator.ScrollByWheel
|
||||
class StoryView(urwid.WidgetWrap):
|
||||
def getCommands(self):
|
||||
return [
|
||||
(keymap.TOGGLE_HIDDEN,
|
||||
"Toggle the hidden flag for the current story"),
|
||||
(keymap.NEXT_STORY,
|
||||
"Go to the next story in the list"),
|
||||
(keymap.PREV_STORY,
|
||||
"Go to the previous story in the list"),
|
||||
(keymap.LEAVE_COMMENT,
|
||||
"Leave a comment on the story"),
|
||||
(keymap.NEW_TASK,
|
||||
"Add a new task to the current story"),
|
||||
(keymap.TOGGLE_HELD,
|
||||
"Toggle the held flag for the current story"),
|
||||
(keymap.TOGGLE_HIDDEN_COMMENTS,
|
||||
"Toggle display of hidden comments"),
|
||||
(keymap.SEARCH_RESULTS,
|
||||
"Back to the list of stories"),
|
||||
(keymap.TOGGLE_STARRED,
|
||||
"Toggle the starred flag for the current story"),
|
||||
(keymap.EDIT_DESCRIPTION,
|
||||
"Edit the commit message of this story"),
|
||||
(keymap.REFRESH,
|
||||
"Refresh this story"),
|
||||
(keymap.EDIT_TITLE,
|
||||
"Edit the title of this story"),
|
||||
]
|
||||
|
||||
def help(self):
|
||||
key = self.app.config.keymap.formatKeys
|
||||
commands = self.getCommands()
|
||||
ret = [(c[0], key(c[0]), c[1]) for c in commands]
|
||||
for k in self.app.config.reviewkeys.values():
|
||||
action = ', '.join(['{category}:{value}'.format(**a) for a in k['approvals']])
|
||||
ret.append(('', keymap.formatKey(k['key']), action))
|
||||
return ret
|
||||
|
||||
def __init__(self, app, story_key):
|
||||
super(StoryView, self).__init__(urwid.Pile([]))
|
||||
self.log = logging.getLogger('boartty.view.story')
|
||||
self.app = app
|
||||
self.story_key = story_key
|
||||
self.task_rows = {}
|
||||
self.event_rows = {}
|
||||
self.hide_events = True
|
||||
self.marked_seen = False
|
||||
self.title_label = urwid.Text(u'', wrap='clip')
|
||||
self.creator_label = mywid.TextButton(u'', on_press=self.searchCreator)
|
||||
self.tags_label = urwid.Text(u'', wrap='clip')
|
||||
self.created_label = urwid.Text(u'', wrap='clip')
|
||||
self.updated_label = urwid.Text(u'', wrap='clip')
|
||||
self.status_label = urwid.Text(u'', wrap='clip')
|
||||
self.permalink_label = mywid.TextButton(u'', on_press=self.openPermalink)
|
||||
story_info = []
|
||||
story_info_map={'story-data': 'focused-story-data'}
|
||||
for l, v in [("Title", self.title_label),
|
||||
("Creator", urwid.Padding(urwid.AttrMap(self.creator_label, None,
|
||||
focus_map=story_info_map),
|
||||
width='pack')),
|
||||
("Tags", urwid.Padding(urwid.AttrMap(self.tags_label, None,
|
||||
focus_map=story_info_map),
|
||||
width='pack')),
|
||||
("Created", self.created_label),
|
||||
("Updated", self.updated_label),
|
||||
("Status", self.status_label),
|
||||
("Permalink", urwid.Padding(urwid.AttrMap(self.permalink_label, None,
|
||||
focus_map=story_info_map),
|
||||
width='pack')),
|
||||
]:
|
||||
row = urwid.Columns([(12, urwid.Text(('story-header', l), wrap='clip')), v])
|
||||
story_info.append(row)
|
||||
story_info = urwid.Pile(story_info)
|
||||
self.description = DescriptionBox(app, u'')
|
||||
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
||||
self._w.contents.append((self.app.header, ('pack', 1)))
|
||||
self._w.contents.append((urwid.Divider(), ('pack', 1)))
|
||||
self._w.contents.append((self.listbox, ('weight', 1)))
|
||||
self._w.set_focus(2)
|
||||
|
||||
self.listbox.body.append(story_info)
|
||||
self.listbox.body.append(urwid.Divider())
|
||||
self.listbox_tasks_start = len(self.listbox.body)
|
||||
self.listbox.body.append(urwid.Divider())
|
||||
self.listbox.body.append(self.description)
|
||||
self.listbox.body.append(urwid.Divider())
|
||||
|
||||
self.refresh()
|
||||
self.listbox.set_focus(3)
|
||||
|
||||
def interested(self, event):
|
||||
if not ((isinstance(event, sync.StoryAddedEvent) and
|
||||
self.story_key == event.story_key)
|
||||
or
|
||||
(isinstance(event, sync.StoryUpdatedEvent) and
|
||||
self.story_key == event.story_key)):
|
||||
self.log.debug("Ignoring refresh story due to event %s" % (event,))
|
||||
return False
|
||||
self.log.debug("Refreshing story due to event %s" % (event,))
|
||||
return True
|
||||
|
||||
def refresh(self):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
# When we first open the story, update its last_seen
|
||||
# time.
|
||||
if not self.marked_seen:
|
||||
story.last_seen = datetime.datetime.utcnow()
|
||||
self.marked_seen = True
|
||||
hidden = starred = held = ''
|
||||
# storyboard
|
||||
#if story.hidden:
|
||||
# hidden = ' (hidden)'
|
||||
#if story.starred:
|
||||
# starred = '* '
|
||||
#if story.held:
|
||||
# held = ' (held)'
|
||||
self.title = '%sStory %s%s%s' % (starred, story.id,
|
||||
hidden, held)
|
||||
self.app.status.update(title=self.title)
|
||||
self.story_rest_id = story.id
|
||||
self.story_title = story.title
|
||||
if story.creator:
|
||||
self.creator_email = story.creator.email
|
||||
else:
|
||||
self.creator_email = None
|
||||
|
||||
if self.creator_email:
|
||||
creator_string = '%s <%s>' % (story.creator_name,
|
||||
story.creator.email)
|
||||
else:
|
||||
creator_string = story.creator_name
|
||||
self.creator_label.text.set_text(('story-data', creator_string))
|
||||
tags_string = ' '.join([t.name for t in story.tags])
|
||||
self.tags_label.set_text(('story-data', tags_string))
|
||||
self.title_label.set_text(('story-data', story.title))
|
||||
self.created_label.set_text(('story-data', str(self.app.time(story.created))))
|
||||
self.updated_label.set_text(('story-data', str(self.app.time(story.updated))))
|
||||
self.status_label.set_text(('story-data', story.status))
|
||||
self.permalink_url = '' # storyboard urlparse.urljoin(self.app.config.url, str(story.number))
|
||||
self.permalink_label.text.set_text(('story-data', self.permalink_url))
|
||||
self.description.set_text(story.description)
|
||||
|
||||
# The listbox has both tasks and events in it, so
|
||||
# keep track of the index separate from the loop.
|
||||
listbox_index = self.listbox_tasks_start
|
||||
# The set of task keys currently displayed
|
||||
unseen_keys = set(self.task_rows.keys())
|
||||
for task in story.tasks:
|
||||
if task.pending_delete:
|
||||
continue
|
||||
row = self.task_rows.get(task.key)
|
||||
if not row:
|
||||
row = TaskRow(self.app, self, task)
|
||||
self.listbox.body.insert(listbox_index, row)
|
||||
self.task_rows[task.key] = row
|
||||
else:
|
||||
unseen_keys.remove(task.key)
|
||||
row.refresh(task)
|
||||
listbox_index += 1
|
||||
# Remove any events that should not be displayed
|
||||
for key in unseen_keys:
|
||||
row = self.task_rows.get(key)
|
||||
self.listbox.body.remove(row)
|
||||
del self.task_rows[key]
|
||||
listbox_index -= 1
|
||||
|
||||
listbox_index = len(self.listbox.body)
|
||||
# Get the set of events that should be displayed
|
||||
display_events = []
|
||||
for event in story.events:
|
||||
if event.comment or (not self.hide_events):
|
||||
display_events.append(event)
|
||||
# The set of event keys currently displayed
|
||||
unseen_keys = set(self.event_rows.keys())
|
||||
# Make sure all of the events that should be displayed are
|
||||
for event in display_events:
|
||||
row = self.event_rows.get(event.key)
|
||||
if not row:
|
||||
box = StoryEventBox(self, event)
|
||||
row = urwid.Padding(box, width=80)
|
||||
self.listbox.body.insert(listbox_index, row)
|
||||
self.event_rows[event.key] = row
|
||||
else:
|
||||
unseen_keys.remove(event.key)
|
||||
row.original_widget.refresh(event)
|
||||
listbox_index += 1
|
||||
# Remove any events that should not be displayed
|
||||
for key in unseen_keys:
|
||||
row = self.event_rows.get(key)
|
||||
self.listbox.body.remove(row)
|
||||
del self.event_rows[key]
|
||||
listbox_index -= 1
|
||||
|
||||
def toggleHidden(self):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
story.hidden = not story.hidden
|
||||
self.app.project_cache.clear(story.project)
|
||||
|
||||
def toggleStarred(self):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
story.starred = not story.starred
|
||||
story.pending_starred = True
|
||||
self.app.sync.submitTask(
|
||||
sync.StoryStarredTask(self.story_key, sync.HIGH_PRIORITY))
|
||||
|
||||
def toggleHeld(self):
|
||||
return self.app.toggleHeldStory(self.story_key)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if not self.app.input_buffer:
|
||||
key = super(StoryView, self).keypress(size, key)
|
||||
keys = self.app.input_buffer + [key]
|
||||
commands = self.app.config.keymap.getCommands(keys)
|
||||
if keymap.TOGGLE_HIDDEN in commands:
|
||||
self.toggleHidden()
|
||||
self.refresh()
|
||||
return None
|
||||
if keymap.TOGGLE_STARRED in commands:
|
||||
self.toggleStarred()
|
||||
self.refresh()
|
||||
return None
|
||||
if keymap.TOGGLE_HELD in commands:
|
||||
self.toggleHeld()
|
||||
self.refresh()
|
||||
return None
|
||||
if keymap.LEAVE_COMMENT in commands:
|
||||
self.leaveComment()
|
||||
return None
|
||||
if keymap.NEW_TASK in commands:
|
||||
self.newTask()
|
||||
return None
|
||||
if keymap.SEARCH_RESULTS in commands:
|
||||
widget = self.app.findStoryList()
|
||||
if widget:
|
||||
self.app.backScreen(widget)
|
||||
return None
|
||||
if ((keymap.NEXT_STORY in commands) or
|
||||
(keymap.PREV_STORY in commands)):
|
||||
widget = self.app.findStoryList()
|
||||
if widget:
|
||||
if keymap.NEXT_STORY in commands:
|
||||
new_story_key = widget.getNextStoryKey(self.story_key)
|
||||
else:
|
||||
new_story_key = widget.getPrevStoryKey(self.story_key)
|
||||
if new_story_key:
|
||||
try:
|
||||
view = StoryView(self.app, new_story_key)
|
||||
self.app.changeScreen(view, push=False)
|
||||
except boartty.view.DisplayError as e:
|
||||
self.app.error(e.message)
|
||||
return None
|
||||
if keymap.TOGGLE_HIDDEN_COMMENTS in commands:
|
||||
self.hide_events = not self.hide_events
|
||||
self.refresh()
|
||||
return None
|
||||
if keymap.EDIT_DESCRIPTION in commands:
|
||||
self.editDescription()
|
||||
return None
|
||||
if keymap.REFRESH in commands:
|
||||
self.app.sync.submitTask(
|
||||
sync.SyncStoryTask(self.story_rest_id, priority=sync.HIGH_PRIORITY))
|
||||
self.app.status.update()
|
||||
return None
|
||||
if keymap.EDIT_TITLE in commands:
|
||||
self.editTitle()
|
||||
return None
|
||||
return key
|
||||
|
||||
def editDescription(self):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
dialog = mywid.TextEditDialog(self.app, u'Edit Description',
|
||||
u'Description:',
|
||||
u'Save', story.description)
|
||||
urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
|
||||
urwid.connect_signal(dialog, 'save', lambda button:
|
||||
self.doEditDescription(dialog))
|
||||
self.app.popup(dialog,
|
||||
relative_width=50, relative_height=75,
|
||||
min_width=60, min_height=20)
|
||||
|
||||
def doEditDescription(self, dialog):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
story.description = dialog.entry.edit_text
|
||||
story.pending = True
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateStoryTask(self.story_key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
self.refresh()
|
||||
|
||||
def leaveComment(self, parent=None, reply_text=None):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
event = story.getDraftCommentEvent(parent)
|
||||
if event:
|
||||
text = event.comment.content
|
||||
else:
|
||||
text = u''
|
||||
if reply_text:
|
||||
text += reply_text
|
||||
dialog = mywid.TextEditDialog(self.app, u'Leave Comment', u'Comment:',
|
||||
u'Save', text)
|
||||
urwid.connect_signal(dialog, 'cancel', lambda button:
|
||||
self.cancelLeaveComment(dialog, parent))
|
||||
urwid.connect_signal(dialog, 'save', lambda button:
|
||||
self.saveLeaveComment(dialog, parent))
|
||||
self.app.popup(dialog,
|
||||
relative_width=50, relative_height=75,
|
||||
min_width=60, min_height=20)
|
||||
|
||||
def cancelLeaveComment(self, dialog, parent):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
user = session.getUser(self.app.user_id)
|
||||
story.setDraftComment(user, parent, dialog.entry.edit_text)
|
||||
self.app.backScreen()
|
||||
self.refresh()
|
||||
|
||||
def saveLeaveComment(self, dialog, parent):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
user = session.getUser(self.app.user_id)
|
||||
event = story.setDraftComment(user, parent, dialog.entry.edit_text)
|
||||
event.comment.pending = True
|
||||
self.app.sync.submitTask(
|
||||
sync.AddCommentTask(event.key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
self.refresh()
|
||||
|
||||
def newTask(self):
|
||||
dialog = NewTaskDialog(self.app)
|
||||
urwid.connect_signal(dialog, 'save',
|
||||
lambda button: self.saveNewTask(dialog))
|
||||
urwid.connect_signal(dialog, 'cancel',
|
||||
lambda button: self.cancelNewTask(dialog))
|
||||
self.app.popup(dialog,
|
||||
relative_width=50, relative_height=25,
|
||||
min_width=60, min_height=8)
|
||||
|
||||
def cancelNewTask(self, dialog):
|
||||
self.app.backScreen()
|
||||
|
||||
def saveNewTask(self, dialog):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
task = story.addTask()
|
||||
task.project = session.getProjectByID(dialog.project_button.key)
|
||||
task.title = dialog.title_field.edit_text
|
||||
task.status = dialog.status_button.key
|
||||
if dialog.assignee_button.key:
|
||||
task.assignee = session.getUserByID(dialog.assignee_button.key)
|
||||
task.pending = True
|
||||
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
self.refresh()
|
||||
|
||||
def editTitle(self):
|
||||
dialog = mywid.LineEditDialog(self.app, 'Edit Story Title', '',
|
||||
'Title: ', self.story_title,
|
||||
ring=self.app.ring)
|
||||
urwid.connect_signal(dialog, 'save',
|
||||
lambda button: self.updateTitle(dialog, True))
|
||||
urwid.connect_signal(dialog, 'cancel',
|
||||
lambda button: self.updateTitle(dialog, False))
|
||||
self.app.popup(dialog)
|
||||
|
||||
def updateTitle(self, dialog, save):
|
||||
if save:
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(self.story_key)
|
||||
story.title = dialog.entry.edit_text
|
||||
self.app.sync.submitTask(
|
||||
sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY))
|
||||
self.app.backScreen()
|
||||
self.refresh()
|
||||
|
||||
def openPermalink(self, widget):
|
||||
self.app.openURL(self.permalink_url)
|
||||
|
||||
def searchCreator(self, widget):
|
||||
if self.creator_email:
|
||||
self.app.doSearch("status:open creator:%s" % (self.creator_email,))
|
||||
|
||||
def searchTags(self, widget):
|
||||
#storyboard
|
||||
if self.topic:
|
||||
self.app.doSearch("status:open topic:%s" % (self.topic,))
|
513
boartty/view/story_list.py
Normal file
513
boartty/view/story_list.py
Normal file
@ -0,0 +1,513 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import six
|
||||
import urwid
|
||||
|
||||
from boartty import keymap
|
||||
from boartty import mywid
|
||||
from boartty import sync
|
||||
from boartty.view import story as view_story
|
||||
from boartty.view import mouse_scroll_decorator
|
||||
import boartty.view
|
||||
|
||||
|
||||
class ColumnInfo(object):
|
||||
def __init__(self, name, packing, value):
|
||||
self.name = name
|
||||
self.packing = packing
|
||||
self.value = value
|
||||
self.options = (packing, value)
|
||||
if packing == 'given':
|
||||
self.spacing = value + 1
|
||||
else:
|
||||
self.spacing = (value * 8) + 1
|
||||
|
||||
|
||||
COLUMNS = [
|
||||
ColumnInfo('ID', 'given', 8),
|
||||
ColumnInfo('Title', 'weight', 4),
|
||||
ColumnInfo('Status', 'weight', 1),
|
||||
ColumnInfo('Creator', 'weight', 1),
|
||||
ColumnInfo('Updated', 'given', 10),
|
||||
]
|
||||
|
||||
|
||||
class StoryListColumns(object):
|
||||
def updateColumns(self):
|
||||
del self.columns.contents[:]
|
||||
cols = self.columns.contents
|
||||
options = self.columns.options
|
||||
|
||||
for colinfo in COLUMNS:
|
||||
if colinfo.name in self.enabled_columns:
|
||||
attr = colinfo.name.lower().replace(' ', '_')
|
||||
cols.append((getattr(self, attr),
|
||||
options(*colinfo.options)))
|
||||
|
||||
|
||||
class StoryRow(urwid.Button, StoryListColumns):
|
||||
story_focus_map = {None: 'focused',
|
||||
'active-story': 'focused-active-story',
|
||||
'inactive-story': 'focused-inactive-story',
|
||||
'starred-story': 'focused-starred-story',
|
||||
'held-story': 'focused-held-story',
|
||||
'marked-story': 'focused-marked-story',
|
||||
}
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def __init__(self, app, story,
|
||||
enabled_columns, callback=None):
|
||||
super(StoryRow, self).__init__('', on_press=callback, user_data=story.key)
|
||||
self.app = app
|
||||
self.story_key = story.key
|
||||
self.enabled_columns = enabled_columns
|
||||
self.title = mywid.SearchableText(u'', wrap='clip')
|
||||
self.id = mywid.SearchableText(u'')
|
||||
self.updated = mywid.SearchableText(u'')
|
||||
self.status = mywid.SearchableText(u'')
|
||||
self.creator = mywid.SearchableText(u'', wrap='clip')
|
||||
self.mark = False
|
||||
self.columns = urwid.Columns([], dividechars=1)
|
||||
self.row_style = urwid.AttrMap(self.columns, '')
|
||||
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.story_focus_map)
|
||||
self.update(story)
|
||||
|
||||
def search(self, search, attribute):
|
||||
if self.title.search(search, attribute):
|
||||
return True
|
||||
if self.id.search(search, attribute):
|
||||
return True
|
||||
if self.creator.search(search, attribute):
|
||||
return True
|
||||
if self.status.search(search, attribute):
|
||||
return True
|
||||
if self.updated.search(search, attribute):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self, story):
|
||||
if story.status != 'active' or story.hidden:
|
||||
style = 'inactive-story'
|
||||
else:
|
||||
style = 'active-story'
|
||||
title = story.title
|
||||
flag = ' '
|
||||
#if story.starred:
|
||||
# flag = '*'
|
||||
# style = 'starred-story'
|
||||
#if story.held:
|
||||
# flag = '!'
|
||||
# style = 'held-story'
|
||||
if self.mark:
|
||||
flag = '%'
|
||||
style = 'marked-story'
|
||||
title = flag + title
|
||||
self.row_style.set_attr_map({None: style})
|
||||
self.title.set_text(title)
|
||||
self.id.set_text(str(story.id))
|
||||
self.creator.set_text(story.creator_name)
|
||||
self.status.set_text(story.status)
|
||||
today = self.app.time(datetime.datetime.utcnow()).date()
|
||||
updated_time = self.app.time(story.updated)
|
||||
if updated_time:
|
||||
if today == updated_time.date():
|
||||
self.updated.set_text(updated_time.strftime("%I:%M %p").upper())
|
||||
else:
|
||||
self.updated.set_text(updated_time.strftime("%Y-%m-%d"))
|
||||
else:
|
||||
self.updated.set_text('Unknown')
|
||||
self.updateColumns()
|
||||
|
||||
class StoryListHeader(urwid.WidgetWrap, StoryListColumns):
|
||||
def __init__(self, enabled_columns):
|
||||
self.enabled_columns = enabled_columns
|
||||
self.title = urwid.Text(u'Title', wrap='clip')
|
||||
self.id = urwid.Text(u'ID')
|
||||
self.updated = urwid.Text(u'Updated')
|
||||
self.status = urwid.Text(u'Status')
|
||||
self.creator = urwid.Text(u'Creator', wrap='clip')
|
||||
self.columns = urwid.Columns([], dividechars=1)
|
||||
super(StoryListHeader, self).__init__(self.columns)
|
||||
|
||||
def update(self):
|
||||
self.updateColumns()
|
||||
|
||||
|
||||
@mouse_scroll_decorator.ScrollByWheel
|
||||
class StoryListView(urwid.WidgetWrap, mywid.Searchable):
|
||||
required_columns = set(['ID', 'Title', 'Updated'])
|
||||
optional_columns = set([])
|
||||
|
||||
def getCommands(self):
|
||||
if self.project_key:
|
||||
refresh_help = "Sync current project"
|
||||
else:
|
||||
refresh_help = "Sync subscribed projects"
|
||||
return [
|
||||
(keymap.TOGGLE_HELD,
|
||||
"Toggle the held flag for the currently selected story"),
|
||||
(keymap.TOGGLE_HIDDEN,
|
||||
"Toggle the hidden flag for the currently selected story"),
|
||||
(keymap.TOGGLE_LIST_ACTIVE,
|
||||
"Toggle whether only active or all changes are displayed"),
|
||||
(keymap.TOGGLE_STARRED,
|
||||
"Toggle the starred flag for the currently selected story"),
|
||||
(keymap.TOGGLE_MARK,
|
||||
"Toggle the process mark for the currently selected story"),
|
||||
(keymap.REFINE_STORY_SEARCH,
|
||||
"Refine the current search query"),
|
||||
(keymap.REFRESH,
|
||||
refresh_help),
|
||||
(keymap.SORT_BY_NUMBER,
|
||||
"Sort stories by number"),
|
||||
(keymap.SORT_BY_UPDATED,
|
||||
"Sort stories by how recently the story was updated"),
|
||||
(keymap.SORT_BY_REVERSE,
|
||||
"Reverse the sort"),
|
||||
(keymap.INTERACTIVE_SEARCH,
|
||||
"Interactive search"),
|
||||
]
|
||||
|
||||
def help(self):
|
||||
key = self.app.config.keymap.formatKeys
|
||||
commands = self.getCommands()
|
||||
return [(c[0], key(c[0]), c[1]) for c in commands]
|
||||
|
||||
def __init__(self, app, query, query_desc=None, project_key=None,
|
||||
active=False, sort_by=None, reverse=None):
|
||||
super(StoryListView, self).__init__(urwid.Pile([]))
|
||||
self.log = logging.getLogger('boartty.view.story_list')
|
||||
self.searchInit()
|
||||
self.app = app
|
||||
self.query = query
|
||||
self.query_desc = query_desc or query
|
||||
self.active = active
|
||||
self.story_rows = {}
|
||||
self.enabled_columns = set()
|
||||
for colinfo in COLUMNS:
|
||||
if (colinfo.name in self.required_columns or
|
||||
colinfo.name not in self.optional_columns):
|
||||
self.enabled_columns.add(colinfo.name)
|
||||
self.disabled_columns = set()
|
||||
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
||||
self.project_key = project_key
|
||||
if 'Project' not in self.required_columns and project_key is not None:
|
||||
self.enabled_columns.discard('Project')
|
||||
self.disabled_columns.add('Project')
|
||||
#storyboard: creator
|
||||
if 'Owner' not in self.required_columns and 'owner:' in query:
|
||||
# This could be or'd with something else, but probably
|
||||
# not.
|
||||
self.enabled_columns.discard('Owner')
|
||||
self.disabled_columns.add('Owner')
|
||||
self.sort_by = sort_by or app.config.story_list_options['sort-by']
|
||||
if reverse is not None:
|
||||
self.reverse = reverse
|
||||
else:
|
||||
self.reverse = app.config.story_list_options['reverse']
|
||||
self.header = StoryListHeader(self.enabled_columns)
|
||||
self.refresh()
|
||||
self._w.contents.append((app.header, ('pack', 1)))
|
||||
self._w.contents.append((urwid.Divider(), ('pack', 1)))
|
||||
self._w.contents.append((urwid.AttrWrap(self.header, 'table-header'), ('pack', 1)))
|
||||
self._w.contents.append((self.listbox, ('weight', 1)))
|
||||
self._w.set_focus(3)
|
||||
|
||||
def interested(self, event):
|
||||
if not ((self.project_key is not None and
|
||||
isinstance(event, sync.StoryAddedEvent) and
|
||||
self.project_key in event.related_project_keys)
|
||||
or
|
||||
(self.project_key is None and
|
||||
isinstance(event, sync.StoryAddedEvent))
|
||||
or
|
||||
(isinstance(event, sync.StoryUpdatedEvent) and
|
||||
event.story_key in self.story_rows.keys())):
|
||||
self.log.debug("Ignoring refresh story list due to event %s" % (event,))
|
||||
return False
|
||||
self.log.debug("Refreshing story list due to event %s" % (event,))
|
||||
return True
|
||||
|
||||
def refresh(self):
|
||||
unseen_keys = set(self.story_rows.keys())
|
||||
with self.app.db.getSession() as session:
|
||||
story_list = session.getStories(self.query, self.active,
|
||||
sort_by=self.sort_by)
|
||||
if self.active:
|
||||
self.title = (u'Active %d stories in %s' %
|
||||
(len(story_list), self.query_desc))
|
||||
else:
|
||||
self.title = (u'All %d stories in %s' %
|
||||
(len(story_list), self.query_desc))
|
||||
self.short_title = self.query_desc
|
||||
if '/' in self.short_title and ' ' not in self.short_title:
|
||||
i = self.short_title.rfind('/')
|
||||
self.short_title = self.short_title[i+1:]
|
||||
self.app.status.update(title=self.title)
|
||||
|
||||
if 'Status' not in self.required_columns and self.active:
|
||||
self.enabled_columns.discard('Status')
|
||||
self.disabled_columns.add('Status')
|
||||
else:
|
||||
self.enabled_columns.add('Status')
|
||||
self.disabled_columns.discard('Status')
|
||||
self.chooseColumns()
|
||||
self.header.update()
|
||||
i = 0
|
||||
if self.reverse:
|
||||
story_list.reverse()
|
||||
new_rows = []
|
||||
if len(self.listbox.body):
|
||||
focus_pos = self.listbox.focus_position
|
||||
focus_row = self.listbox.body[focus_pos]
|
||||
else:
|
||||
focus_pos = 0
|
||||
focus_row = None
|
||||
for story in story_list:
|
||||
row = self.story_rows.get(story.key)
|
||||
if not row:
|
||||
row = StoryRow(self.app, story,
|
||||
self.enabled_columns,
|
||||
callback=self.onSelect)
|
||||
self.listbox.body.insert(i, row)
|
||||
self.story_rows[story.key] = row
|
||||
else:
|
||||
row.update(story)
|
||||
unseen_keys.remove(story.key)
|
||||
new_rows.append(row)
|
||||
i += 1
|
||||
self.listbox.body[:] = new_rows
|
||||
if focus_row in self.listbox.body:
|
||||
pos = self.listbox.body.index(focus_row)
|
||||
else:
|
||||
pos = min(focus_pos, len(self.listbox.body)-1)
|
||||
self.listbox.body.set_focus(pos)
|
||||
for key in unseen_keys:
|
||||
row = self.story_rows[key]
|
||||
del self.story_rows[key]
|
||||
|
||||
def chooseColumns(self):
|
||||
currently_enabled_columns = self.enabled_columns.copy()
|
||||
size = self.app.loop.screen.get_cols_rows()
|
||||
cols = size[0]
|
||||
for colinfo in COLUMNS:
|
||||
if (colinfo.name not in self.disabled_columns):
|
||||
cols -= colinfo.spacing
|
||||
|
||||
for colinfo in COLUMNS:
|
||||
if colinfo.name in self.optional_columns:
|
||||
if cols >= colinfo.spacing:
|
||||
self.enabled_columns.add(colinfo.name)
|
||||
cols -= colinfo.spacing
|
||||
else:
|
||||
self.enabled_columns.discard(colinfo.name)
|
||||
if currently_enabled_columns != self.enabled_columns:
|
||||
self.header.updateColumns()
|
||||
for key, value in six.iteritems(self.story_rows):
|
||||
value.updateColumns()
|
||||
|
||||
def getQueryString(self):
|
||||
if self.project_key is not None:
|
||||
return "project:%s %s" % (self.query_desc, self.app.config.project_story_list_query)
|
||||
return self.app.config.project_story_list_query
|
||||
return self.query
|
||||
|
||||
def clearStoryList(self):
|
||||
for key, value in six.iteritems(self.story_rows):
|
||||
self.listbox.body.remove(value)
|
||||
self.story_rows = {}
|
||||
|
||||
def getNextStoryKey(self, story_key):
|
||||
row = self.story_rows.get(story_key)
|
||||
try:
|
||||
i = self.listbox.body.index(row)
|
||||
except ValueError:
|
||||
return None
|
||||
if i+1 >= len(self.listbox.body):
|
||||
return None
|
||||
row = self.listbox.body[i+1]
|
||||
return row.story_key
|
||||
|
||||
def getPrevStoryKey(self, story_key):
|
||||
row = self.story_rows.get(story_key)
|
||||
try:
|
||||
i = self.listbox.body.index(row)
|
||||
except ValueError:
|
||||
return None
|
||||
if i <= 0:
|
||||
return None
|
||||
row = self.listbox.body[i-1]
|
||||
return row.story_key
|
||||
|
||||
def toggleStarred(self, story_key):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(story_key)
|
||||
story.starred = not story.starred
|
||||
ret = story.starred
|
||||
story.pending_starred = True
|
||||
self.app.sync.submitTask(
|
||||
sync.StoryStarredTask(story_key, sync.HIGH_PRIORITY))
|
||||
return ret
|
||||
|
||||
def toggleHeld(self, story_key):
|
||||
return self.app.toggleHeldStory(story_key)
|
||||
|
||||
def toggleHidden(self, story_key):
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(story_key)
|
||||
story.hidden = not story.hidden
|
||||
ret = story.hidden
|
||||
hidden_str = 'hidden' if story.hidden else 'visible'
|
||||
self.log.debug("Set story %s to %s", story_key, hidden_str)
|
||||
return ret
|
||||
|
||||
def advance(self):
|
||||
pos = self.listbox.focus_position
|
||||
if pos < len(self.listbox.body)-1:
|
||||
pos += 1
|
||||
self.listbox.focus_position = pos
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self.searchKeypress(size, key):
|
||||
return None
|
||||
|
||||
if not self.app.input_buffer:
|
||||
key = super(StoryListView, self).keypress(size, key)
|
||||
keys = self.app.input_buffer + [key]
|
||||
commands = self.app.config.keymap.getCommands(keys)
|
||||
ret = self.handleCommands(commands)
|
||||
if ret is True:
|
||||
if keymap.FURTHER_INPUT not in commands:
|
||||
self.app.clearInputBuffer()
|
||||
return None
|
||||
return key
|
||||
|
||||
def onResize(self):
|
||||
self.chooseColumns()
|
||||
|
||||
def handleCommands(self, commands):
|
||||
if keymap.TOGGLE_LIST_ACTIVE in commands:
|
||||
self.active = not self.active
|
||||
self.refresh()
|
||||
return True
|
||||
if keymap.TOGGLE_HIDDEN in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
pos = self.listbox.focus_position
|
||||
story_key = self.listbox.body[pos].story_key
|
||||
hidden = self.toggleHidden(story_key)
|
||||
if hidden:
|
||||
# Here we can avoid a full refresh by just removing the particular
|
||||
# row from the story list
|
||||
row = self.story_rows[story_key]
|
||||
self.listbox.body.remove(row)
|
||||
del self.story_rows[story_key]
|
||||
else:
|
||||
# Just fall back on doing a full refresh if we're in a situation
|
||||
# where we're not just popping a row from the list of stories.
|
||||
self.refresh()
|
||||
self.advance()
|
||||
return True
|
||||
if keymap.TOGGLE_HELD in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
pos = self.listbox.focus_position
|
||||
story_key = self.listbox.body[pos].story_key
|
||||
self.toggleHeld(story_key)
|
||||
row = self.story_rows[story_key]
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(story_key)
|
||||
row.update(story)
|
||||
self.advance()
|
||||
return True
|
||||
if keymap.TOGGLE_STARRED in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
pos = self.listbox.focus_position
|
||||
story_key = self.listbox.body[pos].story_key
|
||||
self.toggleStarred(story_key)
|
||||
row = self.story_rows[story_key]
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(story_key)
|
||||
row.update(story)
|
||||
self.advance()
|
||||
return True
|
||||
if keymap.TOGGLE_MARK in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
pos = self.listbox.focus_position
|
||||
story_key = self.listbox.body[pos].story_key
|
||||
row = self.story_rows[story_key]
|
||||
row.mark = not row.mark
|
||||
with self.app.db.getSession() as session:
|
||||
story = session.getStory(story_key)
|
||||
row.update(story)
|
||||
self.advance()
|
||||
return True
|
||||
if keymap.REFRESH in commands:
|
||||
if self.project_key:
|
||||
self.app.sync.submitTask(
|
||||
sync.SyncProjectTask(self.project_key, sync.HIGH_PRIORITY))
|
||||
else:
|
||||
self.app.sync.submitTask(
|
||||
sync.SyncSubscribedProjectsTask(sync.HIGH_PRIORITY))
|
||||
self.app.status.update()
|
||||
return True
|
||||
if keymap.SORT_BY_NUMBER in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
self.sort_by = 'number'
|
||||
self.clearStoryList()
|
||||
self.refresh()
|
||||
return True
|
||||
if keymap.SORT_BY_UPDATED in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
self.sort_by = 'updated'
|
||||
self.clearStoryList()
|
||||
self.refresh()
|
||||
return True
|
||||
if keymap.SORT_BY_REVERSE in commands:
|
||||
if not len(self.listbox.body):
|
||||
return True
|
||||
if self.reverse:
|
||||
self.reverse = False
|
||||
else:
|
||||
self.reverse = True
|
||||
self.clearStoryList()
|
||||
self.refresh()
|
||||
return True
|
||||
if keymap.REFINE_STORY_SEARCH in commands:
|
||||
default = self.getQueryString()
|
||||
self.app.searchDialog(default)
|
||||
return True
|
||||
if keymap.INTERACTIVE_SEARCH in commands:
|
||||
self.searchStart()
|
||||
return True
|
||||
return False
|
||||
|
||||
def onSelect(self, button, story_key):
|
||||
try:
|
||||
view = view_story.StoryView(self.app, story_key)
|
||||
self.app.changeScreen(view)
|
||||
except boartty.view.DisplayError as e:
|
||||
self.app.error(str(e))
|
||||
|
@ -85,17 +85,17 @@ qthelp:
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Gertty.qhcp"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Boartty.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Gertty.qhc"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Boartty.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Gertty"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Gertty"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Boartty"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Boartty"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Gertty documentation build configuration file, created by
|
||||
# Boartty documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Jan 15 13:41:54 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
@ -45,17 +45,17 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Gertty'
|
||||
copyright = u'%s, Gertty Contributors' % datetime.date.today().year
|
||||
project = u'Boartty'
|
||||
copyright = u'%s, Boartty Contributors' % datetime.date.today().year
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
from gertty.version import version_info as gertty_version
|
||||
release = gertty_version.version_string_with_vcs()
|
||||
from boartty.version import version_info as boartty_version
|
||||
release = boartty_version.version_string_with_vcs()
|
||||
# The short X.Y version.
|
||||
version = gertty_version.canonical_version_string()
|
||||
version = boartty_version.canonical_version_string()
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -178,7 +178,7 @@ html_static_path = ['_static']
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Gerttydoc'
|
||||
htmlhelp_basename = 'Boarttydoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@ -198,7 +198,7 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'Gertty.tex', u'Gertty Documentation',
|
||||
('index', 'Boartty.tex', u'Boartty Documentation',
|
||||
u'James E. Blair', 'manual'),
|
||||
]
|
||||
|
||||
@ -228,7 +228,7 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'gertty', u'Gertty Documentation',
|
||||
('index', 'boartty', u'Boartty Documentation',
|
||||
[u'James E. Blair'], 1)
|
||||
]
|
||||
|
||||
@ -242,8 +242,8 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Gertty', u'Gertty Documentation',
|
||||
u'James E. Blair', 'Gertty', 'One line description of project.',
|
||||
('index', 'Boartty', u'Boartty Documentation',
|
||||
u'James E. Blair', 'Boartty', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
@ -1,44 +1,36 @@
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Gertty uses a YAML based configuration file that it looks for at
|
||||
``~/.gertty.yaml``. Several sample configuration files are included.
|
||||
Boartty uses a YAML based configuration file that it looks for at
|
||||
``~/.boartty.yaml``. Several sample configuration files are included.
|
||||
You can find them in the examples/ directory of the
|
||||
`source distribution <https://git.openstack.org/cgit/openstack/gertty/tree/examples>`_
|
||||
or the share/gertty/examples directory after installation.
|
||||
`source distribution <https://git.openstack.org/cgit/openstack/boartty/tree/examples>`_
|
||||
or the share/boartty/examples directory after installation.
|
||||
|
||||
Select one of the sample config files, copy it to ~/.gertty.yaml and
|
||||
Select one of the sample config files, copy it to ~/.boartty.yaml and
|
||||
edit as necessary. Search for ``CHANGEME`` to find parameters that
|
||||
need to be supplied. The sample config files are as follows:
|
||||
|
||||
**minimal-gertty.yaml**
|
||||
Only contains the parameters required for Gertty to actually run.
|
||||
**minimal-boartty.yaml**
|
||||
Only contains the parameters required for Boartty to actually run.
|
||||
|
||||
**reference-gertty.yaml**
|
||||
**reference-boartty.yaml**
|
||||
An exhaustive list of all supported options with examples.
|
||||
|
||||
**openstack-gertty.yaml**
|
||||
**openstack-boartty.yaml**
|
||||
A configuration designed for use with OpenStack's installation of
|
||||
Gerrit.
|
||||
|
||||
**googlesource-gertty.yaml**
|
||||
A configuration designed for use with installations of Gerrit
|
||||
running on googlesource.com.
|
||||
You will need a Storyboard authentication token which you can generate
|
||||
or retrieve by navigating to ``Profile``, then ``Tokens`` (the "key"
|
||||
icon), or visiting the `/#!/profile/tokens` URI in your Storyboard
|
||||
installation. Issue a new token if you have not done so before, and
|
||||
give it a sufficiently long lifetime (for example, one decade). Copy
|
||||
and paste the resulting token in your ``~/.boartty.yaml`` file.
|
||||
|
||||
You will need your Gerrit password which you can generate or retrieve
|
||||
by navigating to ``Settings``, then ``HTTP Password``.
|
||||
|
||||
Gertty uses local git repositories to perform much of its work. These
|
||||
can be the same git repositories that you use when developing a
|
||||
project. Gertty will not alter the working directory or index unless
|
||||
you request it to (and even then, the usual git safeguards against
|
||||
accidentally losing work remain in place). You will need to supply
|
||||
the name of a directory where Gertty will find or clone git
|
||||
repositories for your projects as the ``git-root`` parameter.
|
||||
|
||||
The config file is designed to support multiple Gerrit instances. The
|
||||
first one is used by default, but others can be specified by supplying
|
||||
the name on the command line.
|
||||
The config file is designed to support multiple Storyboard instances.
|
||||
The first one is used by default, but others can be specified by
|
||||
supplying the name on the command line.
|
||||
|
||||
Configuration Reference
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -49,8 +41,8 @@ configuration file.
|
||||
Servers
|
||||
+++++++
|
||||
|
||||
This section lists the servers that Gertty can talk to. Multiple
|
||||
servers may be listed; by default, Gertty will use the first one
|
||||
This section lists the servers that Boartty can talk to. Multiple
|
||||
servers may be listed; by default, Boartty will use the first one
|
||||
listed. To select another, simply specify its name on the command
|
||||
line.
|
||||
|
||||
@ -65,30 +57,17 @@ line.
|
||||
**url (required)**
|
||||
The URL of the Gerrit server. HTTPS should be preferred.
|
||||
|
||||
**username (required)**
|
||||
Your username in Gerrit. [required]
|
||||
|
||||
**password (required)**
|
||||
Your password in Gerrit. Obtain it from Settings -> HTTP Password
|
||||
in the Gerrit web interface.
|
||||
|
||||
**auth-type**
|
||||
Authentication type required by the Gerrit server. Can be 'basic',
|
||||
'digest', or 'form'. Defaults to 'digest'.
|
||||
|
||||
**git-root (required)**
|
||||
A location where Gertty should store its git repositories. These
|
||||
can be the same git repositories where you do your own work --
|
||||
Gertty will not modify them unless you tell it to, and even then
|
||||
the normal git protections against losing work remain in place.
|
||||
**token (required)**
|
||||
Your authentication token from Storyboard. Obtain it as described
|
||||
above in "Configuration".
|
||||
|
||||
**dburi**
|
||||
The location of Gertty's sqlite database. If you have more than
|
||||
The location of Boartty's sqlite database. If you have more than
|
||||
one server, you should specify a dburi for any additional servers.
|
||||
By default a SQLite database called ~/.gertty.db is used.
|
||||
By default a SQLite database called ~/.boartty.db is used.
|
||||
|
||||
**ssl-ca-path**
|
||||
If your Gerrit server uses a non-standard certificate chain
|
||||
If your Storyboard server uses a non-standard certificate chain
|
||||
(e.g. on a test server), you can pass a full path to a bundle of
|
||||
CA certificates here:
|
||||
|
||||
@ -98,18 +77,18 @@ line.
|
||||
turn off certificate validation.
|
||||
|
||||
**log-file**
|
||||
By default Gertty logs errors to a file and truncates that file
|
||||
By default Boartty logs errors to a file and truncates that file
|
||||
each time it starts (so that it does not grow without bound). If
|
||||
you would like to log to a different location, you may specify it
|
||||
with this option.
|
||||
|
||||
**socket**
|
||||
Gertty listens on a unix domain socket for remote commands at
|
||||
~/.gertty.sock. This option may be used to change the path.
|
||||
Boartty listens on a unix domain socket for remote commands at
|
||||
~/.boartty.sock. This option may be used to change the path.
|
||||
|
||||
**lock-file**
|
||||
Gertty uses a lock file per server to prevent multiple processes
|
||||
from running at the same time. The default is ~/.gertty.servername.lock
|
||||
Boartty uses a lock file per server to prevent multiple processes
|
||||
from running at the same time. The default is ~/.boartty.servername.lock
|
||||
|
||||
Example:
|
||||
|
||||
@ -117,14 +96,12 @@ Example:
|
||||
servers:
|
||||
- name: CHANGEME
|
||||
url: https://CHANGEME.example.org/
|
||||
username: CHANGEME
|
||||
password: CHANGEME
|
||||
git-root: ~/git/
|
||||
token: CHANGEME
|
||||
|
||||
Palettes
|
||||
++++++++
|
||||
|
||||
Gertty comes with two palettes defined internally. The default
|
||||
Boartty comes with two palettes defined internally. The default
|
||||
palette is suitable for use on a terminal with a dark background. The
|
||||
`light` palette is for a terminal with a white or light background.
|
||||
You may customize the colors in either of those palettes, or define
|
||||
@ -140,7 +117,7 @@ high-color terminals.
|
||||
For a reference of possible color names, see the `Urwid Manual
|
||||
<http://urwid.org/manual/displayattributes.html#foreground-and-background-settings>`_
|
||||
|
||||
To see the list of possible palette entries, run `gertty --print-palette`.
|
||||
To see the list of possible palette entries, run `boartty --print-palette`.
|
||||
|
||||
The following example alters two colors in the default palette, one
|
||||
color in the light palette, and one color in a custom palette.
|
||||
@ -148,12 +125,12 @@ color in the light palette, and one color in a custom palette.
|
||||
.. code-block: yaml
|
||||
palettes:
|
||||
- name: default
|
||||
added-line: ['dark green', '']
|
||||
added-word: ['light green', '']
|
||||
task-title: ['light green', '']
|
||||
task-id: ['dark cyan', '']
|
||||
- name: light
|
||||
filename: ['dark cyan', '']
|
||||
task-project: ['dark blue', '']
|
||||
- name: custom
|
||||
filename: ['light yellow', '']
|
||||
task-project: ['dark red', '']
|
||||
|
||||
Palettes may be selected at runtime with the `-p PALETTE` command
|
||||
line option, or you may set the default palette in the config file.
|
||||
@ -170,21 +147,21 @@ may be overridden and custom keymaps defined and selected in the
|
||||
config file or the command line.
|
||||
|
||||
Each keymap contains a mapping of command -> key(s). If a command is
|
||||
not specified, Gertty will use the keybinding specified in the default
|
||||
not specified, Boartty will use the keybinding specified in the default
|
||||
map. More than one key can be bound to a command.
|
||||
|
||||
Run `gertty --print-keymap` for a list of commands that can be bound.
|
||||
Run `boartty --print-keymap` for a list of commands that can be bound.
|
||||
|
||||
The following example modifies the `default` keymap:
|
||||
|
||||
.. code-block: yaml
|
||||
keymaps:
|
||||
- name: default
|
||||
diff: 'd'
|
||||
leave-comment: 'r'
|
||||
- name: custom
|
||||
review: ['r', 'R']
|
||||
- name: osx #OS X blocks ctrl+o
|
||||
change-search: 'ctrl s'
|
||||
leave-comment: ['r', 'R']
|
||||
- name: osx # OS X blocks ctrl+o
|
||||
story-search: 'ctrl s'
|
||||
|
||||
|
||||
To specify a sequence of keys, they must be a list of keystrokes
|
||||
@ -204,9 +181,9 @@ option, or in the config file.
|
||||
Commentlinks
|
||||
++++++++++++
|
||||
|
||||
Commentlinks are regular expressions that are applied to commit and
|
||||
review messages. They can be replaced with internal or external
|
||||
links, or have colors applied.
|
||||
Commentlinks are regular expressions that are applied to story
|
||||
descriptions and comments. They can be replaced with internal or
|
||||
external links, or have colors applied.
|
||||
|
||||
**commentlinks**
|
||||
This is a list of commentlink patterns. Each commentlink pattern is
|
||||
@ -247,7 +224,7 @@ links, or have colors applied.
|
||||
palette entry.
|
||||
|
||||
**search**
|
||||
A hyperlink that will perform a Gertty search when activated.
|
||||
A hyperlink that will perform a Boartty search when activated.
|
||||
|
||||
**text**
|
||||
The replacement text.
|
||||
@ -255,31 +232,31 @@ links, or have colors applied.
|
||||
**query**
|
||||
The search query to use.
|
||||
|
||||
This example matches Gerrit change ids, and replaces them with a link
|
||||
to an internal Gertty search for that change id.
|
||||
This example matches story numbers, and replaces them with a link to
|
||||
an internal Boartty search for that story.
|
||||
|
||||
.. code-block: yaml
|
||||
commentlinks:
|
||||
- match: "(?P<id>I[0-9a-fA-F]{40})"
|
||||
- match: "(?P<id>[0-9]+)"
|
||||
replacements:
|
||||
- search:
|
||||
text: "{id}"
|
||||
query: "change:{id}"
|
||||
query: "story:{id}"
|
||||
|
||||
Change List Options
|
||||
+++++++++++++++++++
|
||||
Story List Options
|
||||
++++++++++++++++++
|
||||
|
||||
**change-list-query**
|
||||
This is the query used for the list of changes when a project is
|
||||
selected. The default is `status:open`.
|
||||
**story-list-query**
|
||||
This is the query used for the list of storyies when a project is
|
||||
selected. The default is empty.
|
||||
|
||||
**change-list-options**
|
||||
This section defines default sorting options for the change list.
|
||||
**story-list-options**
|
||||
This section defines default sorting options for the story list.
|
||||
|
||||
**sort-by**
|
||||
This key specifies the sort order, which can be `number` (the
|
||||
Change number), `updated` (when the change was last updated), or
|
||||
`last-seen` (when the change was last opened in Gertty).
|
||||
Story number), `updated` (when the story was last updated), or
|
||||
`last-seen` (when the story was last opened in Boartty).
|
||||
|
||||
**reverse**
|
||||
This is a boolean value which indicates whether the list should be
|
||||
@ -288,45 +265,16 @@ Change List Options
|
||||
Example:
|
||||
|
||||
.. code-block: yaml
|
||||
change-list-options:
|
||||
story-list-options:
|
||||
sort-by: 'number'
|
||||
reverse: false
|
||||
|
||||
**thread-changes**
|
||||
Dependent changes are displayed as "threads" in the change list by
|
||||
default. To disable this behavior, set this value to false.
|
||||
|
||||
Change View Options
|
||||
+++++++++++++++++++
|
||||
|
||||
**hide-comments**
|
||||
This is a list of descriptors which cause matching comments to be
|
||||
hidden by default. Press the `t` key to toggle the display of
|
||||
matching comments.
|
||||
|
||||
The only supported criterion is `author`.
|
||||
|
||||
**author**
|
||||
A regular expression to match against the comment author's name.
|
||||
|
||||
For example, to hide comments from a CI system:
|
||||
|
||||
.. code-block: yaml
|
||||
|
||||
hide-comments:
|
||||
- author: "^(.*CI|Jenkins)$"
|
||||
|
||||
**diff-view**
|
||||
Specifies how patch diffs should be displayed. The values `unified`
|
||||
or `side-by-side` (the default) are supported.
|
||||
|
||||
|
||||
Dashboards
|
||||
++++++++++
|
||||
|
||||
This section defines customized dashboards. You may supply any
|
||||
Gertty search string and bind them to any key. They will appear in
|
||||
the global help text, and pressing the key anywhere in Gertty will
|
||||
Boartty search string and bind them to any key. They will appear in
|
||||
the global help text, and pressing the key anywhere in Boartty will
|
||||
run the query and display the results.
|
||||
|
||||
**dashboards**
|
||||
@ -337,7 +285,7 @@ run the query and display the results.
|
||||
bar at the top of the screen.
|
||||
|
||||
**query**
|
||||
The search query to perform to gather changes to be listed in the
|
||||
The search query to perform to gather stories to be listed in the
|
||||
dashboard.
|
||||
|
||||
**key**
|
||||
@ -348,60 +296,15 @@ Example:
|
||||
.. code-block: yaml
|
||||
|
||||
dashboards:
|
||||
- name: "My changes"
|
||||
query: "owner:self status:open"
|
||||
- name: "My stories"
|
||||
query: "creator:self status:active"
|
||||
key: "f2"
|
||||
|
||||
Reviewkeys
|
||||
++++++++++
|
||||
|
||||
Reviewkeys are hotkeys that perform immediate reviews within the
|
||||
change screen. Any pending comments or review messages will be
|
||||
attached to the review; otherwise an empty review message will be
|
||||
left. The approvals list is exhaustive, so if you specify an empty
|
||||
list, Gertty will submit a review that clears any previous approvals.
|
||||
Reviewkeys appear in the help text for the change screen.
|
||||
|
||||
**reviewkeys**
|
||||
A list of reviewkey definitions, the format of which is described
|
||||
below.
|
||||
|
||||
**key**
|
||||
This key to which this review action should be bound.
|
||||
|
||||
**approvals**
|
||||
A list of approvals to include when this reviewkey is activated.
|
||||
Each element of the list should include both a category and a
|
||||
value.
|
||||
|
||||
**category**
|
||||
The name of the review label for this approval.
|
||||
|
||||
**value**
|
||||
The value for this approval.
|
||||
|
||||
**submit**
|
||||
Set this to `true` to instruct Gerrit to submit the change when
|
||||
this reviewkey is activated.
|
||||
|
||||
The following example includes a reviewkey that clears all labels, as
|
||||
well as one that leaves a +1 "Code-Review" approval.
|
||||
|
||||
.. code-block: yaml
|
||||
|
||||
reviewkeys:
|
||||
- key: 'meta 0'
|
||||
approvals: []
|
||||
- key: 'meta 1'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 1
|
||||
|
||||
General Options
|
||||
+++++++++++++++
|
||||
|
||||
**breadcrumbs**
|
||||
Gertty displays a footer at the bottom of the screen by default
|
||||
Boartty displays a footer at the bottom of the screen by default
|
||||
which contains navigation information in the form of "breadcrumbs"
|
||||
-- short descriptions of previous screens, with the right-most entry
|
||||
indicating the screen that will be displayed if you press the `ESC`
|
||||
@ -412,15 +315,6 @@ General Options
|
||||
them in UTC instead, set this value to `true`.
|
||||
|
||||
**handle-mouse**
|
||||
Gertty handles mouse input by default. If you don't want it
|
||||
Boartty handles mouse input by default. If you don't want it
|
||||
interfering with your terminal's mouse handling, set this value to
|
||||
`false`.
|
||||
|
||||
**expire-age**
|
||||
By default, closed changes that are older than two months are
|
||||
removed from the local database (and their refs are removed from the
|
||||
local git repos so that git may garbage collect them). If you would
|
||||
like to change the expiration delay or disable it, uncomment the
|
||||
following line. The time interval is specified in the same way as
|
||||
the "age:" term in Gerrit's search syntax. To disable it
|
||||
altogether, set the value to the empty string.
|
||||
|
@ -1,10 +1,10 @@
|
||||
Contributing
|
||||
------------
|
||||
|
||||
For information on how to contribute to Gertty, please see the
|
||||
For information on how to contribute to Boartty, please see the
|
||||
contents of the CONTRIBUTING.rst file.
|
||||
|
||||
Bugs
|
||||
~~~~
|
||||
|
||||
Bugs are handled at: https://storyboard.openstack.org/#!/project/698
|
||||
Bugs are handled at: https://storyboard.openstack.org/
|
||||
|
@ -1,28 +1,23 @@
|
||||
Gertty
|
||||
======
|
||||
Boartty
|
||||
=======
|
||||
|
||||
Gertty is a console-based interface to the Gerrit Code Review system.
|
||||
Boartty is a console-based interface to the Storyboard task-tracking
|
||||
system.
|
||||
|
||||
As compared to the web interface, the main advantages are:
|
||||
|
||||
* Workflow -- the interface is designed to support a workflow similar
|
||||
to reading network news or mail. In particular, it is designed to
|
||||
deal with a large number of review requests across a large number
|
||||
of projects.
|
||||
deal with a large number of stories across a large number of
|
||||
projects.
|
||||
|
||||
* Offline Use -- Gertty syncs information about changes in subscribed
|
||||
projects to a local database and local git repos. All review
|
||||
operations are performed against that database and then synced back
|
||||
to Gerrit.
|
||||
* Offline Use -- Boartty syncs information about changes in
|
||||
subscribed projects to a local database. All review operations are
|
||||
performed against that database and then synced back to Storyboard.
|
||||
|
||||
* Speed -- user actions modify locally cached content and need not
|
||||
wait for server interaction.
|
||||
|
||||
* Convenience -- because Gertty downloads all changes to local git
|
||||
repos, a single command instructs it to checkout a change into that
|
||||
repo for detailed examination or testing of larger changes.
|
||||
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
|
@ -1,52 +1,18 @@
|
||||
Installation
|
||||
------------
|
||||
|
||||
Debian
|
||||
~~~~~~
|
||||
|
||||
Gertty is packaged in Debian and is currently available in:
|
||||
|
||||
* unstable
|
||||
* testing
|
||||
* stable
|
||||
|
||||
You can install it with::
|
||||
|
||||
apt-get install gertty
|
||||
|
||||
Fedora
|
||||
~~~~~~
|
||||
|
||||
Gertty is packaged starting in Fedora 21. You can install it with::
|
||||
|
||||
yum install python-gertty
|
||||
|
||||
openSUSE
|
||||
~~~~~~~~
|
||||
|
||||
Gertty is packaged for openSUSE 13.1 onwards. You can install it via
|
||||
`1-click install from the Open Build Service <http://software.opensuse.org/package/python-gertty>`_.
|
||||
|
||||
Arch Linux
|
||||
~~~~~~~~~~
|
||||
|
||||
Gertty packages are available in the Arch User Repository packages. You
|
||||
can get the package from::
|
||||
|
||||
https://aur.archlinux.org/packages/python2-gertty/
|
||||
|
||||
Source
|
||||
~~~~~~
|
||||
|
||||
When installing from source, it is recommended (but not required) to
|
||||
install Gertty in a virtualenv. To set one up::
|
||||
install Boartty in a virtualenv. To set one up::
|
||||
|
||||
virtualenv gertty-env
|
||||
source gertty-env/bin/activate
|
||||
virtualenv boartty-env
|
||||
source boartty-env/bin/activate
|
||||
|
||||
To install the latest version from the cheeseshop::
|
||||
|
||||
pip install gertty
|
||||
pip install boartty
|
||||
|
||||
To install from a git checkout::
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
Usage
|
||||
-----
|
||||
|
||||
After installing Gertty, you should be able to run it by invoking
|
||||
``gertty``. If you installed it in a virtualenv, you can invoke it
|
||||
without activating the virtualenv with ``/path/to/venv/bin/gertty``
|
||||
which you may wish to add to your shell aliases. Use ``gertty
|
||||
After installing Boartty, you should be able to run it by invoking
|
||||
``boartty``. If you installed it in a virtualenv, you can invoke it
|
||||
without activating the virtualenv with ``/path/to/venv/bin/boartty``
|
||||
which you may wish to add to your shell aliases. Use ``boartty
|
||||
--help`` to see a list of command line options available.
|
||||
|
||||
Once Gertty is running, you will need to start by subscribing to some
|
||||
Once Boartty is running, you will need to start by subscribing to some
|
||||
projects. Use 'L' to list all of the projects and then 's' to
|
||||
subscribe to the ones you are interested in. Hit 'L' again to shrink
|
||||
the list to your subscribed projects.
|
||||
@ -15,37 +15,27 @@ the list to your subscribed projects.
|
||||
In general, pressing the F1 key will show help text on any screen, and
|
||||
ESC will take you to the previous screen.
|
||||
|
||||
Gertty works seamlessly offline or online. All of the actions that it
|
||||
performs are first recorded in a local database (in ``~/.gertty.db``
|
||||
by default), and are then transmitted to Gerrit. If Gertty is unable
|
||||
to contact Gerrit for any reason, it will continue to operate against
|
||||
the local database, and once it re-establishes contact, it will
|
||||
process any pending changes.
|
||||
Boartty works seamlessly offline or online. All of the actions that
|
||||
it performs are first recorded in a local database (in
|
||||
``~/.boartty.db`` by default), and are then transmitted to Storyboard.
|
||||
If Boartty is unable to contact Storyboard for any reason, it will
|
||||
continue to operate against the local database, and once it
|
||||
re-establishes contact, it will process any pending changes.
|
||||
|
||||
The status bar at the top of the screen displays the current number of
|
||||
outstanding tasks that Gertty must perform in order to be fully up to
|
||||
outstanding tasks that Boartty must perform in order to be fully up to
|
||||
date. Some of these tasks are more complicated than others, and some
|
||||
of them will end up creating new tasks (for instance, one task may be
|
||||
to search for new changes in a project which will then produce 5 new
|
||||
tasks if there are 5 new changes).
|
||||
to search for new stories in a project which will then produce 5 new
|
||||
tasks if there are 5 new stories).
|
||||
|
||||
If Gertty is offline, it will so indicate in the status bar. It will
|
||||
If Boartty is offline, it will so indicate in the status bar. It will
|
||||
retry requests if needed, and will switch between offline and online
|
||||
mode automatically.
|
||||
|
||||
If you review a change while offline with a positive vote, and someone
|
||||
else leaves a negative vote on that change in the same category before
|
||||
Gertty is able to upload your review, Gertty will detect the situation
|
||||
and mark the change as "held" so that you may re-inspect the change
|
||||
and any new comments before uploading the review. The status bar will
|
||||
alert you to any held changes and direct you to a list of them (the
|
||||
`F12` key by default). When viewing a change, the "held" flag may be
|
||||
toggled with the exclamation key (`!`). Once held, a change must be
|
||||
explicitly un-held in this manner for your review to be uploaded.
|
||||
|
||||
If Gertty encounters an error, this will also be indicated in the
|
||||
status bar. You may wish to examine ~/.gertty.log to see what the
|
||||
error was. In many cases, Gertty can continue after encountering an
|
||||
If Boartty encounters an error, this will also be indicated in the
|
||||
status bar. You may wish to examine ~/.boartty.log to see what the
|
||||
error was. In many cases, Boartty can continue after encountering an
|
||||
error. The error flag will be cleared when you leave the current
|
||||
screen.
|
||||
|
||||
@ -56,17 +46,17 @@ Terminal Integration
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you use rxvt-unicode, you can add something like the following to
|
||||
``.Xresources`` to make Gerrit URLs that are displayed in your
|
||||
``.Xresources`` to make Storyboard URLs that are displayed in your
|
||||
terminal (perhaps in an email or irc client) clickable links that open
|
||||
in Gertty::
|
||||
in Boartty::
|
||||
|
||||
URxvt.perl-ext: default,matcher
|
||||
URxvt.url-launcher: sensible-browser
|
||||
URxvt.keysym.C-Delete: perl:matcher:last
|
||||
URxvt.keysym.M-Delete: perl:matcher:list
|
||||
URxvt.matcher.button: 1
|
||||
URxvt.matcher.pattern.1: https:\/\/review.example.org/(\\#\/c\/)?(\\d+)[\w]*
|
||||
URxvt.matcher.launcher.1: gertty --open $0
|
||||
URxvt.matcher.pattern.1: https:\/\/storyboard.example.org/#!/story/(\\d+)[\w]*
|
||||
URxvt.matcher.launcher.1: boartty --open $0
|
||||
|
||||
You will want to adjust the pattern to match the review site you are
|
||||
You will want to adjust the pattern to match the Storyboard site you are
|
||||
interested in; multiple patterns may be added as needed.
|
||||
|
@ -1,88 +0,0 @@
|
||||
# This is an example ~/.gertty.yaml file for use with installations of
|
||||
# Gerrit running on googlesource.com. Most of these options are not
|
||||
# required, rather, they customize Gertty to better deal with the
|
||||
# particulars of Google's Gerrit configuration.
|
||||
|
||||
# This file does not list all of the available options. For a full
|
||||
# list with explanations, see the 'reference-gertty.yaml' file.
|
||||
|
||||
servers:
|
||||
- name: CHANGEME-review
|
||||
url: https://CHANGEME-review.googlesource.com/
|
||||
# Your gerrit username.
|
||||
username: CHANGEME
|
||||
# Set password at https://{name}-review.googlesource.com/#/settings/http-password
|
||||
# Note this is not your Google password.
|
||||
password: CHANGEME
|
||||
auth-type: basic
|
||||
git-root: ~/git/
|
||||
|
||||
# Uncomment the next line if your terminal has a white background
|
||||
# palette: light
|
||||
|
||||
# Commentlinks are regexes that are applied to commit and review
|
||||
# messages. They can be replaced with internal or external links, or
|
||||
# have colors applied.
|
||||
commentlinks:
|
||||
# Match Gerrit change ids, and replace them with a link to an
|
||||
# internal Gertty search for that change id.
|
||||
- match: "(?P<id>I[0-9a-fA-F]{40})"
|
||||
replacements:
|
||||
- search:
|
||||
text: "{id}"
|
||||
query: "change:{id}"
|
||||
|
||||
# Uncomment the following line to use a unified diff view instead
|
||||
# of the default side-by-side:
|
||||
# diff-view: unified
|
||||
|
||||
# This section defines customized dashboards. You can supply any
|
||||
# Gertty search string and bind them to any key. They will appear in
|
||||
# the global help text, and pressing the key anywhere in Gertty will
|
||||
# discard the current display stack and replace it with the results of
|
||||
# the query.
|
||||
#
|
||||
# NB: "recentlyseen:24 hours" does not just return changes seen in the
|
||||
# last 24 hours -- it returns changes seen within 24 hours of the most
|
||||
# recently seen change. So you can take the weekend off and pick up
|
||||
# where you were.
|
||||
dashboards:
|
||||
- name: "My changes"
|
||||
query: "owner:self status:open"
|
||||
key: "f2"
|
||||
- name: "Incoming reviews"
|
||||
query: "is:open is:reviewer"
|
||||
key: "f3"
|
||||
- name: "Starred changes"
|
||||
query: "is:starred"
|
||||
key: "f4"
|
||||
- name: "Recently seen changes"
|
||||
query: "recentlyseen:24 hours"
|
||||
sort-by: "last-seen"
|
||||
reverse: True
|
||||
key: "f5"
|
||||
|
||||
# Reviewkeys are hotkeys that perform immediate reviews within the
|
||||
# change screen. Any pending comments or review messages will be
|
||||
# attached to the review; otherwise an empty review will be left. The
|
||||
# approvals list is exhaustive, so if you specify an empty list,
|
||||
# Gertty will submit a review that clears any previous approvals. To
|
||||
# submit the change with the review, include 'submit: True' with the
|
||||
# reviewkey. Reviewkeys appear in the help text for the change
|
||||
# screen.
|
||||
reviewkeys:
|
||||
- key: 'meta 0'
|
||||
approvals: []
|
||||
- key: 'meta 1'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 1
|
||||
- key: 'meta 2'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 2
|
||||
- key: 'meta 3'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 2
|
||||
submit: True
|
15
examples/minimal-boartty.yaml
Normal file
15
examples/minimal-boartty.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# This is an example ~/.boartty.yaml file with only the required
|
||||
# settings.
|
||||
|
||||
# This file does not list all of the available options. For a full
|
||||
# list with explanations, see the 'reference-boartty.yaml' file.
|
||||
|
||||
servers:
|
||||
- name: CHANGEME
|
||||
url: https://CHANGEME.example.org/
|
||||
# Your authentication token for Storyboard. Go to the "Profile"
|
||||
# and then "Tokens" (the "key" icon) page in the Storyboard web
|
||||
# interface and create a token. Give it a sufficiently long
|
||||
# validity period (e.g., one decade), and copy and paste the value
|
||||
# here.
|
||||
token: CHANGEME
|
@ -1,13 +0,0 @@
|
||||
# This is an example ~/.gertty.yaml file with only the required
|
||||
# settings.
|
||||
|
||||
# This file does not list all of the available options. For a full
|
||||
# list with explanations, see the 'reference-gertty.yaml' file.
|
||||
|
||||
servers:
|
||||
- name: CHANGEME
|
||||
url: https://CHANGEME.example.org/
|
||||
username: CHANGEME
|
||||
# Set corresponding HTTP password in gerrit settings
|
||||
password: CHANGEME
|
||||
git-root: ~/git/
|
42
examples/openstack-boartty.yaml
Normal file
42
examples/openstack-boartty.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
# This is an example ~/.boartty.yaml file for use with OpenStack's
|
||||
# Storyboard. Most of these options are not required, rather, they
|
||||
# customize Boartty to better deal with the particulars of OpenStack's
|
||||
# Storyboard configuration.
|
||||
|
||||
# This file does not list all of the available options. For a full
|
||||
# list with explanations, see the 'reference-boartty.yaml' file.
|
||||
|
||||
servers:
|
||||
- name: openstack
|
||||
url: https://review.openstack.org/
|
||||
# Your authentication token for Storyboard. Go to the "Profile"
|
||||
# and then "Tokens" (the "key" icon) page in the Storyboard web
|
||||
# interface and create a token. Give it a sufficiently long
|
||||
# validity period (e.g., one decade), and copy and paste the value
|
||||
# here.
|
||||
token: CHANGEME
|
||||
|
||||
# Uncomment the next line if your terminal has a white background
|
||||
# palette: light
|
||||
|
||||
# This section defines customized dashboards. You can supply any
|
||||
# Boartty search string and bind them to any key. They will appear in
|
||||
# the global help text, and pressing the key anywhere in Boartty will
|
||||
# run the query and display the results.
|
||||
#
|
||||
# NB: "recentlyseen:24 hours" does not just return stories seen in the
|
||||
# last 24 hours -- it returns stories seen within 24 hours of the most
|
||||
# recently seen change. So you can take the weekend off and pick up
|
||||
# where you were.
|
||||
dashboards:
|
||||
- name: "My stories"
|
||||
query: "creator:self status:active"
|
||||
key: "f2"
|
||||
- name: "Starred stories"
|
||||
query: "is:starred"
|
||||
key: "f3"
|
||||
- name: "Recently seen stories"
|
||||
query: "recentlyseen:24 hours"
|
||||
sort-by: "last-seen"
|
||||
reverse: True
|
||||
key: "f4"
|
@ -1,142 +0,0 @@
|
||||
# This is an example ~/.gertty.yaml file for use with OpenStack's
|
||||
# Gerrit. Most of these options are not required, rather, they
|
||||
# customize Gertty to better deal with the particulars of OpenStack's
|
||||
# Gerrit configuration.
|
||||
|
||||
# This file does not list all of the available options. For a full
|
||||
# list with explanations, see the 'reference-gertty.yaml' file.
|
||||
|
||||
servers:
|
||||
- name: openstack
|
||||
url: https://review.openstack.org/
|
||||
# Your gerrit username.
|
||||
username: CHANGEME
|
||||
# Set password at https://review.openstack.org/#/settings/http-password
|
||||
# Note this is not your launchpad password.
|
||||
password: CHANGEME
|
||||
git-root: ~/git/
|
||||
|
||||
# This section adds the colors that we will reference later in the
|
||||
# commentlinks section for test results. You can also change other
|
||||
# colors here.
|
||||
palettes:
|
||||
- name: default
|
||||
test-SUCCESS: ['light green', '']
|
||||
test-FAILURE: ['light red', '']
|
||||
test-UNSTABLE: ['yellow', '']
|
||||
|
||||
# Uncomment the next line if your terminal has a white background
|
||||
# palette: light
|
||||
|
||||
# Commentlinks are regexes that are applied to commit and review
|
||||
# messages. They can be replaced with internal or external links, or
|
||||
# have colors applied.
|
||||
commentlinks:
|
||||
# This matches the job results left by Zuul.
|
||||
- match: "^- (?P<job>.*?) (?P<url>.*?) : (?P<result>[^ ]+) ?(?P<comment>.*)$"
|
||||
# This indicates that this is a test result, and should be indexed
|
||||
# using the "job" match group from the commentlink regex. Gertty
|
||||
# displays test results in their own area of the screen.
|
||||
test-result: "{job}"
|
||||
replacements:
|
||||
# Replace the matching text with a hyperlink to the "url" match
|
||||
# group whose text is the "job" match group.
|
||||
- link:
|
||||
text: "{job:<42}"
|
||||
url: "{url}"
|
||||
# Follow that with the plain text of the "result" match group
|
||||
# with the color "test-{result}" applied. See the palette
|
||||
# section above.
|
||||
- text:
|
||||
color: "test-{result}"
|
||||
text: "{result} "
|
||||
# And then follow that with the plain text of the "comment"
|
||||
# match group.
|
||||
- text: "{comment}"
|
||||
# Match Gerrit change ids, and replace them with a link to an
|
||||
# internal Gertty search for that change id.
|
||||
- match: "(?P<id>I[0-9a-fA-F]{40})"
|
||||
replacements:
|
||||
- search:
|
||||
text: "{id}"
|
||||
query: "change:{id}"
|
||||
# Match external references to bugs on Launchpad
|
||||
- match: "(?P<bug_str>(?:[Cc]loses|[Pp]artial|[Rr]elated)-[Bb]ug *: *#?(?P<bug_id>\\d+))"
|
||||
replacements:
|
||||
- link:
|
||||
text: "{bug_str}"
|
||||
url: "https://launchpad.net/bugs/{bug_id}"
|
||||
# Match external references to blueprints on Launchpad
|
||||
- match: "blueprint +(?P<blueprint>[\\w\\-.]+)"
|
||||
replacements:
|
||||
- link:
|
||||
text: "blueprint {blueprint}"
|
||||
url: "https://blueprints.launchpad.net/openstack/?searchtext={blueprint}"
|
||||
|
||||
# This is the query used for the list of changes when a project is
|
||||
# selected. The default is "status:open". If you don't want to see
|
||||
# changes which are WIP or have verification failures, use a query like this:
|
||||
# change-list-query: "status:open not label:Workflow=-1"
|
||||
|
||||
# If you also want to exclude reviews with failed tests, the query is slightly
|
||||
# more complex:
|
||||
# "status:open not (label:Workflow=-1 or label:Verified=-1)"
|
||||
|
||||
# Uncomment the following line to use a unified diff view instead of the
|
||||
# default side-by-side:
|
||||
# diff-view: unified
|
||||
|
||||
# Hide comments by default that match the following criteria.
|
||||
# You can toggle their display with 't'.
|
||||
hide-comments:
|
||||
- author: "^(.*CI|Jenkins)$"
|
||||
|
||||
# This section defines customized dashboards. You can supply any
|
||||
# Gertty search string and bind them to any key. They will appear in
|
||||
# the global help text, and pressing the key anywhere in Gertty will
|
||||
# discard the current display stack and replace it with the results of
|
||||
# the query.
|
||||
#
|
||||
# NB: "recentlyseen:24 hours" does not just return changes seen in the
|
||||
# last 24 hours -- it returns changes seen within 24 hours of the most
|
||||
# recently seen change. So you can take the weekend off and pick up
|
||||
# where you were.
|
||||
dashboards:
|
||||
- name: "My changes"
|
||||
query: "owner:self status:open"
|
||||
key: "f2"
|
||||
- name: "Incoming reviews"
|
||||
query: "is:open is:reviewer"
|
||||
key: "f3"
|
||||
- name: "Starred changes"
|
||||
query: "is:starred"
|
||||
key: "f4"
|
||||
- name: "Recently seen changes"
|
||||
query: "recentlyseen:24 hours"
|
||||
sort-by: "last-seen"
|
||||
reverse: True
|
||||
key: "f5"
|
||||
|
||||
# Reviewkeys are hotkeys that perform immediate reviews within the
|
||||
# change screen. Any pending comments or review messages will be
|
||||
# attached to the review; otherwise an empty review will be left. The
|
||||
# approvals list is exhaustive, so if you specify an empty list,
|
||||
# Gertty will submit a review that clears any previous approvals.
|
||||
# They will appear in the help text for the change screen.
|
||||
reviewkeys:
|
||||
- key: 'meta 0'
|
||||
approvals: []
|
||||
- key: 'meta 1'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 1
|
||||
- key: 'meta 2'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 2
|
||||
- key: 'meta 3'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 2
|
||||
- category: 'Workflow'
|
||||
value: 1
|
187
examples/reference-boartty.yaml
Normal file
187
examples/reference-boartty.yaml
Normal file
@ -0,0 +1,187 @@
|
||||
# This is an example ~/.boartty.yaml with an exhaustive listing of
|
||||
# options with commentary.
|
||||
|
||||
# This section lists the servers that Boartty can talk to. Multiple
|
||||
# servers may be listed; by default, Boartty will use the first one
|
||||
# listed. To select another, simply specify its name on the command
|
||||
# line.
|
||||
servers:
|
||||
- name: CHANGEME
|
||||
url: https://CHANGEME.example.org/
|
||||
# Your authentication token for Storyboard. Go to the "Profile"
|
||||
# and then "Tokens" (the "key" icon) page in the Storyboard web
|
||||
# interface and create a token. Give it a sufficiently long
|
||||
# validity period (e.g., one decade), and copy and paste the value
|
||||
# here.
|
||||
token: CHANGEME
|
||||
lock-file: ~/.boartty.CHANGEME.lock
|
||||
|
||||
# Each server section can have the following fields:
|
||||
# A name that describes the server, to reference on the command line. [required]
|
||||
# - name: sample
|
||||
# The URL of the Storyboard server. HTTPS should be preferred. [required]
|
||||
# url: https://example.org/
|
||||
# Your authentication token for Storyboard. Go to the "Profile" and
|
||||
# then "Tokens" (the "key" icon) page in the Storyboard web interface
|
||||
# and create a token. Give it a sufficiently long validity period
|
||||
# (e.g., one decade), and copy and paste the value here.
|
||||
# token: CHANGEME
|
||||
# The location of Boartty's sqlite database. If you have more than one
|
||||
# server, you should specify a dburi for any additional servers.
|
||||
# By default a SQLite database called ~/.boartty.db is used.
|
||||
# dburi: sqlite:////home/user/.boartty.db
|
||||
# If your Storyboard server uses a non-standard certificate chain (e.g. on a test
|
||||
# server), you can pass a full path to a bundle of CA certificates here:
|
||||
# ssl-ca-path: ~/.pki/ca-chain.pem
|
||||
# In case you do not care about security and want to use a sledgehammer
|
||||
# approach to SSL, you can set this value to false to turn off certificate
|
||||
# validation.
|
||||
# verify-ssl: true
|
||||
# By default Boartty logs errors to a file and truncates that file each
|
||||
# time it starts (so that it does not grow without bound). If you
|
||||
# would like to log to a different location, you may specify it here.
|
||||
# log-file: ~/.boartty.log
|
||||
# Boartty listens on a unix domain socket for remote commands at
|
||||
# ~/.boartty.sock. You may change the path here:
|
||||
# socket: ~/.boartty.sock
|
||||
# Boartty uses a lock file per server to prevent multiple processes
|
||||
# from running at the same time. Example:
|
||||
# lock-file: /run/lockme.lock
|
||||
|
||||
# Boartty comes with two palettes defined internally. The default
|
||||
# palette is suitable for use on a terminal with a dark background.
|
||||
# The "light" palette is for a terminal with a white or light
|
||||
# background. You may customize the colors in either of those
|
||||
# palettes, or define your own palette.
|
||||
|
||||
# The following alters two colors in the default palette, one color in
|
||||
# the light palette, and one color in a custom palette. If any color
|
||||
# is not defined in a palette, the value from the default palette is
|
||||
# used. The values are a list of at least two elements describing the
|
||||
# colors to be use for the foreground and background colors.
|
||||
# Additional elements can specify (in order) the color to use for
|
||||
# monochrome terminals, the foreground, and background colors to use
|
||||
# in high-color terminals.
|
||||
|
||||
# For a reference of possible color names, see:
|
||||
# http://urwid.org/manual/displayattributes.html#foreground-and-background-settings
|
||||
# To see the list of possible palette entries, run "boartty --print-palette".
|
||||
|
||||
palettes:
|
||||
- name: default
|
||||
task-title: ['light green', '']
|
||||
task-id: ['dark cyan', '']
|
||||
- name: light
|
||||
task-project: ['dark blue', '']
|
||||
- name: custom
|
||||
task-project: ['dark red', '']
|
||||
|
||||
# Palettes may be selected at runtime with the "-p PALETTE" command
|
||||
# line option, or you may set the default palette here:
|
||||
# palette: light
|
||||
|
||||
# Keymaps work the same way as palettes. Two keymaps are defined
|
||||
# internally, the 'default' keymap and the 'vi' keymap. Individual
|
||||
# keys may be overridden and custom keymaps defined and selected in
|
||||
# the config file or the command line.
|
||||
|
||||
# Each keymap contains a mapping of command -> key(s). If a command
|
||||
# is not specified, Boartty will use the keybinding specified in the
|
||||
# default map. More than one key can be bound to a command.
|
||||
|
||||
# Run "boartty --print-keymap" for a list of commands that can be
|
||||
# bound.
|
||||
|
||||
keymaps:
|
||||
- name: default
|
||||
leave-comment: 'r'
|
||||
- name: custom
|
||||
leave-comment: ['r', 'R']
|
||||
- name: osx # OS X blocks ctrl+o
|
||||
story-search: 'ctrl s'
|
||||
# To specify a sequence of keys, they must be a list of keystrokes
|
||||
# within a list of key series. For example:
|
||||
- name: vi
|
||||
quit: [[':', 'q']]
|
||||
|
||||
# The default keymap may be selected with the '-k KEYMAP' command line
|
||||
# option, or with the following line:
|
||||
# keymap: custom
|
||||
|
||||
# Commentlinks are regular expressions that are applied to commit and
|
||||
# review messages. They can be replaced with internal or external
|
||||
# links, or have colors applied.
|
||||
commentlinks:
|
||||
# This example matches story numbers, and replaces them with a link to
|
||||
# an internal Boartty search for that story.
|
||||
- match: "(?P<id>[0-9]+)"
|
||||
replacements:
|
||||
- search:
|
||||
text: "{id}"
|
||||
query: "story:{id}"
|
||||
# Any number of commentlink entries may be specified. Start each with
|
||||
# a "match" key and regex. Named match groups within the regex may be
|
||||
# used in the replacements section. Any number of replacements may be
|
||||
# specified. The types of replacement available are:
|
||||
#
|
||||
# Text: Plain text whose color may be specified. The color references
|
||||
# a palette entry.
|
||||
# - text:
|
||||
# color: ""
|
||||
# text: ""
|
||||
# Link: A hyperlink with the indicated text that when activated will
|
||||
# open the user's browser with the supplied URL
|
||||
# - link:
|
||||
# text: ""
|
||||
# url: ""
|
||||
# Search: A hyperlink that will perform a Boartty search when
|
||||
# activated.
|
||||
# - search:
|
||||
# text: "{id}"
|
||||
# query: "change:{id}"
|
||||
|
||||
# This is the query used for the list of stories when a project is
|
||||
# selected. The default is empty.
|
||||
# story-list-query: ""
|
||||
|
||||
# This section defines default sorting options for the story
|
||||
# list. The "sort-by" key specifies the sort order, which can be
|
||||
# 'number', 'updated', or 'last-seen'. The 'reverse' key specifies
|
||||
# ascending (true) or descending (false) order.
|
||||
# story-list-options:
|
||||
# sort-by: 'number'
|
||||
# reverse: false
|
||||
|
||||
# Uncomment the following line to disable the navigation breadcrumbs
|
||||
# at the bottom of the screen:
|
||||
# breadcrumbs: false
|
||||
|
||||
# Times are displayed in the local timezone by default. To display
|
||||
# them in UTC instead, uncomment the following line:
|
||||
# display-times-in-utc: true
|
||||
|
||||
# Boartty handles mouse input by default. If you don't want it messing
|
||||
# with your terminal's mouse handling, uncomment the following line:
|
||||
# handle-mouse: false
|
||||
|
||||
# This section defines customized dashboards. You can supply any
|
||||
# Boartty search string and bind them to any key. They will appear in
|
||||
# the global help text, and pressing the key anywhere in Boartty will
|
||||
# run the query and display the results.
|
||||
#
|
||||
# NB: "recentlyseen:24 hours" does not just return stories seen in the
|
||||
# last 24 hours -- it returns stories seen within 24 hours of the most
|
||||
# recently seen change. So you can take the weekend off and pick up
|
||||
# where you were.
|
||||
dashboards:
|
||||
- name: "My stories"
|
||||
query: "creator:self status:active"
|
||||
key: "f2"
|
||||
- name: "Starred stories"
|
||||
query: "is:starred"
|
||||
key: "f3"
|
||||
- name: "Recently seen stories"
|
||||
query: "recentlyseen:24 hours"
|
||||
sort-by: "last-seen"
|
||||
reverse: True
|
||||
key: "f4"
|
@ -1,248 +0,0 @@
|
||||
# This is an example ~/.gertty.yaml with an exhaustive listing of
|
||||
# options with commentary.
|
||||
|
||||
# This section lists the servers that Gertty can talk to. Multiple
|
||||
# servers may be listed; by default, Gertty will use the first one
|
||||
# listed. To select another, simply specify its name on the command
|
||||
# line.
|
||||
servers:
|
||||
- name: CHANGEME
|
||||
url: https://CHANGEME.example.org/
|
||||
username: CHANGEME
|
||||
# Your HTTP Password for gerrit. Go to the "HTTP Password" section in your
|
||||
# account settings to generate/retrieve this password.
|
||||
password: CHANGEME
|
||||
git-root: ~/git/
|
||||
lock-file: ~/.gertty.CHANGEME.lock
|
||||
|
||||
# Each server section can have the following fields:
|
||||
# A name that describes the server, to reference on the command line. [required]
|
||||
# - name: sample
|
||||
# The URL of the Gerrit server. HTTPS should be preferred. [required]
|
||||
# url: https://example.org/
|
||||
# Your username in Gerrit. [required]
|
||||
# username: CHANGEME
|
||||
# Your password in Gerrit (Settings -> HTTP Password). [required]
|
||||
# password: CHANGEME
|
||||
# Authentication type required by the Gerrit server. Can be 'basic',
|
||||
# 'digest', or 'form'. Defaults to 'digest'.
|
||||
# auth-type: digest
|
||||
# A location where Gertty should store its git repositories. These
|
||||
# can be the same git repositories where you do your own work --
|
||||
# Gertty will not modify them unless you tell it to, and even then the
|
||||
# normal git protections against losing work remain in place. [required]
|
||||
# git-root: ~/git/
|
||||
# The URL to clone git repos. By default, <url>/p/<project> is used. For a list
|
||||
# of valid URLs, see:
|
||||
# https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
|
||||
# git-url: ssh://user@example.org:29418
|
||||
# The location of Gertty's sqlite database. If you have more than one
|
||||
# server, you should specify a dburi for any additional servers.
|
||||
# By default a SQLite database called ~/.gertty.db is used.
|
||||
# dburi: sqlite:////home/user/.gertty.db
|
||||
# If your Gerrit server uses a non-standard certificate chain (e.g. on a test
|
||||
# server), you can pass a full path to a bundle of CA certificates here:
|
||||
# ssl-ca-path: ~/.pki/ca-chain.pem
|
||||
# In case you do not care about security and want to use a sledgehammer
|
||||
# approach to SSL, you can set this value to false to turn off certificate
|
||||
# validation.
|
||||
# verify-ssl: true
|
||||
# By default Gertty logs errors to a file and truncates that file each
|
||||
# time it starts (so that it does not grow without bound). If you
|
||||
# would like to log to a different location, you may specify it here.
|
||||
# log-file: ~/.gertty.log
|
||||
# Gertty listens on a unix domain socket for remote commands at
|
||||
# ~/.gertty.sock. You may change the path here:
|
||||
# socket: ~/.gertty.sock
|
||||
# Gertty uses a lock file per server to prevent multiple processes
|
||||
# from running at the same time. Example:
|
||||
# lock-file: /run/lockme.lock
|
||||
|
||||
# Gertty comes with two palettes defined internally. The default
|
||||
# palette is suitable for use on a terminal with a dark background.
|
||||
# The "light" palette is for a terminal with a white or light
|
||||
# background. You may customize the colors in either of those
|
||||
# palettes, or define your own palette.
|
||||
|
||||
# The following alters two colors in the default palette, one color in
|
||||
# the light palette, and one color in a custom palette. If any color
|
||||
# is not defined in a palette, the value from the default palette is
|
||||
# used. The values are a list of at least two elements describing the
|
||||
# colors to be use for the foreground and background colors.
|
||||
# Additional elements can specify (in order) the color to use for
|
||||
# monochrome terminals, the foreground, and background colors to use
|
||||
# in high-color terminals.
|
||||
|
||||
# For a reference of possible color names, see:
|
||||
# http://urwid.org/manual/displayattributes.html#foreground-and-background-settings
|
||||
# To see the list of possible palette entries, run "gertty --print-palette".
|
||||
|
||||
palettes:
|
||||
- name: default
|
||||
added-line: ['dark green', '']
|
||||
added-word: ['light green', '']
|
||||
- name: light
|
||||
filename: ['dark cyan', '']
|
||||
- name: custom
|
||||
filename: ['light yellow', '']
|
||||
|
||||
# Palettes may be selected at runtime with the "-p PALETTE" command
|
||||
# line option, or you may set the default palette here:
|
||||
# palette: light
|
||||
|
||||
# Keymaps work the same way as palettes. Two keymaps are defined
|
||||
# internally, the 'default' keymap and the 'vi' keymap. Individual
|
||||
# keys may be overridden and custom keymaps defined and selected in
|
||||
# the config file or the command line.
|
||||
|
||||
# Each keymap contains a mapping of command -> key(s). If a command
|
||||
# is not specified, Gertty will use the keybinding specified in the
|
||||
# default map. More than one key can be bound to a command.
|
||||
|
||||
# Run "gertty --print-keymap" for a list of commands that can be
|
||||
# bound.
|
||||
|
||||
keymaps:
|
||||
- name: default
|
||||
diff: 'd'
|
||||
- name: custom
|
||||
review: ['r', 'R']
|
||||
- name: osx # OS X blocks ctrl+o
|
||||
change-search: 'ctrl s'
|
||||
# To specify a sequence of keys, they must be a list of keystrokes
|
||||
# within a list of key series. For example:
|
||||
- name: vi
|
||||
quit: [[':', 'q']]
|
||||
|
||||
# The default keymap may be selected with the '-k KEYMAP' command line
|
||||
# option, or with the following line:
|
||||
# keymap: custom
|
||||
|
||||
# Commentlinks are regular expressions that are applied to commit and
|
||||
# review messages. They can be replaced with internal or external
|
||||
# links, or have colors applied.
|
||||
commentlinks:
|
||||
# Match Gerrit change ids, and replace them with a link to an internal
|
||||
# Gertty search for that change id.
|
||||
- match: "(?P<id>I[0-9a-fA-F]{40})"
|
||||
replacements:
|
||||
- search:
|
||||
text: "{id}"
|
||||
query: "change:{id}"
|
||||
# Any number of commentlink entries may be specified. Start each with
|
||||
# a "match" key and regex. Named match groups within the regex may be
|
||||
# used in the replacements section. Any number of replacements may be
|
||||
# specified. The types of replacement available are:
|
||||
#
|
||||
# Text: Plain text whose color may be specified. The color references
|
||||
# a palette entry.
|
||||
# - text:
|
||||
# color: ""
|
||||
# text: ""
|
||||
# Link: A hyperlink with the indicated text that when activated will
|
||||
# open the user's browser with the supplied URL
|
||||
# - link:
|
||||
# text: ""
|
||||
# url: ""
|
||||
# Search: A hyperlink that will perform a Gertty search when
|
||||
# activated.
|
||||
# - search:
|
||||
# text: "{id}"
|
||||
# query: "change:{id}"
|
||||
|
||||
# This is the query used for the list of changes when a project is
|
||||
# selected. The default is "status:open".
|
||||
# change-list-query: "status:open"
|
||||
|
||||
# This section defines default sorting options for the change
|
||||
# list. The "sort-by" key specifies the sort order, which can be
|
||||
# 'number', 'updated', or 'last-seen'. The 'reverse' key specifies
|
||||
# ascending (true) or descending (false) order.
|
||||
# change-list-options:
|
||||
# sort-by: 'number'
|
||||
# reverse: false
|
||||
|
||||
# Uncomment the following line to disable the navigation breadcrumbs
|
||||
# at the bottom of the screen:
|
||||
# breadcrumbs: false
|
||||
|
||||
# Uncomment the following line to use a unified diff view instead
|
||||
# of the default side-by-side:
|
||||
# diff-view: unified
|
||||
|
||||
# Dependent changes are displayed as "threads" in the change list by
|
||||
# default. To disable this behavior, uncomment the following line:
|
||||
# thread-changes: false
|
||||
|
||||
# Times are displayed in the local timezone by default. To display
|
||||
# them in UTC instead, uncomment the following line:
|
||||
# display-times-in-utc: true
|
||||
|
||||
# Gertty handles mouse input by default. If you don't want it messing
|
||||
# with your terminal's mouse handling, uncomment the following line:
|
||||
# handle-mouse: false
|
||||
|
||||
# Closed changes that are older than two months are removed from the
|
||||
# local database (and their refs are removed from the local git repos
|
||||
# so that git may garbage collect them). If you would like to change
|
||||
# the expiration delay or disable it, uncomment the following line.
|
||||
# The time interval is specified in the same way as the "age:" term in
|
||||
# Gerrit's search syntax. To disable it altogether, set the value to
|
||||
# the empty string.
|
||||
# expire-age: '2 months'
|
||||
|
||||
# Uncomment the following lines to Hide comments by default that match
|
||||
# certain criteria. You can toggle their display with 't'. Currently
|
||||
# the only supported criterion is "author".
|
||||
# hide-comments:
|
||||
# - author: "^(.*CI|Jenkins)$"
|
||||
|
||||
# This section defines customized dashboards. You can supply any
|
||||
# Gertty search string and bind them to any key. They will appear in
|
||||
# the global help text, and pressing the key anywhere in Gertty will
|
||||
# run the query and display the results.
|
||||
#
|
||||
# NB: "recentlyseen:24 hours" does not just return changes seen in the
|
||||
# last 24 hours -- it returns changes seen within 24 hours of the most
|
||||
# recently seen change. So you can take the weekend off and pick up
|
||||
# where you were.
|
||||
dashboards:
|
||||
- name: "My changes"
|
||||
query: "owner:self status:open"
|
||||
key: "f2"
|
||||
- name: "Incoming reviews"
|
||||
query: "is:open is:reviewer"
|
||||
key: "f3"
|
||||
- name: "Starred changes"
|
||||
query: "is:starred"
|
||||
key: "f4"
|
||||
- name: "Recently seen changes"
|
||||
query: "recentlyseen:24 hours"
|
||||
sort-by: "last-seen"
|
||||
reverse: True
|
||||
key: "f5"
|
||||
|
||||
# Reviewkeys are hotkeys that perform immediate reviews within the
|
||||
# change screen. Any pending comments or review messages will be
|
||||
# attached to the review; otherwise an empty review will be left. The
|
||||
# approvals list is exhaustive, so if you specify an empty list,
|
||||
# Gertty will submit a review that clears any previous approvals. To
|
||||
# submit the change with the review, include 'submit: True' with the
|
||||
# reviewkey. Reviewkeys appear in the help text for the change
|
||||
# screen.
|
||||
reviewkeys:
|
||||
- key: 'meta 0'
|
||||
approvals: []
|
||||
- key: 'meta 1'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 1
|
||||
- key: 'meta 2'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 2
|
||||
- key: 'meta 3'
|
||||
approvals:
|
||||
- category: 'Code-Review'
|
||||
value: 2
|
||||
submit: True
|
@ -1,26 +0,0 @@
|
||||
"""add query sync table
|
||||
|
||||
Revision ID: 1bb187bcd401
|
||||
Revises: 3cc7e3753dc3
|
||||
Create Date: 2015-03-26 07:32:33.584657
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1bb187bcd401'
|
||||
down_revision = '3cc7e3753dc3'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('sync_query',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(255), index=True, unique=True, nullable=False),
|
||||
sa.Column('updated', sa.DateTime, index=True),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,22 +0,0 @@
|
||||
"""add revision indexes
|
||||
|
||||
Revision ID: 1cdd4e2e74c
|
||||
Revises: 4a802b741d2f
|
||||
Create Date: 2015-03-10 16:17:41.330825
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1cdd4e2e74c'
|
||||
down_revision = '4a802b741d2f'
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_index(op.f('ix_revision_commit'), 'revision', ['commit'])
|
||||
op.create_index(op.f('ix_revision_parent'), 'revision', ['parent'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,64 +0,0 @@
|
||||
"""attach comments to files
|
||||
|
||||
Revision ID: 254ac5fc3941
|
||||
Revises: 50344aecd1c2
|
||||
Create Date: 2015-04-13 15:52:07.104397
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '254ac5fc3941'
|
||||
down_revision = '50344aecd1c2'
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns, sqlite_drop_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('comment', sa.Column('file_key', sa.Integer()))
|
||||
sqlite_alter_columns('comment', [
|
||||
sa.Column('file_key', sa.Integer(), sa.ForeignKey('file.key'))
|
||||
])
|
||||
|
||||
update_query = sa.text('update comment set file_key=:file_key where key=:key')
|
||||
file_query = sa.text('select f.key from file f where f.revision_key=:revision_key and f.path=:path')
|
||||
|
||||
file_insert_query = sa.text('insert into file (key, revision_key, path, old_path, status, inserted, deleted) '
|
||||
' values (NULL, :revision_key, :path, NULL, NULL, NULL, NULL)')
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
countres = conn.execute('select count(*) from comment')
|
||||
comments = countres.fetchone()[0]
|
||||
|
||||
comment_res = conn.execute('select p.name, c.number, c.status, r.key, r.number, m.file, m.key '
|
||||
'from project p, change c, revision r, comment m '
|
||||
'where m.revision_key=r.key and r.change_key=c.key and '
|
||||
'c.project_key=p.key order by p.name')
|
||||
|
||||
count = 0
|
||||
for (pname, cnumber, cstatus, rkey, rnumber, mfile, mkey) in comment_res.fetchall():
|
||||
count += 1
|
||||
sys.stdout.write('Comment %s / %s\r' % (count, comments))
|
||||
sys.stdout.flush()
|
||||
|
||||
file_res = conn.execute(file_query, revision_key=rkey, path=mfile)
|
||||
file_key = file_res.fetchone()
|
||||
if not file_key:
|
||||
conn.execute(file_insert_query, revision_key=rkey, path=mfile)
|
||||
file_res = conn.execute(file_query, revision_key=rkey, path=mfile)
|
||||
file_key = file_res.fetchone()
|
||||
fkey = file_key[0]
|
||||
file_res = conn.execute(update_query, file_key=fkey, key=mkey)
|
||||
sqlite_drop_columns('comment', ['revision_key', 'file'])
|
||||
print
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,25 +0,0 @@
|
||||
"""fix account table
|
||||
|
||||
Revision ID: 2a11dd14665
|
||||
Revises: 4cc9c46f9d8b
|
||||
Create Date: 2014-08-20 13:07:25.079603
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2a11dd14665'
|
||||
down_revision = '4cc9c46f9d8b'
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.drop_index('ix_account_name', 'account')
|
||||
op.drop_index('ix_account_username', 'account')
|
||||
op.drop_index('ix_account_email', 'account')
|
||||
op.create_index(op.f('ix_account_name'), 'account', ['name'])
|
||||
op.create_index(op.f('ix_account_username'), 'account', ['username'])
|
||||
op.create_index(op.f('ix_account_email'), 'account', ['email'])
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,36 +0,0 @@
|
||||
"""add can_submit column
|
||||
|
||||
Revision ID: 312cd5a9f878
|
||||
Revises: 46b175bfa277
|
||||
Create Date: 2014-09-18 16:37:13.149729
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '312cd5a9f878'
|
||||
down_revision = '46b175bfa277'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('revision', sa.Column('can_submit', sa.Boolean()))
|
||||
|
||||
conn = op.get_bind()
|
||||
q = sa.text('update revision set can_submit=:submit')
|
||||
conn.execute(q, submit=False)
|
||||
|
||||
sqlite_alter_columns('revision', [
|
||||
sa.Column('can_submit', sa.Boolean(), nullable=False),
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,27 +0,0 @@
|
||||
"""add conflicts table
|
||||
|
||||
Revision ID: 3610c2543e07
|
||||
Revises: 4388de50824a
|
||||
Create Date: 2016-02-05 16:43:20.047238
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3610c2543e07'
|
||||
down_revision = '4388de50824a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('change_conflict',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('change1_key', sa.Integer(), sa.ForeignKey('change.key'), index=True),
|
||||
sa.Column('change2_key', sa.Integer(), sa.ForeignKey('change.key'), index=True),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,26 +0,0 @@
|
||||
"""add last_seen column to change
|
||||
|
||||
Revision ID: 37a702b7f58e
|
||||
Revises: 3610c2543e07
|
||||
Create Date: 2016-02-06 09:09:38.728225
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '37a702b7f58e'
|
||||
down_revision = '3610c2543e07'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('change', sa.Column('last_seen', sa.DateTime, index=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,33 +0,0 @@
|
||||
"""Added project updated column
|
||||
|
||||
Revision ID: 38104b4c1b84
|
||||
Revises: 56e48a4a064a
|
||||
Create Date: 2014-05-31 06:52:12.452205
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '38104b4c1b84'
|
||||
down_revision = '56e48a4a064a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('project', sa.Column('updated', sa.DateTime))
|
||||
|
||||
conn = op.get_bind()
|
||||
res = conn.execute('select "key", name from project')
|
||||
for (key, name) in res.fetchall():
|
||||
q = sa.text("select max(updated) from change where project_key=:key")
|
||||
res = conn.execute(q, key=key)
|
||||
for (updated,) in res.fetchall():
|
||||
q = sa.text('update project set updated=:updated where "key"=:key')
|
||||
conn.execute(q, key=key, updated=updated)
|
||||
|
||||
op.create_index(op.f('ix_project_updated'), 'project', ['updated'], unique=False)
|
||||
|
||||
def downgrade():
|
||||
op.drop_index(op.f('ix_project_updated'), table_name='project')
|
||||
op.drop_column('project', 'updated')
|
@ -1,37 +0,0 @@
|
||||
"""add held
|
||||
|
||||
Revision ID: 3cc7e3753dc3
|
||||
Revises: 1cdd4e2e74c
|
||||
Create Date: 2015-03-22 08:48:15.516289
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3cc7e3753dc3'
|
||||
down_revision = '1cdd4e2e74c'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('change', sa.Column('held', sa.Boolean()))
|
||||
|
||||
connection = op.get_bind()
|
||||
change = sa.sql.table('change',
|
||||
sa.sql.column('held', sa.Boolean()))
|
||||
connection.execute(change.update().values({'held':False}))
|
||||
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('held', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,49 +0,0 @@
|
||||
"""add draft fields
|
||||
|
||||
Revision ID: 3d429503a29a
|
||||
Revises: 2a11dd14665
|
||||
Create Date: 2014-08-30 13:26:03.698902
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3d429503a29a'
|
||||
down_revision = '2a11dd14665'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns, sqlite_drop_columns
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('message', sa.Column('draft', sa.Boolean()))
|
||||
op.add_column('comment', sa.Column('draft', sa.Boolean()))
|
||||
op.add_column('approval', sa.Column('draft', sa.Boolean()))
|
||||
|
||||
conn = op.get_bind()
|
||||
conn.execute("update message set draft=pending")
|
||||
conn.execute("update comment set draft=pending")
|
||||
conn.execute("update approval set draft=pending")
|
||||
|
||||
sqlite_alter_columns('message', [
|
||||
sa.Column('draft', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
sqlite_alter_columns('comment', [
|
||||
sa.Column('draft', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
sqlite_alter_columns('approval', [
|
||||
sa.Column('draft', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
sqlite_drop_columns('comment', ['pending'])
|
||||
sqlite_drop_columns('approval', ['pending'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,35 +0,0 @@
|
||||
"""add topic table
|
||||
|
||||
Revision ID: 4388de50824a
|
||||
Revises: 254ac5fc3941
|
||||
Create Date: 2015-10-31 19:06:38.538948
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4388de50824a'
|
||||
down_revision = '254ac5fc3941'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('topic',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), index=True, nullable=False),
|
||||
sa.Column('sequence', sa.Integer(), index=True, unique=True, nullable=False),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
|
||||
op.create_table('project_topic',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('project_key', sa.Integer(), sa.ForeignKey('project.key'), index=True),
|
||||
sa.Column('topic_key', sa.Integer(), sa.ForeignKey('topic.key'), index=True),
|
||||
sa.Column('sequence', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('key'),
|
||||
sa.UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
|
||||
)
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,176 +0,0 @@
|
||||
"""Initial schema
|
||||
|
||||
Revision ID: 44402069e137
|
||||
Revises: None
|
||||
Create Date: 2014-05-04 17:10:23.127702
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '44402069e137'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('project',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('subscribed', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_project_name'), 'project', ['name'], unique=True)
|
||||
op.create_index(op.f('ix_project_subscribed'), 'project', ['subscribed'], unique=False)
|
||||
op.create_table('change',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('project_key', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.String(length=255), nullable=False),
|
||||
sa.Column('number', sa.Integer(), nullable=False),
|
||||
sa.Column('branch', sa.String(length=255), nullable=False),
|
||||
sa.Column('change_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('topic', sa.String(length=255), nullable=True),
|
||||
sa.Column('owner', sa.String(length=255), nullable=True),
|
||||
sa.Column('subject', sa.Text(), nullable=False),
|
||||
sa.Column('created', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated', sa.DateTime(), nullable=False),
|
||||
sa.Column('status', sa.String(length=8), nullable=False),
|
||||
sa.Column('hidden', sa.Boolean(), nullable=False),
|
||||
sa.Column('reviewed', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_change_branch'), 'change', ['branch'], unique=False)
|
||||
op.create_index(op.f('ix_change_change_id'), 'change', ['change_id'], unique=False)
|
||||
op.create_index(op.f('ix_change_created'), 'change', ['created'], unique=False)
|
||||
op.create_index(op.f('ix_change_hidden'), 'change', ['hidden'], unique=False)
|
||||
op.create_index(op.f('ix_change_id'), 'change', ['id'], unique=True)
|
||||
op.create_index(op.f('ix_change_number'), 'change', ['number'], unique=True)
|
||||
op.create_index(op.f('ix_change_owner'), 'change', ['owner'], unique=False)
|
||||
op.create_index(op.f('ix_change_project_key'), 'change', ['project_key'], unique=False)
|
||||
op.create_index(op.f('ix_change_reviewed'), 'change', ['reviewed'], unique=False)
|
||||
op.create_index(op.f('ix_change_status'), 'change', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_change_topic'), 'change', ['topic'], unique=False)
|
||||
op.create_index(op.f('ix_change_updated'), 'change', ['updated'], unique=False)
|
||||
op.create_table('approval',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('change_key', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('category', sa.String(length=255), nullable=False),
|
||||
sa.Column('value', sa.Integer(), nullable=False),
|
||||
sa.Column('pending', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_approval_change_key'), 'approval', ['change_key'], unique=False)
|
||||
op.create_index(op.f('ix_approval_pending'), 'approval', ['pending'], unique=False)
|
||||
op.create_table('revision',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('change_key', sa.Integer(), nullable=True),
|
||||
sa.Column('number', sa.Integer(), nullable=False),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('commit', sa.String(length=255), nullable=False),
|
||||
sa.Column('parent', sa.String(length=255), nullable=False),
|
||||
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_revision_change_key'), 'revision', ['change_key'], unique=False)
|
||||
op.create_index(op.f('ix_revision_number'), 'revision', ['number'], unique=False)
|
||||
op.create_table('label',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('change_key', sa.Integer(), nullable=True),
|
||||
sa.Column('category', sa.String(length=255), nullable=False),
|
||||
sa.Column('value', sa.Integer(), nullable=False),
|
||||
sa.Column('description', sa.String(length=255), nullable=False),
|
||||
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_label_change_key'), 'label', ['change_key'], unique=False)
|
||||
op.create_table('permitted_label',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('change_key', sa.Integer(), nullable=True),
|
||||
sa.Column('category', sa.String(length=255), nullable=False),
|
||||
sa.Column('value', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_permitted_label_change_key'), 'permitted_label', ['change_key'], unique=False)
|
||||
op.create_table('comment',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('revision_key', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.String(length=255), nullable=True),
|
||||
sa.Column('in_reply_to', sa.String(length=255), nullable=True),
|
||||
sa.Column('created', sa.DateTime(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('file', sa.Text(), nullable=False),
|
||||
sa.Column('parent', sa.Boolean(), nullable=False),
|
||||
sa.Column('line', sa.Integer(), nullable=True),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('pending', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['revision_key'], ['revision.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_comment_created'), 'comment', ['created'], unique=False)
|
||||
op.create_index(op.f('ix_comment_id'), 'comment', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_comment_pending'), 'comment', ['pending'], unique=False)
|
||||
op.create_index(op.f('ix_comment_revision_key'), 'comment', ['revision_key'], unique=False)
|
||||
op.create_table('message',
|
||||
sa.Column('key', sa.Integer(), nullable=False, quote=True),
|
||||
sa.Column('revision_key', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.String(length=255), nullable=True),
|
||||
sa.Column('created', sa.DateTime(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('pending', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['revision_key'], ['revision.key'], ),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_index(op.f('ix_message_created'), 'message', ['created'], unique=False)
|
||||
op.create_index(op.f('ix_message_id'), 'message', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_message_pending'), 'message', ['pending'], unique=False)
|
||||
op.create_index(op.f('ix_message_revision_key'), 'message', ['revision_key'], unique=False)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_message_revision_key'), table_name='message')
|
||||
op.drop_index(op.f('ix_message_pending'), table_name='message')
|
||||
op.drop_index(op.f('ix_message_id'), table_name='message')
|
||||
op.drop_index(op.f('ix_message_created'), table_name='message')
|
||||
op.drop_table('message')
|
||||
op.drop_index(op.f('ix_comment_revision_key'), table_name='comment')
|
||||
op.drop_index(op.f('ix_comment_pending'), table_name='comment')
|
||||
op.drop_index(op.f('ix_comment_id'), table_name='comment')
|
||||
op.drop_index(op.f('ix_comment_created'), table_name='comment')
|
||||
op.drop_table('comment')
|
||||
op.drop_index(op.f('ix_permitted_label_change_key'), table_name='permitted_label')
|
||||
op.drop_table('permitted_label')
|
||||
op.drop_index(op.f('ix_label_change_key'), table_name='label')
|
||||
op.drop_table('label')
|
||||
op.drop_index(op.f('ix_revision_number'), table_name='revision')
|
||||
op.drop_index(op.f('ix_revision_change_key'), table_name='revision')
|
||||
op.drop_table('revision')
|
||||
op.drop_index(op.f('ix_approval_pending'), table_name='approval')
|
||||
op.drop_index(op.f('ix_approval_change_key'), table_name='approval')
|
||||
op.drop_table('approval')
|
||||
op.drop_index(op.f('ix_change_updated'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_topic'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_status'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_reviewed'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_project_key'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_owner'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_number'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_id'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_hidden'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_created'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_change_id'), table_name='change')
|
||||
op.drop_index(op.f('ix_change_branch'), table_name='change')
|
||||
op.drop_table('change')
|
||||
op.drop_index(op.f('ix_project_subscribed'), table_name='project')
|
||||
op.drop_index(op.f('ix_project_name'), table_name='project')
|
||||
op.drop_table('project')
|
||||
### end Alembic commands ###
|
@ -1,66 +0,0 @@
|
||||
"""add pending actions
|
||||
|
||||
Revision ID: 46b175bfa277
|
||||
Revises: 3d429503a29a
|
||||
Create Date: 2014-08-31 09:20:11.789330
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '46b175bfa277'
|
||||
down_revision = '3d429503a29a'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('branch',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('project_key', sa.Integer(), sa.ForeignKey('project.key'), index=True, nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
op.create_table('pending_cherry_pick',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('revision_key', sa.Integer(), sa.ForeignKey('revision.key'), index=True, nullable=False),
|
||||
sa.Column('branch', sa.String(length=255), nullable=False),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('change', sa.Column('pending_rebase', sa.Boolean()))
|
||||
op.add_column('change', sa.Column('pending_topic', sa.Boolean()))
|
||||
op.add_column('change', sa.Column('pending_status', sa.Boolean()))
|
||||
op.add_column('change', sa.Column('pending_status_message', sa.Text()))
|
||||
op.add_column('revision', sa.Column('pending_message', sa.Boolean()))
|
||||
|
||||
connection = op.get_bind()
|
||||
change = sa.sql.table('change',
|
||||
sa.sql.column('pending_rebase', sa.Boolean()),
|
||||
sa.sql.column('pending_topic', sa.Boolean()),
|
||||
sa.sql.column('pending_status', sa.Boolean()))
|
||||
connection.execute(change.update().values({'pending_rebase':False,
|
||||
'pending_topic':False,
|
||||
'pending_status':False}))
|
||||
revision = sa.sql.table('revision',
|
||||
sa.sql.column('pending_message', sa.Boolean()))
|
||||
connection.execute(revision.update().values({'pending_message':False}))
|
||||
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('pending_rebase', sa.Boolean(), index=True, nullable=False),
|
||||
sa.Column('pending_topic', sa.Boolean(), index=True, nullable=False),
|
||||
sa.Column('pending_status', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
sqlite_alter_columns('revision', [
|
||||
sa.Column('pending_message', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,41 +0,0 @@
|
||||
"""add starred
|
||||
|
||||
Revision ID: 4a802b741d2f
|
||||
Revises: 312cd5a9f878
|
||||
Create Date: 2015-02-12 18:10:19.187733
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4a802b741d2f'
|
||||
down_revision = '312cd5a9f878'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('change', sa.Column('starred', sa.Boolean()))
|
||||
op.add_column('change', sa.Column('pending_starred', sa.Boolean()))
|
||||
|
||||
connection = op.get_bind()
|
||||
change = sa.sql.table('change',
|
||||
sa.sql.column('starred', sa.Boolean()),
|
||||
sa.sql.column('pending_starred', sa.Boolean()))
|
||||
connection.execute(change.update().values({'starred':False,
|
||||
'pending_starred':False}))
|
||||
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('starred', sa.Boolean(), index=True, nullable=False),
|
||||
sa.Column('pending_starred', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,73 +0,0 @@
|
||||
"""add account table
|
||||
|
||||
Revision ID: 4cc9c46f9d8b
|
||||
Revises: 725816dc500
|
||||
Create Date: 2014-07-23 16:01:47.462597
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4cc9c46f9d8b'
|
||||
down_revision = '725816dc500'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns, sqlite_drop_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
sqlite_drop_columns('message', ['name'])
|
||||
sqlite_drop_columns('comment', ['name'])
|
||||
sqlite_drop_columns('approval', ['name'])
|
||||
sqlite_drop_columns('change', ['owner'])
|
||||
|
||||
op.create_table('account',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), index=True, unique=True, nullable=False),
|
||||
sa.Column('name', sa.String(length=255)),
|
||||
sa.Column('username', sa.String(length=255)),
|
||||
sa.Column('email', sa.String(length=255)),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
|
||||
op.create_index(op.f('ix_account_name'), 'account', ['name'], unique=True)
|
||||
op.create_index(op.f('ix_account_username'), 'account', ['name'], unique=True)
|
||||
op.create_index(op.f('ix_account_email'), 'account', ['name'], unique=True)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('message', sa.Column('account_key', sa.Integer()))
|
||||
op.add_column('comment', sa.Column('account_key', sa.Integer()))
|
||||
op.add_column('approval', sa.Column('account_key', sa.Integer()))
|
||||
op.add_column('change', sa.Column('account_key', sa.Integer()))
|
||||
sqlite_alter_columns('message', [
|
||||
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
|
||||
])
|
||||
sqlite_alter_columns('comment', [
|
||||
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
|
||||
])
|
||||
sqlite_alter_columns('approval', [
|
||||
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
|
||||
])
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
|
||||
])
|
||||
|
||||
op.create_index(op.f('ix_message_account_key'), 'message', ['account_key'], unique=False)
|
||||
op.create_index(op.f('ix_comment_account_key'), 'comment', ['account_key'], unique=False)
|
||||
op.create_index(op.f('ix_approval_account_key'), 'approval', ['account_key'], unique=False)
|
||||
op.create_index(op.f('ix_change_account_key'), 'change', ['account_key'], unique=False)
|
||||
|
||||
connection = op.get_bind()
|
||||
project = sa.sql.table('project', sa.sql.column('updated', sa.DateTime))
|
||||
connection.execute(project.update().values({'updated':None}))
|
||||
|
||||
approval = sa.sql.table('approval', sa.sql.column('pending'))
|
||||
connection.execute(approval.delete().where(approval.c.pending==False))
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,94 +0,0 @@
|
||||
"""add files table
|
||||
|
||||
Revision ID: 50344aecd1c2
|
||||
Revises: 1bb187bcd401
|
||||
Create Date: 2015-04-13 08:08:08.682803
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '50344aecd1c2'
|
||||
down_revision = '1bb187bcd401'
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
from alembic import op, context
|
||||
import sqlalchemy as sa
|
||||
import git.exc
|
||||
|
||||
import gertty.db
|
||||
import gertty.gitrepo
|
||||
|
||||
def upgrade():
|
||||
op.create_table('file',
|
||||
sa.Column('key', sa.Integer(), nullable=False),
|
||||
sa.Column('revision_key', sa.Integer(), nullable=False, index=True),
|
||||
sa.Column('path', sa.Text(), nullable=False, index=True),
|
||||
sa.Column('old_path', sa.Text(), index=True),
|
||||
sa.Column('status', sa.String(length=1)),
|
||||
sa.Column('inserted', sa.Integer()),
|
||||
sa.Column('deleted', sa.Integer()),
|
||||
sa.PrimaryKeyConstraint('key')
|
||||
)
|
||||
|
||||
pathre = re.compile('((.*?)\{|^)(.*?) => (.*?)(\}(.*)|$)')
|
||||
insert = sa.text('insert into file (key, revision_key, path, old_path, status, inserted, deleted) '
|
||||
' values (NULL, :revision_key, :path, :old_path, :status, :inserted, :deleted)')
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
countres = conn.execute('select count(*) from revision')
|
||||
revisions = countres.fetchone()[0]
|
||||
if revisions > 50:
|
||||
print('')
|
||||
print('Adding support for searching for changes by file modified. '
|
||||
'This may take a while.')
|
||||
|
||||
qres = conn.execute('select p.name, c.number, c.status, r.key, r.number, r."commit", r.parent from project p, change c, revision r '
|
||||
'where r.change_key=c.key and c.project_key=p.key order by p.name')
|
||||
|
||||
count = 0
|
||||
for (pname, cnumber, cstatus, rkey, rnumber, commit, parent) in qres.fetchall():
|
||||
count += 1
|
||||
sys.stdout.write('Diffstat revision %s / %s\r' % (count, revisions))
|
||||
sys.stdout.flush()
|
||||
ires = conn.execute(insert, revision_key=rkey, path='/COMMIT_MSG', old_path=None,
|
||||
status=None, inserted=None, deleted=None)
|
||||
repo = gertty.gitrepo.get_repo(pname, context.config.gertty_app.config)
|
||||
try:
|
||||
stats = repo.diffstat(parent, commit)
|
||||
except git.exc.GitCommandError:
|
||||
# Probably a missing commit
|
||||
if cstatus not in ['MERGED', 'ABANDONED']:
|
||||
print("Unable to examine diff for %s %s change %s,%s" % (cstatus, pname, cnumber, rnumber))
|
||||
continue
|
||||
for stat in stats:
|
||||
try:
|
||||
(added, removed, path) = stat
|
||||
except ValueError:
|
||||
if cstatus not in ['MERGED', 'ABANDONED']:
|
||||
print("Empty diffstat for %s %s change %s,%s" % (cstatus, pname, cnumber, rnumber))
|
||||
m = pathre.match(path)
|
||||
status = gertty.db.File.STATUS_MODIFIED
|
||||
old_path = None
|
||||
if m:
|
||||
status = gertty.db.File.STATUS_RENAMED
|
||||
pre = m.group(2) or ''
|
||||
post = m.group(6) or ''
|
||||
old_path = pre+m.group(3)+post
|
||||
path = pre+m.group(4)+post
|
||||
try:
|
||||
added = int(added)
|
||||
except ValueError:
|
||||
added = None
|
||||
try:
|
||||
removed = int(removed)
|
||||
except ValueError:
|
||||
removed = None
|
||||
conn.execute(insert, revision_key=rkey, path=path, old_path=old_path,
|
||||
status=status, inserted=added, deleted=removed)
|
||||
print('')
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,26 +0,0 @@
|
||||
"""Increase status field width
|
||||
|
||||
Revision ID: 56e48a4a064a
|
||||
Revises: 44402069e137
|
||||
Create Date: 2014-05-05 11:49:42.133569
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '56e48a4a064a'
|
||||
down_revision = '44402069e137'
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
def upgrade():
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('status', sa.String(16), index=True, nullable=False)
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('status', sa.String(8), index=True, nullable=False)
|
||||
])
|
@ -1,41 +0,0 @@
|
||||
"""Add fetch ref column
|
||||
|
||||
Revision ID: 725816dc500
|
||||
Revises: 38104b4c1b84
|
||||
Create Date: 2014-05-31 14:51:08.078616
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '725816dc500'
|
||||
down_revision = '38104b4c1b84'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('revision', sa.Column('fetch_auth', sa.Boolean()))
|
||||
op.add_column('revision', sa.Column('fetch_ref', sa.String(length=255)))
|
||||
|
||||
conn = op.get_bind()
|
||||
res = conn.execute('select r.key, r.number, c.number from revision r, "change" c where r.change_key=c.key')
|
||||
for (rkey, rnumber, cnumber) in res.fetchall():
|
||||
q = sa.text('update revision set fetch_auth=:auth, fetch_ref=:ref where "key"=:key')
|
||||
ref = 'refs/changes/%s/%s/%s' % (str(cnumber)[-2:], cnumber, rnumber)
|
||||
res = conn.execute(q, key=rkey, ref=ref, auth=False)
|
||||
|
||||
sqlite_alter_columns('revision', [
|
||||
sa.Column('fetch_auth', sa.Boolean(), nullable=False),
|
||||
sa.Column('fetch_ref', sa.String(length=255), nullable=False)
|
||||
])
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('revision', 'fetch_auth')
|
||||
op.drop_column('revision', 'fetch_ref')
|
@ -1,37 +0,0 @@
|
||||
"""add change.outdated
|
||||
|
||||
Revision ID: 7ef7dfa2ca3a
|
||||
Revises: 37a702b7f58e
|
||||
Create Date: 2016-08-09 08:59:04.441926
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7ef7dfa2ca3a'
|
||||
down_revision = '37a702b7f58e'
|
||||
|
||||
import warnings
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from gertty.dbsupport import sqlite_alter_columns
|
||||
|
||||
|
||||
def upgrade():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
op.add_column('change', sa.Column('outdated', sa.Boolean()))
|
||||
|
||||
connection = op.get_bind()
|
||||
change = sa.sql.table('change',
|
||||
sa.sql.column('outdated', sa.Boolean()))
|
||||
connection.execute(change.update().values({'outdated':False}))
|
||||
|
||||
sqlite_alter_columns('change', [
|
||||
sa.Column('outdated', sa.Boolean(), index=True, nullable=False),
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1,69 +0,0 @@
|
||||
# Copyright 2015 Christoph Gysin <christoph.gysin@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
|
||||
class FormAuth(requests.auth.AuthBase):
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.log = logging.getLogger('gertty.auth')
|
||||
|
||||
def _retry_using_form_auth(self, response, args):
|
||||
adapter = requests.adapters.HTTPAdapter()
|
||||