Browse Source

Initial fork from gertty -> boartty

Change-Id: I8c0ce5550f2287f77fb31c790c3923d3d1b80481
changes/91/391991/2
James E. Blair 5 years ago
parent
commit
e4c972b803
75 changed files with 4318 additions and 8645 deletions
  1. +1
    -1
      .gitignore
  2. +1
    -1
      .gitreview
  3. +14
    -22
      CONTRIBUTING.rst
  4. +57
    -120
      README.rst
  5. +0
    -0
      boartty/__init__.py
  6. +1
    -1
      boartty/alembic.ini
  7. +0
    -0
      boartty/alembic/README
  8. +2
    -2
      boartty/alembic/env.py
  9. +0
    -0
      boartty/alembic/script.py.mako
  10. +193
    -0
      boartty/alembic/versions/183755ac91df_initial_schema.py
  11. +81
    -107
      boartty/app.py
  12. +1
    -1
      boartty/commentlink.py
  13. +40
    -54
      boartty/config.py
  14. +676
    -0
      boartty/db.py
  15. +1
    -11
      boartty/dbsupport.py
  16. +27
    -42
      boartty/keymap.py
  17. +91
    -7
      boartty/mywid.py
  18. +103
    -0
      boartty/palette.py
  19. +0
    -0
      boartty/requestsexceptions.py
  20. +14
    -14
      boartty/search/__init__.py
  21. +109
    -104
      boartty/search/parser.py
  22. +5
    -5
      boartty/search/tokenizer.py
  23. +1137
    -0
      boartty/sync.py
  24. +1
    -1
      boartty/version.py
  25. +0
    -0
      boartty/view/__init__.py
  26. +0
    -0
      boartty/view/mouse_scroll_decorator.py
  27. +39
    -53
      boartty/view/project_list.py
  28. +837
    -0
      boartty/view/story.py
  29. +513
    -0
      boartty/view/story_list.py
  30. +4
    -4
      doc/Makefile
  31. +11
    -11
      doc/source/conf.py
  32. +69
    -175
      doc/source/configuration.rst
  33. +2
    -2
      doc/source/contributing.rst
  34. +9
    -14
      doc/source/index.rst
  35. +4
    -38
      doc/source/installation.rst
  36. +23
    -33
      doc/source/usage.rst
  37. +0
    -88
      examples/googlesource-gertty.yaml
  38. +15
    -0
      examples/minimal-boartty.yaml
  39. +0
    -13
      examples/minimal-gertty.yaml
  40. +42
    -0
      examples/openstack-boartty.yaml
  41. +0
    -142
      examples/openstack-gertty.yaml
  42. +187
    -0
      examples/reference-boartty.yaml
  43. +0
    -248
      examples/reference-gertty.yaml
  44. +0
    -26
      gertty/alembic/versions/1bb187bcd401_add_query_sync_table.py
  45. +0
    -22
      gertty/alembic/versions/1cdd4e2e74c_add_revision_indexes.py
  46. +0
    -64
      gertty/alembic/versions/254ac5fc3941_attach_comments_to_files.py
  47. +0
    -25
      gertty/alembic/versions/2a11dd14665_fix_account_table.py
  48. +0
    -36
      gertty/alembic/versions/312cd5a9f878_add_can_submit_column.py
  49. +0
    -27
      gertty/alembic/versions/3610c2543e07_add_conflicts_table.py
  50. +0
    -26
      gertty/alembic/versions/37a702b7f58e_add_last_seen_column_to_change.py
  51. +0
    -33
      gertty/alembic/versions/38104b4c1b84_added_project_updated_column.py
  52. +0
    -37
      gertty/alembic/versions/3cc7e3753dc3_add_hold.py
  53. +0
    -49
      gertty/alembic/versions/3d429503a29a_add_draft_fields.py
  54. +0
    -35
      gertty/alembic/versions/4388de50824a_add_topic_table.py
  55. +0
    -176
      gertty/alembic/versions/44402069e137_initial_schema.py
  56. +0
    -66
      gertty/alembic/versions/46b175bfa277_add_pending_actions.py
  57. +0
    -41
      gertty/alembic/versions/4a802b741d2f_add_starred.py
  58. +0
    -73
      gertty/alembic/versions/4cc9c46f9d8b_add_account_table.py
  59. +0
    -94
      gertty/alembic/versions/50344aecd1c2_add_files_table.py
  60. +0
    -26
      gertty/alembic/versions/56e48a4a064a_increase_status_field_width.py
  61. +0
    -41
      gertty/alembic/versions/725816dc500_add_fetch_ref_column.py
  62. +0
    -37
      gertty/alembic/versions/7ef7dfa2ca3a_add_change_outdated.py
  63. +0
    -69
      gertty/auth.py
  64. +0
    -1006
      gertty/db.py
  65. +0
    -540
      gertty/gitrepo.py
  66. +0
    -140
      gertty/palette.py
  67. +0
    -1577
      gertty/sync.py
  68. +0
    -1169
      gertty/view/change.py
  69. +0
    -844
      gertty/view/change_list.py
  70. +0
    -540
      gertty/view/diff.py
  71. +0
    -241
      gertty/view/side_diff.py
  72. +0
    -262
      gertty/view/unified_diff.py
  73. +0
    -1
      requirements.txt
  74. +6
    -6
      setup.cfg
  75. +2
    -2
      tox.ini

+ 1
- 1
.gitignore View File

@ -1,5 +1,5 @@
*.pyc
*.egg*
gertty-env
boartty-env
.tox
doc/build

+ 1
- 1
.gitreview View File

@ -1,4 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/gertty.git
project=openstack/boartty.git

+ 14
- 22
CONTRIBUTING.rst View File

@ -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.

+ 57
- 120
README.rst View File

@ -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 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.
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.
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/

gertty/__init__.py → boartty/__init__.py View File


gertty/alembic.ini → boartty/alembic.ini View File


gertty/alembic/README → boartty/alembic/README View File


gertty/alembic/env.py → boartty/alembic/env.py View File


gertty/alembic/script.py.mako → boartty/alembic/script.py.mako View File


+ 193
- 0
boartty/alembic/versions/183755ac91df_initial_schema.py View 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

gertty/app.py → boartty/app.py View File


gertty/commentlink.py → boartty/commentlink.py View File


gertty/config.py → boartty/config.py View File


+ 676
- 0
boartty/db.py View 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

gertty/dbsupport.py → boartty/dbsupport.py View File


gertty/keymap.py → boartty/keymap.py View File


gertty/mywid.py → boartty/mywid.py View File


+ 103
- 0
boartty/palette.py View 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

gertty/requestsexceptions.py → boartty/requestsexceptions.py View File


gertty/search/__init__.py → boartty/search/__init__.py View File


gertty/search/parser.py → boartty/search/parser.py View File


gertty/search/tokenizer.py → boartty/search/tokenizer.py View File


+ 1137
- 0
boartty/sync.py
File diff suppressed because it is too large
View File


gertty/version.py → boartty/version.py View File


gertty/view/__init__.py → boartty/view/__init__.py View File


gertty/view/mouse_scroll_decorator.py → boartty/view/mouse_scroll_decorator.py View File


gertty/view/project_list.py → boartty/view/project_list.py View File


+ 837
- 0
boartty/view/story.py View 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):