Browse Source

Initial fork from gertty -> boartty

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

2
.gitignore

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

2
.gitreview

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

36
CONTRIBUTING.rst

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

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

0
gertty/__init__.py → boartty/__init__.py

2
gertty/alembic.ini → boartty/alembic.ini

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

0
gertty/alembic/README → boartty/alembic/README

4
gertty/alembic/env.py → boartty/alembic/env.py

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

0
gertty/alembic/script.py.mako → boartty/alembic/script.py.mako

193
boartty/alembic/versions/183755ac91df_initial_schema.py

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

188
gertty/app.py → boartty/app.py

@ -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 saveNewStory(self, dialog):
with self.db.getSession() as session:
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)
self.sync.submitTask(
sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY))
self.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
self.backScreen()
def saveReviews(self, revision_keys, approvals, message, upload, submit):
message_keys = []
def initSystemData(self):
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
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
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
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',

2
gertty/commentlink.py → boartty/commentlink.py

@ -22,7 +22,7 @@ import re
import six
import urwid
from gertty import mywid
from boartty import mywid
try:
OrderedDict = collections.OrderedDict

94
gertty/config.py → boartty/config.py

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

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