Browse Source

Initial fork from gertty -> boartty

Change-Id: I8c0ce5550f2287f77fb31c790c3923d3d1b80481
changes/91/391991/2
James E. Blair 2 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 @@
1 1
 *.pyc
2 2
 *.egg*
3
-gertty-env
3
+boartty-env
4 4
 .tox
5 5
 doc/build

+ 1
- 1
.gitreview View File

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

+ 14
- 22
CONTRIBUTING.rst View File

@@ -1,10 +1,10 @@
1 1
 Contributing
2 2
 ============
3 3
 
4
-To browse the latest code, see: https://git.openstack.org/cgit/stackforge/gertty/tree/
5
-To clone the latest code, use `git clone git://git.openstack.org/stackforge/gertty`
4
+To browse the latest code, see: https://git.openstack.org/cgit/openstack/boartty/tree/
5
+To clone the latest code, use `git clone git://git.openstack.org/openstack/boartty`
6 6
 
7
-Bugs are handled at: https://storyboard.openstack.org/#!/project/698
7
+Bugs are handled at: https://storyboard.openstack.org/
8 8
 
9 9
 Code reviews are handled by gerrit at: https://review.openstack.org
10 10
 
@@ -18,26 +18,19 @@ that links to your launchpad account). Example::
18 18
 Philosophy
19 19
 ----------
20 20
 
21
-Gertty is based on the following precepts which should inform changes
21
+Boartty is based on the following precepts which should inform changes
22 22
 to the program:
23 23
 
24
-* Support large numbers of review requests across large numbers of
25
-  projects.  Help the user prioritize those reviews.
24
+* Support large numbers of stories across large numbers of projects.
26 25
 
27
-* Adopt a news/mailreader-like workflow in support of the above.
28
-  Being able to subscribe to projects, mark reviews as "read" without
29
-  reviewing, etc, are all useful concepts to support a heavy review
30
-  load (they have worked extremely well in supporting people who
31
-  read/write a lot of mail/news).
32
-
33
-* Support off-line use.  Gertty should be completely usable off-line
34
-  with reliable syncing between local data and Gerrit when a
26
+* Support off-line use.  Boartty should be completely usable off-line
27
+  with reliable syncing between local data and Storyboard when a
35 28
   connection is available (just like git or mail or news).
36 29
 
37 30
 * Ample use of color.  Unlike a web interface, a good text interface
38 31
   relies mostly on color and precise placement rather than whitespace
39 32
   and decoration to indicate to the user the purpose of a given piece
40
-  of information.  Gertty should degrade well to 16 colors, but more
33
+  of information.  Boartty should degrade well to 16 colors, but more
41 34
   (88 or 256) may be used.
42 35
 
43 36
 * Keyboard navigation (with easy-to-remember commands) should be
@@ -51,10 +44,9 @@ to the program:
51 44
   messages or comments) and navigating back intuitive (it matches
52 45
   expectations set by the web browsers).
53 46
 
54
-* Support a wide variety of Gerrit installations.  The initial
55
-  development of Gertty is against the OpenStack project's Gerrit, and
56
-  many of the features are intended to help its developers with their
57
-  workflow, however, those features should be implemented in a generic
58
-  way so that the system does not require a specific Gerrit
59
-  configuration.
60
-
47
+* Support a wide variety of Storyboard installations.  The initial
48
+  development of Boartty is against the OpenStack project's
49
+  Storyboard, and many of the features are intended to help its
50
+  developers with their workflow, however, those features should be
51
+  implemented in a generic way so that the system does not require a
52
+  specific Storyboard configuration.

+ 57
- 120
README.rst View File

@@ -1,137 +1,84 @@
1
-Gertty
2
-======
1
+Boartty
2
+=======
3 3
 
4
-Gertty is a console-based interface to the Gerrit Code Review system.
4
+Boartty is a console-based interface to the Storyboard task-tracking
5
+system.
5 6
 
6 7
 As compared to the web interface, the main advantages are:
7 8
 
8 9
  * Workflow -- the interface is designed to support a workflow similar
9 10
    to reading network news or mail.  In particular, it is designed to
10
-   deal with a large number of review requests across a large number
11
-   of projects.
11
+   deal with a large number of stories across a large number of
12
+   projects.
12 13
 
13
- * Offline Use -- Gertty syncs information about changes in subscribed
14
-   projects to a local database and local git repos.  All review
15
-   operations are performed against that database and then synced back
16
-   to Gerrit.
14
+ * Offline Use -- Boartty syncs information about changes in
15
+   subscribed projects to a local database.  All review operations are
16
+   performed against that database and then synced back to Storyboard.
17 17
 
18 18
  * Speed -- user actions modify locally cached content and need not
19 19
    wait for server interaction.
20 20
 
21
- * Convenience -- because Gertty downloads all changes to local git
22
-   repos, a single command instructs it to checkout a change into that
23
-   repo for detailed examination or testing of larger changes.
24
-
25 21
 Installation
26 22
 ------------
27 23
 
28
-Debian
29
-~~~~~~
30
-
31
-Gertty is packaged in Debian and is currently available in:
32
-
33
- * unstable
34
- * testing
35
- * stable
36
-
37
-You can install it with::
38
-
39
-  apt-get install gertty
40
-
41
-Fedora
42
-~~~~~~
43
-
44
-Gertty is packaged starting in Fedora 21.  You can install it with::
45
-
46
-  yum install python-gertty
47
-
48
-openSUSE
49
-~~~~~~~~
50
-
51
-Gertty is packaged for openSUSE 13.1 onwards.  You can install it via
52
-`1-click install from the Open Build Service <http://software.opensuse.org/package/python-gertty>`_.
53
-
54
-Gentoo
55
-~~~~~~
56
-
57
-Gertty is available in the main Gentoo repository.  You can install it with::
58
-
59
-  emerge gertty
60
-
61
-Arch Linux
62
-~~~~~~~~~~
63
-
64
-Gertty packages are available in the Arch User Repository packages. You
65
-can get the package from::
66
-
67
-  https://aur.archlinux.org/packages/python2-gertty/
68
-
69 24
 Source
70 25
 ~~~~~~
71 26
 
72 27
 When installing from source, it is recommended (but not required) to
73
-install Gertty in a virtualenv.  To set one up::
28
+install Boartty in a virtualenv.  To set one up::
74 29
 
75
-  virtualenv gertty-env
76
-  source gertty-env/bin/activate
30
+  virtualenv boartty-env
31
+  source boartty-env/bin/activate
77 32
 
78 33
 To install the latest version from the cheeseshop::
79 34
 
80
-  pip install gertty
35
+  pip install boartty
81 36
 
82 37
 To install from a git checkout::
83 38
 
84 39
   pip install .
85 40
 
86
-Gertty uses a YAML based configuration file that it looks for at
87
-``~/.gertty.yaml``.  Several sample configuration files are included.
41
+Boartty uses a YAML based configuration file that it looks for at
42
+``~/.boartty.yaml``.  Several sample configuration files are included.
88 43
 You can find them in the examples/ directory of the
89
-`source distribution <https://git.openstack.org/cgit/openstack/gertty/tree/examples>`_
90
-or the share/gertty/examples directory after installation.
44
+`source distribution <https://git.openstack.org/cgit/openstack/boartty/tree/examples>`_
45
+or the share/boartty/examples directory after installation.
91 46
 
92
-Select one of the sample config files, copy it to ~/.gertty.yaml and
47
+Select one of the sample config files, copy it to ~/.boartty.yaml and
93 48
 edit as necessary.  Search for ``CHANGEME`` to find parameters that
94 49
 need to be supplied.  The sample config files are as follows:
95 50
 
96
-**minimal-gertty.yaml**
97
-  Only contains the parameters required for Gertty to actually run.
51
+**minimal-boartty.yaml**
52
+  Only contains the parameters required for Boartty to actually run.
98 53
 
99
-**reference-gertty.yaml**
54
+**reference-boartty.yaml**
100 55
   An exhaustive list of all supported options with examples.
101 56
 
102
-**openstack-gertty.yaml**
57
+**openstack-boartty.yaml**
103 58
   A configuration designed for use with OpenStack's installation of
104 59
   Gerrit.
105 60
 
106
-**googlesource-gertty.yaml**
107
-  A configuration designed for use with installations of Gerrit
108
-  running on googlesource.com.
109
-
110
-You will need your Gerrit password which you can generate or retrieve
111
-by navigating to ``Settings``, then ``HTTP Password``.
112
-
113
-Gertty uses local git repositories to perform much of its work.  These
114
-can be the same git repositories that you use when developing a
115
-project.  Gertty will not alter the working directory or index unless
116
-you request it to (and even then, the usual git safeguards against
117
-accidentally losing work remain in place).  You will need to supply
118
-the name of a directory where Gertty will find or clone git
119
-repositories for your projects as the ``git-root`` parameter.
61
+You will need a Storyboard authentication token which you can generate
62
+or retrieve by navigating to ``Profile``, then ``Tokens`` (the "key"
63
+icon), or visiting the `/#!/profile/tokens` URI in your Storyboard
64
+installation.  Issue a new token if you have not done so before, and
65
+give it a sufficiently long lifetime (for example, one decade).  Copy
66
+and paste the resulting token in your ``~/.boartty.yaml`` file.
120 67
 
121
-The config file is designed to support multiple Gerrit instances.  The
122
-first one is used by default, but others can be specified by supplying
123
-the name on the command line.
68
+The config file is designed to support multiple Storyboard instances.
69
+The first one is used by default, but others can be specified by
70
+supplying the name on the command line.
124 71
 
125 72
 Usage
126 73
 -----
127 74
 
128
-After installing Gertty, you should be able to run it by invoking
129
-``gertty``.  If you installed it in a virtualenv, you can invoke it
130
-without activating the virtualenv with ``/path/to/venv/bin/gertty``
131
-which you may wish to add to your shell aliases.  Use ``gertty
75
+After installing Boartty, you should be able to run it by invoking
76
+``boartty``.  If you installed it in a virtualenv, you can invoke it
77
+without activating the virtualenv with ``/path/to/venv/bin/boartty``
78
+which you may wish to add to your shell aliases.  Use ``boartty
132 79
 --help`` to see a list of command line options available.
133 80
 
134
-Once Gertty is running, you will need to start by subscribing to some
81
+Once Boartty is running, you will need to start by subscribing to some
135 82
 projects.  Use 'L' to list all of the projects and then 's' to
136 83
 subscribe to the ones you are interested in.  Hit 'L' again to shrink
137 84
 the list to your subscribed projects.
@@ -139,37 +86,27 @@ the list to your subscribed projects.
139 86
 In general, pressing the F1 key will show help text on any screen, and
140 87
 ESC will take you to the previous screen.
141 88
 
142
-Gertty works seamlessly offline or online.  All of the actions that it
143
-performs are first recorded in a local database (in ``~/.gertty.db``
144
-by default), and are then transmitted to Gerrit.  If Gertty is unable
145
-to contact Gerrit for any reason, it will continue to operate against
146
-the local database, and once it re-establishes contact, it will
147
-process any pending changes.
89
+Boartty works seamlessly offline or online.  All of the actions that
90
+it performs are first recorded in a local database (in
91
+``~/.boartty.db`` by default), and are then transmitted to Storyboard.
92
+If Boartty is unable to contact Storyboard for any reason, it will
93
+continue to operate against the local database, and once it
94
+re-establishes contact, it will process any pending changes.
148 95
 
149 96
 The status bar at the top of the screen displays the current number of
150
-outstanding tasks that Gertty must perform in order to be fully up to
97
+outstanding tasks that Boartty must perform in order to be fully up to
151 98
 date.  Some of these tasks are more complicated than others, and some
152 99
 of them will end up creating new tasks (for instance, one task may be
153
-to search for new changes in a project which will then produce 5 new
154
-tasks if there are 5 new changes).
100
+to search for new stories in a project which will then produce 5 new
101
+tasks if there are 5 new stories).
155 102
 
156
-If Gertty is offline, it will so indicate in the status bar.  It will
103
+If Boartty is offline, it will so indicate in the status bar.  It will
157 104
 retry requests if needed, and will switch between offline and online
158 105
 mode automatically.
159 106
 
160
-If you review a change while offline with a positive vote, and someone
161
-else leaves a negative vote on that change in the same category before
162
-Gertty is able to upload your review, Gertty will detect the situation
163
-and mark the change as "held" so that you may re-inspect the change
164
-and any new comments before uploading the review.  The status bar will
165
-alert you to any held changes and direct you to a list of them (the
166
-`F12` key by default).  When viewing a change, the "held" flag may be
167
-toggled with the exclamation key (`!`).  Once held, a change must be
168
-explicitly un-held in this manner for your review to be uploaded.
169
-
170
-If Gertty encounters an error, this will also be indicated in the
171
-status bar.  You may wish to examine ~/.gertty.log to see what the
172
-error was.  In many cases, Gertty can continue after encountering an
107
+If Boartty encounters an error, this will also be indicated in the
108
+status bar.  You may wish to examine ~/.boartty.log to see what the
109
+error was.  In many cases, Boartty can continue after encountering an
173 110
 error.  The error flag will be cleared when you leave the current
174 111
 screen.
175 112
 
@@ -180,28 +117,28 @@ Terminal Integration
180 117
 --------------------
181 118
 
182 119
 If you use rxvt-unicode, you can add something like the following to
183
-``.Xresources`` to make Gerrit URLs that are displayed in your
120
+``.Xresources`` to make Storyboard URLs that are displayed in your
184 121
 terminal (perhaps in an email or irc client) clickable links that open
185
-in Gertty::
122
+in Boartty::
186 123
 
187 124
   URxvt.perl-ext:           default,matcher
188 125
   URxvt.url-launcher:       sensible-browser
189 126
   URxvt.keysym.C-Delete:    perl:matcher:last
190 127
   URxvt.keysym.M-Delete:    perl:matcher:list
191 128
   URxvt.matcher.button:     1
192
-  URxvt.matcher.pattern.1:  https:\/\/review.example.org/(\\#\/c\/)?(\\d+)[\w]*
193
-  URxvt.matcher.launcher.1: gertty --open $0
129
+  URxvt.matcher.pattern.1:  https:\/\/storyboard.example.org/#!/story/(\\d+)[\w]*
130
+  URxvt.matcher.launcher.1: boartty --open $0
194 131
 
195
-You will want to adjust the pattern to match the review site you are
196
-interested in; multiple patterns may be added as needed.
132
+You will want to adjust the pattern to match the Storyboard site you
133
+are interested in; multiple patterns may be added as needed.
197 134
 
198 135
 Contributing
199 136
 ------------
200 137
 
201
-For information on how to contribute to Gertty, please see the
138
+For information on how to contribute to Boartty, please see the
202 139
 contents of the CONTRIBUTING.rst file.
203 140
 
204 141
 Bugs
205 142
 ----
206 143
 
207
-Bugs are handled at: https://storyboard.openstack.org/#!/project/698
144
+Bugs are handled at: https://storyboard.openstack.org/

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


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

@@ -20,7 +20,7 @@ script_location = alembic
20 20
 # versions/ directory
21 21
 # sourceless = false
22 22
 
23
-sqlalchemy.url = sqlite:////tmp/gertty.db
23
+sqlalchemy.url = sqlite:////tmp/boartty.db
24 24
 
25 25
 
26 26
 # Logging configuration

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


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

@@ -15,8 +15,8 @@ config = context.config
15 15
 # for 'autogenerate' support
16 16
 # from myapp import mymodel
17 17
 # target_metadata = mymodel.Base.metadata
18
-import gertty.db
19
-target_metadata = gertty.db.metadata
18
+import boartty.db
19
+target_metadata = boartty.db.metadata
20 20
 
21 21
 # other values from the config, defined by the needs of env.py,
22 22
 # can be acquired:

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 @@
1
+"""initial schema
2
+
3
+Revision ID: 183755ac91df
4
+Revises: None
5
+Create Date: 2016-10-31 08:54:59.399741
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '183755ac91df'
11
+down_revision = None
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    ### commands auto generated by Alembic - please adjust! ###
19
+    op.create_table('comment',
20
+    sa.Column('key', sa.Integer(), nullable=False),
21
+    sa.Column('id', sa.Integer(), nullable=True),
22
+    sa.Column('parent_comment_key', sa.Integer(), nullable=True),
23
+    sa.Column('content', sa.Text(), nullable=True),
24
+    sa.Column('draft', sa.Boolean(), nullable=False),
25
+    sa.Column('pending', sa.Boolean(), nullable=False),
26
+    sa.Column('pending_delete', sa.Boolean(), nullable=False),
27
+    sa.ForeignKeyConstraint(['parent_comment_key'], ['comment.key'], ),
28
+    sa.PrimaryKeyConstraint('key')
29
+    )
30
+    op.create_index(op.f('ix_comment_draft'), 'comment', ['draft'], unique=False)
31
+    op.create_index(op.f('ix_comment_id'), 'comment', ['id'], unique=False)
32
+    op.create_index(op.f('ix_comment_pending'), 'comment', ['pending'], unique=False)
33
+    op.create_index(op.f('ix_comment_pending_delete'), 'comment', ['pending_delete'], unique=False)
34
+    op.create_table('project',
35
+    sa.Column('key', sa.Integer(), nullable=False),
36
+    sa.Column('id', sa.Integer(), nullable=True),
37
+    sa.Column('name', sa.String(length=255), nullable=False),
38
+    sa.Column('subscribed', sa.Boolean(), nullable=True),
39
+    sa.Column('description', sa.Text(), nullable=True),
40
+    sa.Column('updated', sa.DateTime(), nullable=True),
41
+    sa.PrimaryKeyConstraint('key')
42
+    )
43
+    op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False)
44
+    op.create_index(op.f('ix_project_name'), 'project', ['name'], unique=False)
45
+    op.create_index(op.f('ix_project_subscribed'), 'project', ['subscribed'], unique=False)
46
+    op.create_index(op.f('ix_project_updated'), 'project', ['updated'], unique=False)
47
+    op.create_table('sync_query',
48
+    sa.Column('key', sa.Integer(), nullable=False),
49
+    sa.Column('name', sa.String(length=255), nullable=False),
50
+    sa.Column('updated', sa.DateTime(), nullable=True),
51
+    sa.PrimaryKeyConstraint('key')
52
+    )
53
+    op.create_index(op.f('ix_sync_query_name'), 'sync_query', ['name'], unique=True)
54
+    op.create_index(op.f('ix_sync_query_updated'), 'sync_query', ['updated'], unique=False)
55
+    op.create_table('system',
56
+    sa.Column('key', sa.Integer(), nullable=False),
57
+    sa.Column('user_id', sa.Integer(), nullable=True),
58
+    sa.PrimaryKeyConstraint('key')
59
+    )
60
+    op.create_table('tag',
61
+    sa.Column('key', sa.Integer(), nullable=False),
62
+    sa.Column('id', sa.Integer(), nullable=True),
63
+    sa.Column('name', sa.String(length=255), nullable=False),
64
+    sa.PrimaryKeyConstraint('key')
65
+    )
66
+    op.create_index(op.f('ix_tag_id'), 'tag', ['id'], unique=False)
67
+    op.create_index(op.f('ix_tag_name'), 'tag', ['name'], unique=False)
68
+    op.create_table('topic',
69
+    sa.Column('key', sa.Integer(), nullable=False),
70
+    sa.Column('name', sa.String(length=255), nullable=False),
71
+    sa.Column('sequence', sa.Integer(), nullable=False),
72
+    sa.PrimaryKeyConstraint('key')
73
+    )
74
+    op.create_index(op.f('ix_topic_name'), 'topic', ['name'], unique=False)
75
+    op.create_index(op.f('ix_topic_sequence'), 'topic', ['sequence'], unique=True)
76
+    op.create_table('user',
77
+    sa.Column('key', sa.Integer(), nullable=False),
78
+    sa.Column('id', sa.Integer(), nullable=True),
79
+    sa.Column('name', sa.String(length=255), nullable=True),
80
+    sa.Column('email', sa.String(length=255), nullable=True),
81
+    sa.PrimaryKeyConstraint('key')
82
+    )
83
+    op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=False)
84
+    op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
85
+    op.create_index(op.f('ix_user_name'), 'user', ['name'], unique=False)
86
+    op.create_table('project_topic',
87
+    sa.Column('key', sa.Integer(), nullable=False),
88
+    sa.Column('project_key', sa.Integer(), nullable=True),
89
+    sa.Column('topic_key', sa.Integer(), nullable=True),
90
+    sa.Column('sequence', sa.Integer(), nullable=False),
91
+    sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
92
+    sa.ForeignKeyConstraint(['topic_key'], ['topic.key'], ),
93
+    sa.PrimaryKeyConstraint('key'),
94
+    sa.UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const')
95
+    )
96
+    op.create_index(op.f('ix_project_topic_project_key'), 'project_topic', ['project_key'], unique=False)
97
+    op.create_index(op.f('ix_project_topic_topic_key'), 'project_topic', ['topic_key'], unique=False)
98
+    op.create_table('story',
99
+    sa.Column('key', sa.Integer(), nullable=False),
100
+    sa.Column('id', sa.Integer(), nullable=True),
101
+    sa.Column('user_key', sa.Integer(), nullable=True),
102
+    sa.Column('status', sa.String(length=16), nullable=False),
103
+    sa.Column('hidden', sa.Boolean(), nullable=False),
104
+    sa.Column('subscribed', sa.Boolean(), nullable=False),
105
+    sa.Column('title', sa.String(length=255), nullable=True),
106
+    sa.Column('private', sa.Boolean(), nullable=False),
107
+    sa.Column('description', sa.Text(), nullable=True),
108
+    sa.Column('created', sa.DateTime(), nullable=True),
109
+    sa.Column('updated', sa.DateTime(), nullable=True),
110
+    sa.Column('last_seen', sa.DateTime(), nullable=True),
111
+    sa.Column('outdated', sa.Boolean(), nullable=False),
112
+    sa.Column('pending', sa.Boolean(), nullable=False),
113
+    sa.Column('pending_delete', sa.Boolean(), nullable=False),
114
+    sa.ForeignKeyConstraint(['user_key'], ['user.key'], ),
115
+    sa.PrimaryKeyConstraint('key')
116
+    )
117
+    op.create_index(op.f('ix_story_created'), 'story', ['created'], unique=False)
118
+    op.create_index(op.f('ix_story_hidden'), 'story', ['hidden'], unique=False)
119
+    op.create_index(op.f('ix_story_id'), 'story', ['id'], unique=False)
120
+    op.create_index(op.f('ix_story_last_seen'), 'story', ['last_seen'], unique=False)
121
+    op.create_index(op.f('ix_story_outdated'), 'story', ['outdated'], unique=False)
122
+    op.create_index(op.f('ix_story_pending'), 'story', ['pending'], unique=False)
123
+    op.create_index(op.f('ix_story_pending_delete'), 'story', ['pending_delete'], unique=False)
124
+    op.create_index(op.f('ix_story_status'), 'story', ['status'], unique=False)
125
+    op.create_index(op.f('ix_story_subscribed'), 'story', ['subscribed'], unique=False)
126
+    op.create_index(op.f('ix_story_title'), 'story', ['title'], unique=False)
127
+    op.create_index(op.f('ix_story_updated'), 'story', ['updated'], unique=False)
128
+    op.create_index(op.f('ix_story_user_key'), 'story', ['user_key'], unique=False)
129
+    op.create_table('event',
130
+    sa.Column('key', sa.Integer(), nullable=False),
131
+    sa.Column('id', sa.Integer(), nullable=True),
132
+    sa.Column('type', sa.String(length=255), nullable=False),
133
+    sa.Column('user_key', sa.Integer(), nullable=True),
134
+    sa.Column('story_key', sa.Integer(), nullable=True),
135
+    sa.Column('created', sa.DateTime(), nullable=True),
136
+    sa.Column('comment_key', sa.Integer(), nullable=True),
137
+    sa.Column('info', sa.Text(), nullable=True),
138
+    sa.ForeignKeyConstraint(['comment_key'], ['comment.key'], ),
139
+    sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
140
+    sa.ForeignKeyConstraint(['user_key'], ['user.key'], ),
141
+    sa.PrimaryKeyConstraint('key')
142
+    )
143
+    op.create_index(op.f('ix_event_created'), 'event', ['created'], unique=False)
144
+    op.create_index(op.f('ix_event_id'), 'event', ['id'], unique=False)
145
+    op.create_index(op.f('ix_event_type'), 'event', ['type'], unique=False)
146
+    op.create_index(op.f('ix_event_user_key'), 'event', ['user_key'], unique=False)
147
+    op.create_table('story_tag',
148
+    sa.Column('key', sa.Integer(), nullable=False),
149
+    sa.Column('story_key', sa.Integer(), nullable=True),
150
+    sa.Column('tag_key', sa.Integer(), nullable=True),
151
+    sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
152
+    sa.ForeignKeyConstraint(['tag_key'], ['tag.key'], ),
153
+    sa.PrimaryKeyConstraint('key')
154
+    )
155
+    op.create_index(op.f('ix_story_tag_story_key'), 'story_tag', ['story_key'], unique=False)
156
+    op.create_index(op.f('ix_story_tag_tag_key'), 'story_tag', ['tag_key'], unique=False)
157
+    op.create_table('task',
158
+    sa.Column('key', sa.Integer(), nullable=False),
159
+    sa.Column('id', sa.Integer(), nullable=True),
160
+    sa.Column('title', sa.String(length=255), nullable=True),
161
+    sa.Column('status', sa.String(length=16), nullable=True),
162
+    sa.Column('creator_user_key', sa.Integer(), nullable=True),
163
+    sa.Column('story_key', sa.Integer(), nullable=True),
164
+    sa.Column('project_key', sa.Integer(), nullable=True),
165
+    sa.Column('assignee_user_key', sa.Integer(), nullable=True),
166
+    sa.Column('priority', sa.String(length=16), nullable=True),
167
+    sa.Column('link', sa.Text(), nullable=True),
168
+    sa.Column('created', sa.DateTime(), nullable=True),
169
+    sa.Column('updated', sa.DateTime(), nullable=True),
170
+    sa.Column('pending', sa.Boolean(), nullable=False),
171
+    sa.Column('pending_delete', sa.Boolean(), nullable=False),
172
+    sa.ForeignKeyConstraint(['assignee_user_key'], ['user.key'], ),
173
+    sa.ForeignKeyConstraint(['creator_user_key'], ['user.key'], ),
174
+    sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
175
+    sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
176
+    sa.PrimaryKeyConstraint('key')
177
+    )
178
+    op.create_index(op.f('ix_task_assignee_user_key'), 'task', ['assignee_user_key'], unique=False)
179
+    op.create_index(op.f('ix_task_created'), 'task', ['created'], unique=False)
180
+    op.create_index(op.f('ix_task_creator_user_key'), 'task', ['creator_user_key'], unique=False)
181
+    op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False)
182
+    op.create_index(op.f('ix_task_pending'), 'task', ['pending'], unique=False)
183
+    op.create_index(op.f('ix_task_pending_delete'), 'task', ['pending_delete'], unique=False)
184
+    op.create_index(op.f('ix_task_project_key'), 'task', ['project_key'], unique=False)
185
+    op.create_index(op.f('ix_task_status'), 'task', ['status'], unique=False)
186
+    op.create_index(op.f('ix_task_story_key'), 'task', ['story_key'], unique=False)
187
+    op.create_index(op.f('ix_task_title'), 'task', ['title'], unique=False)
188
+    op.create_index(op.f('ix_task_updated'), 'task', ['updated'], unique=False)
189
+    ### end Alembic commands ###
190
+
191
+
192
+def downgrade():
193
+    pass

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

@@ -35,28 +35,27 @@ from six.moves.urllib import parse as urlparse
35 35
 import sqlalchemy.exc
36 36
 import urwid
37 37
 
38
-from gertty import db
39
-from gertty import config
40
-from gertty import gitrepo
41
-from gertty import keymap
42
-from gertty import mywid
43
-from gertty import palette
44
-from gertty import sync
45
-from gertty import search
46
-from gertty import requestsexceptions
47
-from gertty.view import change_list as view_change_list
48
-from gertty.view import project_list as view_project_list
49
-from gertty.view import change as view_change
50
-import gertty.view
51
-import gertty.version
38
+from boartty import db
39
+from boartty import config
40
+from boartty import keymap
41
+from boartty import mywid
42
+from boartty import palette
43
+from boartty import sync
44
+from boartty import search
45
+from boartty import requestsexceptions
46
+from boartty.view import story_list as view_story_list
47
+from boartty.view import project_list as view_project_list
48
+from boartty.view import story as view_story
49
+import boartty.view
50
+import boartty.version
52 51
 
53 52
 WELCOME_TEXT = """\
54
-Welcome to Gertty!
53
+Welcome to Boartty!
55 54
 
56 55
 To get started, you should subscribe to some projects.  Press the "L"
57 56
 key (shift-L) to list all the projects, navigate to the ones you are
58
-interested in, and then press "s" to subscribe to them.  Gertty will
59
-automatically sync changes in your subscribed projects.
57
+interested in, and then press "s" to subscribe to them.  Boardtty will
58
+automatically sync stories in your subscribed projects.
60 59
 
61 60
 Press the F1 key anywhere to get help.  Your terminal emulator may
62 61
 require you to press function-F1 or alt-F1 instead.
@@ -233,8 +232,8 @@ class ProjectCache(object):
233 232
     def get(self, project):
234 233
         if project.key not in self.projects:
235 234
             self.projects[project.key] = dict(
236
-                unreviewed_changes = len(project.unreviewed_changes),
237
-                open_changes = len(project.open_changes),
235
+                active_stories = len(project.active_stories),
236
+                stories = len(project.stories),
238 237
             )
239 238
         return self.projects[project.key]
240 239
 
@@ -272,14 +271,14 @@ class App(object):
272 271
             req_logger.setLevel(level)
273 272
         else:
274 273
             req_logger.setLevel(req_level_name)
275
-        self.log = logging.getLogger('gertty.App')
274
+        self.log = logging.getLogger('boartty.App')
276 275
         self.log.debug("Starting")
277 276
 
278 277
         self.lock_fd = open(self.config.lock_file, 'w')
279 278
         try:
280 279
             fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
281 280
         except IOError:
282
-            print("error: another instance of gertty is running for: %s" % self.config.server['name'])
281
+            print("error: another instance of boartty is running for: %s" % self.config.server['name'])
283 282
             sys.exit(1)
284 283
 
285 284
         self.project_cache = ProjectCache()
@@ -291,6 +290,7 @@ class App(object):
291 290
         self.config.keymap.updateCommandMap()
292 291
         self.search = search.SearchCompiler(self.config.username)
293 292
         self.db = db.Database(self, self.config.dburi, self.search)
293
+        self.initSystemData()
294 294
         self.sync = sync.Sync(self, disable_background_sync)
295 295
 
296 296
         self.status = StatusHeader(self)
@@ -405,6 +405,7 @@ class App(object):
405 405
         if not self.screens:
406 406
             return
407 407
         while self.screens:
408
+            self.log.debug("screens %s" % (target_widget,))
408 409
             widget = self.screens.pop()
409 410
             if (not target_widget) or (widget is target_widget):
410 411
                 break
@@ -415,9 +416,9 @@ class App(object):
415 416
         self.frame.body = widget
416 417
         self.refresh(force=True)
417 418
 
418
-    def findChangeList(self):
419
+    def findStoryList(self):
419 420
         for widget in reversed(self.screens):
420
-            if isinstance(widget, view_change_list.ChangeListView):
421
+            if isinstance(widget, view_story_list.StoryListView):
421 422
                 return widget
422 423
         return None
423 424
 
@@ -450,6 +451,7 @@ class App(object):
450 451
         self.status.refresh()
451 452
 
452 453
     def updateStatusQueries(self):
454
+        return # TODO: storyboard
453 455
         with self.db.getSession() as session:
454 456
             held = len(session.getHeld())
455 457
             self.status.update(held=held)
@@ -515,6 +517,7 @@ class App(object):
515 517
             lambda button: self.backScreen())
516 518
         self.popup(dialog, min_width=76, min_height=len(lines)+4)
517 519
 
520
+    #storyboard
518 521
     def _syncOneChangeFromQuery(self, query):
519 522
         number = changeid = restid = None
520 523
         if query.startswith("change:"):
@@ -574,7 +577,7 @@ class App(object):
574 577
         with self.db.getSession() as session:
575 578
             try:
576 579
                 changes = session.getChanges(query)
577
-            except gertty.search.SearchSyntaxError as e:
580
+            except boartty.search.SearchSyntaxError as e:
578 581
                 return self.error(e.message)
579 582
             except sqlalchemy.exc.OperationalError as e:
580 583
                 return self.error(e.message)
@@ -587,9 +590,9 @@ class App(object):
587 590
             if change_key:
588 591
                 view = view_change.ChangeView(self, change_key)
589 592
             else:
590
-                view = view_change_list.ChangeListView(self, query)
593
+                view = view_story_list.StoryListView(self, query)
591 594
             self.changeScreen(view)
592
-        except gertty.view.DisplayError as e:
595
+        except boartty.view.DisplayError as e:
593 596
             return self.error(e.message)
594 597
 
595 598
     def searchDialog(self, default):
@@ -677,15 +680,17 @@ class App(object):
677 680
             self.help()
678 681
         elif keymap.QUIT in commands:
679 682
             self.quit()
680
-        elif keymap.CHANGE_SEARCH in commands:
683
+        elif keymap.STORY_SEARCH in commands:
681 684
             self.searchDialog('')
685
+        elif keymap.NEW_STORY in commands:
686
+            self.newStory()
682 687
         elif keymap.LIST_HELD in commands:
683 688
             self.doSearch("is:held")
684 689
         elif key in self.config.dashboards:
685 690
             d = self.config.dashboards[key]
686
-            view = view_change_list.ChangeListView(self, d['query'], d['name'],
687
-                                                   sort_by=d.get('sort-by'),
688
-                                                   reverse=d.get('reverse'))
691
+            view = view_story_list.StoryListView(self, d['query'], d['name'],
692
+                                                 sort_by=d.get('sort-by'),
693
+                                                 reverse=d.get('reverse'))
689 694
             self.changeScreen(view)
690 695
         elif keymap.FURTHER_INPUT in commands:
691 696
             self.input_buffer.append(key)
@@ -711,6 +716,8 @@ class App(object):
711 716
         self.loop.screen.clear()
712 717
 
713 718
     def time(self, dt):
719
+        if dt is None:
720
+            return None
714 721
         utc = dt.replace(tzinfo=dateutil.tz.tzutc())
715 722
         if self.config.utc:
716 723
             return utc
@@ -753,6 +760,7 @@ class App(object):
753 760
         else:
754 761
             self.log.error("Unable to parse command %s with data %s" % (command, data))
755 762
 
763
+    #storyboard
756 764
     def toggleHeldChange(self, change_key):
757 765
         with self.db.getSession() as session:
758 766
             change = session.getChange(change_key)
@@ -767,88 +775,54 @@ class App(object):
767 775
         self.updateStatusQueries()
768 776
         return ret
769 777
 
770
-    def localCheckoutCommit(self, project_name, commit_sha):
771
-        repo = gitrepo.get_repo(project_name, self.config)
772
-        try:
773
-            repo.checkout(commit_sha)
774
-            dialog = mywid.MessageDialog('Checkout', 'Change checked out in %s' % repo.path)
775
-            min_height=8
776
-        except gitrepo.GitCheckoutError as e:
777
-            dialog = mywid.MessageDialog('Error', e.msg)
778
-            min_height=12
779
-        urwid.connect_signal(dialog, 'close',
780
-            lambda button: self.backScreen())
781
-        self.popup(dialog, min_height=min_height)
778
+    def newStory(self):
779
+        dialog = view_story.NewStoryDialog(self)
780
+        urwid.connect_signal(dialog, 'save',
781
+                             lambda button: self.saveNewStory(dialog))
782
+        urwid.connect_signal(dialog, 'cancel',
783
+                             lambda button: self.cancelNewStory(dialog))
784
+        self.popup(dialog,
785
+                   relative_width=50, relative_height=25,
786
+                   min_width=60, min_height=8)
782 787
 
783
-    def localCherryPickCommit(self, project_name, commit_sha):
784
-        repo = gitrepo.get_repo(project_name, self.config)
785
-        try:
786
-            repo.cherryPick(commit_sha)
787
-            dialog = mywid.MessageDialog('Cherry-Pick', 'Change cherry-picked in %s' % repo.path)
788
-            min_height=8
789
-        except gitrepo.GitCheckoutError as e:
790
-            dialog = mywid.MessageDialog('Error', e.msg)
791
-            min_height=12
792
-        urwid.connect_signal(dialog, 'close',
793
-            lambda button: self.backScreen())
794
-        self.popup(dialog, min_height=min_height)
788
+    def cancelNewStory(self, dialog):
789
+        self.backScreen()
790
+
791
+    def saveNewStory(self, dialog):
792
+        with self.db.getSession() as session:
793
+            story = session.createStory(
794
+                title=dialog.title_field.edit_text,
795
+                description=dialog.description_field.edit_text,
796
+                pending=True)
797
+            task = story.addTask(
798
+                project=session.getProjectByID(dialog.project_button.key),
799
+                title=dialog.title_field.edit_text,
800
+                pending=True)
801
+
802
+        self.sync.submitTask(
803
+            sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY))
804
+        self.sync.submitTask(
805
+            sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
806
+        self.backScreen()
795 807
 
796
-    def saveReviews(self, revision_keys, approvals, message, upload, submit):
797
-        message_keys = []
808
+    def initSystemData(self):
798 809
         with self.db.getSession() as session:
799
-            account = session.getAccountByUsername(self.config.username)
800
-            for revision_key in revision_keys:
801
-                k = self._saveReview(session, account, revision_key,
802
-                                     approvals, message, upload, submit)
803
-                if k:
804
-                    message_keys.append(k)
805
-        return message_keys
806
-
807
-    def _saveReview(self, session, account, revision_key,
808
-                    approvals, message, upload, submit):
809
-        message_key = None
810
-        revision = session.getRevision(revision_key)
811
-        change = revision.change
812
-        draft_approvals = {}
813
-        for approval in change.draft_approvals:
814
-            draft_approvals[approval.category] = approval
815
-
816
-        categories = set()
817
-        for label in change.permitted_labels:
818
-            categories.add(label.category)
819
-        for category in categories:
820
-            value = approvals.get(category, 0)
821
-            approval = draft_approvals.get(category)
822
-            if not approval:
823
-                approval = change.createApproval(account, category, 0, draft=True)
824
-                draft_approvals[category] = approval
825
-            approval.value = value
826
-        draft_message = revision.getPendingMessage()
827
-        if not draft_message:
828
-            draft_message = revision.getDraftMessage()
829
-        if not draft_message:
830
-            if message or upload:
831
-                draft_message = revision.createMessage(None, account,
832
-                                                       datetime.datetime.utcnow(),
833
-                                                       '', draft=True)
834
-        if draft_message:
835
-            draft_message.created = datetime.datetime.utcnow()
836
-            draft_message.message = message
837
-            draft_message.pending = upload
838
-            message_key = draft_message.key
839
-        if upload:
840
-            change.reviewed = True
841
-            self.project_cache.clear(change.project)
842
-        if submit:
843
-            change.status = 'SUBMITTED'
844
-            change.pending_status = True
845
-            change.pending_status_message = None
846
-        return message_key
810
+            system = session.getSystem()
811
+            if system is None:
812
+                self.user_id = None
813
+            else:
814
+                self.user_id = system.user_id
847 815
 
816
+    def setUserID(self, user_id):
817
+        with self.db.getSession() as session:
818
+            system = session.getSystem()
819
+            if system is None:
820
+                system = session.createSystem()
821
+            system.user_id = self.user_id = user_id
848 822
 
849 823
 
850 824
 def version():
851
-    return "Gertty version: %s" % gertty.version.version_info.release_string()
825
+    return "Boardtty version: %s" % boartty.version.version_info.release_string()
852 826
 
853 827
 class PrintKeymapAction(argparse.Action):
854 828
     def __call__(self, parser, namespace, values, option_string=None):
@@ -900,10 +874,10 @@ def main():
900 874
                         help='print the palette attribute names to stdout')
901 875
     parser.add_argument('--open', nargs=1, action=OpenChangeAction,
902 876
                         metavar='URL',
903
-                        help='open the given URL in a running Gertty')
877
+                        help='open the given URL in a running Boardtty')
904 878
     parser.add_argument('--version', dest='version', action='version',
905 879
                         version=version(),
906
-                        help='show Gertty\'s version')
880
+                        help='show Boardtty\'s version')
907 881
     parser.add_argument('-p', dest='palette', default='default',
908 882
                         help='color palette to use')
909 883
     parser.add_argument('-k', dest='keymap', default='default',

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

@@ -22,7 +22,7 @@ import re
22 22
 import six
23 23
 import urwid
24 24
 
25
-from gertty import mywid
25
+from boartty import mywid
26 26
 
27 27
 try:
28 28
     OrderedDict = collections.OrderedDict

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

@@ -26,27 +26,24 @@ import yaml
26 26
 from six.moves.urllib import parse as urlparse
27 27
 import voluptuous as v
28 28
 
29
-import gertty.commentlink
30
-import gertty.palette
31
-import gertty.keymap
29
+import boartty.commentlink
30
+import boartty.palette
31
+import boartty.keymap
32 32
 
33 33
 try:
34 34
     OrderedDict = collections.OrderedDict
35 35
 except AttributeError:
36 36
     OrderedDict = ordereddict.OrderedDict
37 37
 
38
-DEFAULT_CONFIG_PATH='~/.gertty.yaml'
38
+DEFAULT_CONFIG_PATH='~/.boartty.yaml'
39 39
 
40 40
 class ConfigSchema(object):
41 41
     server = {v.Required('name'): str,
42 42
               v.Required('url'): str,
43
-              v.Required('username'): str,
44
-              'password': str,
43
+              v.Required('token'): str,
45 44
               'verify-ssl': bool,
46 45
               'ssl-ca-path': str,
47 46
               'dburi': str,
48
-              v.Required('git-root'): str,
49
-              'git-url': str,
50 47
               'log-file': str,
51 48
               'socket': str,
52 49
               'auth-type': v.Any('basic', 'digest', 'form'),
@@ -100,7 +97,7 @@ class ConfigSchema(object):
100 97
 
101 98
     hide_comments = [hide_comment]
102 99
 
103
-    change_list_options = {'sort-by': sort_by,
100
+    story_list_options = {'sort-by': sort_by,
104 101
                            'reverse': bool}
105 102
 
106 103
     keymap = {v.Required('name'): str,
@@ -117,14 +114,13 @@ class ConfigSchema(object):
117 114
                            'commentlinks': self.commentlinks,
118 115
                            'dashboards': self.dashboards,
119 116
                            'reviewkeys': self.reviewkeys,
120
-                           'change-list-query': str,
117
+                           'story-list-query': str,
121 118
                            'diff-view': str,
122 119
                            'hide-comments': self.hide_comments,
123
-                           'thread-changes': bool,
124 120
                            'display-times-in-utc': bool,
125 121
                            'handle-mouse': bool,
126 122
                            'breadcrumbs': bool,
127
-                           'change-list-options': self.change_list_options,
123
+                           'story-list-options': self.story_list_options,
128 124
                            'expire-age': str,
129 125
                            })
130 126
         return schema
@@ -149,21 +145,17 @@ class Config(object):
149 145
         self.url = url
150 146
         result = urlparse.urlparse(url)
151 147
         self.hostname = result.netloc
152
-        self.username = server['username']
153
-        self.password = server.get('password')
154
-        if self.password is None:
155
-            self.password = getpass.getpass("Password for %s (%s): "
156
-                                            % (self.url, self.username))
157
-        else:
158
-            # Ensure file is only readable by user as password is stored in
159
-            # file.
160
-            mode = os.stat(self.path).st_mode & 0o0777
161
-            if not mode == 0o600:
162
-                print (
163
-                    "Error: Config file '{}' contains a password and does "
164
-                    "not have permissions set to 0600.\n"
165
-                    "Permissions are: {}".format(self.path, oct(mode)))
166
-                exit(1)
148
+        self.token = server['token']
149
+        self.username = '' # TODO: storyboard
150
+        # Ensure file is only readable by user as password is stored in
151
+        # file.
152
+        mode = os.stat(self.path).st_mode & 0o0777
153
+        if not mode == 0o600:
154
+            print (
155
+                "Error: Config file '{}' contains an api key and does "
156
+                "not have permissions set to 0600.\n"
157
+                "Permissions are: {}".format(self.path, oct(mode)))
158
+            exit(1)
167 159
         self.auth_type = server.get('auth-type', 'digest')
168 160
         self.verify_ssl = server.get('verify-ssl', True)
169 161
         if not self.verify_ssl:
@@ -171,54 +163,49 @@ class Config(object):
171 163
         self.ssl_ca_path = server.get('ssl-ca-path', None)
172 164
         if self.ssl_ca_path is not None:
173 165
             self.ssl_ca_path = os.path.expanduser(self.ssl_ca_path)
174
-            # Gertty itself uses the Requests library
166
+            # Boardtty itself uses the Requests library
175 167
             os.environ['REQUESTS_CA_BUNDLE'] = self.ssl_ca_path
176 168
             # And this is to allow Git callouts
177 169
             os.environ['GIT_SSL_CAINFO'] = self.ssl_ca_path
178
-        self.git_root = os.path.expanduser(server['git-root'])
179
-        git_url = server.get('git-url', self.url + 'p/')
180
-        if not git_url.endswith('/'):
181
-            git_url += '/'
182
-        self.git_url = git_url
183 170
         self.dburi = server.get('dburi',
184
-                                'sqlite:///' + os.path.expanduser('~/.gertty.db'))
185
-        socket_path = server.get('socket', '~/.gertty.sock')
171
+                                'sqlite:///' + os.path.expanduser('~/.boartty.db'))
172
+        socket_path = server.get('socket', '~/.boartty.sock')
186 173
         self.socket_path = os.path.expanduser(socket_path)
187
-        log_file = server.get('log-file', '~/.gertty.log')
174
+        log_file = server.get('log-file', '~/.boartty.log')
188 175
         self.log_file = os.path.expanduser(log_file)
189
-        lock_file = server.get('lock-file', '~/.gertty.%s.lock' % server['name'])
176
+        lock_file = server.get('lock-file', '~/.boartty.%s.lock' % server['name'])
190 177
         self.lock_file = os.path.expanduser(lock_file)
191 178
 
192
-        self.palettes = {'default': gertty.palette.Palette({}),
193
-                         'light': gertty.palette.Palette(gertty.palette.LIGHT_PALETTE),
179
+        self.palettes = {'default': boartty.palette.Palette({}),
180
+                         'light': boartty.palette.Palette(boartty.palette.LIGHT_PALETTE),
194 181
                          }
195 182
         for p in self.config.get('palettes', []):
196 183
             if p['name'] not in self.palettes:
197
-                self.palettes[p['name']] = gertty.palette.Palette(p)
184
+                self.palettes[p['name']] = boartty.palette.Palette(p)
198 185
             else:
199 186
                 self.palettes[p['name']].update(p)
200 187
         self.palette = self.palettes[self.config.get('palette', palette)]
201 188
 
202
-        self.keymaps = {'default': gertty.keymap.KeyMap({}),
203
-                        'vi': gertty.keymap.KeyMap(gertty.keymap.VI_KEYMAP)}
189
+        self.keymaps = {'default': boartty.keymap.KeyMap({}),
190
+                        'vi': boartty.keymap.KeyMap(boartty.keymap.VI_KEYMAP)}
204 191
         for p in self.config.get('keymaps', []):
205 192
             if p['name'] not in self.keymaps:
206
-                self.keymaps[p['name']] = gertty.keymap.KeyMap(p)
193
+                self.keymaps[p['name']] = boartty.keymap.KeyMap(p)
207 194
             else:
208 195
                 self.keymaps[p['name']].update(p)
209 196
         self.keymap = self.keymaps[self.config.get('keymap', keymap)]
210 197
 
211
-        self.commentlinks = [gertty.commentlink.CommentLink(c)
198
+        self.commentlinks = [boartty.commentlink.CommentLink(c)
212 199
                              for c in self.config.get('commentlinks', [])]
213 200
         self.commentlinks.append(
214
-            gertty.commentlink.CommentLink(dict(
201
+            boartty.commentlink.CommentLink(dict(
215 202
                     match="(?P<url>https?://\\S*)",
216 203
                     replacements=[
217 204
                         dict(link=dict(
218 205
                                 text="{url}",
219 206
                                 url="{url}"))])))
220 207
 
221
-        self.project_change_list_query = self.config.get('change-list-query', 'status:open')
208
+        self.project_story_list_query = self.config.get('story-list-query', '')
222 209
 
223 210
         self.diff_view = self.config.get('diff-view', 'side-by-side')
224 211
 
@@ -235,15 +222,14 @@ class Config(object):
235 222
         for h in self.config.get('hide-comments', []):
236 223
             self.hide_comments.append(re.compile(h['author']))
237 224
 
238
-        self.thread_changes = self.config.get('thread-changes', True)
239 225
         self.utc = self.config.get('display-times-in-utc', False)
240 226
         self.breadcrumbs = self.config.get('breadcrumbs', True)
241 227
         self.handle_mouse = self.config.get('handle-mouse', True)
242 228
 
243
-        change_list_options = self.config.get('change-list-options', {})
244
-        self.change_list_options = {
245
-            'sort-by': change_list_options.get('sort-by', 'number'),
246
-            'reverse': change_list_options.get('reverse', False)}
229
+        story_list_options = self.config.get('story-list-options', {})
230
+        self.story_list_options = {
231
+            'sort-by': story_list_options.get('sort-by', 'number'),
232
+            'reverse': story_list_options.get('reverse', False)}
247 233
 
248 234
         self.expire_age = self.config.get('expire-age', '2 months')
249 235
 
@@ -254,11 +240,11 @@ class Config(object):
254 240
         return None
255 241
 
256 242
     def printSample(self):
257
-        filename = 'share/gertty/examples'
258
-        print("""Gertty requires a configuration file at ~/.gertty.yaml
243
+        filename = 'share/boartty/examples'
244
+        print("""Boardtty requires a configuration file at ~/.boartty.yaml
259 245
 If the file contains a password then permissions must be set to 0600.
260 246
 
261
-Several sample configuration files were installed with Gertty and are
247
+Several sample configuration files were installed with Boardtty and are
262 248
 available in %s in the root of the installation.
263 249
 
264 250
 For more information, please see the README.

+ 676
- 0
boartty/db.py View File

@@ -0,0 +1,676 @@
1
+# Copyright 2014 OpenStack Foundation
2
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
3
+#
4
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+# not use this file except in compliance with the License. You may obtain
6
+# a copy of the License at
7
+#
8
+#      http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+# Unless required by applicable law or agreed to in writing, software
11
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+# License for the specific language governing permissions and limitations
14
+# under the License.
15
+
16
+import datetime
17
+import re
18
+import time
19
+import logging
20
+import threading
21
+
22
+import alembic
23
+import alembic.config
24
+import six
25
+import sqlalchemy
26
+from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text, UniqueConstraint
27
+from sqlalchemy.schema import ForeignKey
28
+from sqlalchemy.orm import mapper, sessionmaker, relationship, scoped_session, joinedload
29
+from sqlalchemy.orm.session import Session
30
+from sqlalchemy.sql import exists
31
+from sqlalchemy.sql.expression import and_
32
+
33
+metadata = MetaData()
34
+system_table = Table(
35
+    'system', metadata,
36
+    Column('key', Integer, primary_key=True),
37
+    Column('user_id', Integer),
38
+    )
39
+project_table = Table(
40
+    'project', metadata,
41
+    Column('key', Integer, primary_key=True),
42
+    Column('id', Integer, index=True),
43
+    Column('name', String(255), index=True, nullable=False),
44
+    Column('subscribed', Boolean, index=True, default=False),
45
+    Column('description', Text),
46
+    Column('updated', DateTime, index=True),
47
+    )
48
+topic_table = Table(
49
+    'topic', metadata,
50
+    Column('key', Integer, primary_key=True),
51
+    Column('name', String(255), index=True, nullable=False),
52
+    Column('sequence', Integer, index=True, unique=True, nullable=False),
53
+    )
54
+project_topic_table = Table(
55
+    'project_topic', metadata,
56
+    Column('key', Integer, primary_key=True),
57
+    Column('project_key', Integer, ForeignKey("project.key"), index=True),
58
+    Column('topic_key', Integer, ForeignKey("topic.key"), index=True),
59
+    Column('sequence', Integer, nullable=False),
60
+    UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
61
+    )
62
+story_table = Table(
63
+    'story', metadata,
64
+    Column('key', Integer, primary_key=True),
65
+    Column('id', Integer, index=True),
66
+    Column('user_key', Integer, ForeignKey("user.key"), index=True),
67
+    Column('status', String(16), index=True, nullable=False),
68
+    Column('hidden', Boolean, index=True, nullable=False),
69
+    Column('subscribed', Boolean, index=True, nullable=False),
70
+    Column('title', String(255), index=True),
71
+    Column('private', Boolean, nullable=False),
72
+    Column('description', Text),
73
+    Column('created', DateTime, index=True),
74
+    # TODO: make sure updated is never null in storyboard
75
+    Column('updated', DateTime, index=True),
76
+    Column('last_seen', DateTime, index=True),
77
+    Column('outdated', Boolean, index=True, nullable=False),
78
+    Column('pending', Boolean, index=True, nullable=False),
79
+    Column('pending_delete', Boolean, index=True, nullable=False),
80
+)
81
+tag_table = Table(
82
+    'tag', metadata,
83
+    Column('key', Integer, primary_key=True),
84
+    Column('id', Integer, index=True),
85
+    Column('name', String(255), index=True, nullable=False),
86
+)
87
+story_tag_table = Table(
88
+    'story_tag', metadata,
89
+    Column('key', Integer, primary_key=True),
90
+    Column('story_key', Integer, ForeignKey("story.key"), index=True),
91
+    Column('tag_key', Integer, ForeignKey("tag.key"), index=True),
92
+)
93
+task_table = Table(
94
+    'task', metadata,
95
+    Column('key', Integer, primary_key=True),
96
+    Column('id', Integer, index=True),
97
+    Column('title', String(255), index=True),
98
+    Column('status', String(16), index=True),
99
+    Column('creator_user_key', Integer, ForeignKey("user.key"), index=True),
100
+    Column('story_key', Integer, ForeignKey("story.key"), index=True),
101
+    Column('project_key', Integer, ForeignKey("project.key"), index=True),
102
+    Column('assignee_user_key', Integer, ForeignKey("user.key"), index=True),
103
+    Column('priority', String(16)),
104
+    Column('link', Text),
105
+    Column('created', DateTime, index=True),
106
+    # TODO: make sure updated is never null in storyboard
107
+    Column('updated', DateTime, index=True),
108
+    Column('pending', Boolean, index=True, nullable=False),
109
+    Column('pending_delete', Boolean, index=True, nullable=False),
110
+)
111
+event_table = Table(
112
+    'event', metadata,
113
+    Column('key', Integer, primary_key=True),
114
+    Column('id', Integer, index=True),
115
+    Column('type', String(255), index=True, nullable=False),
116
+    Column('user_key', Integer, ForeignKey("user.key"), index=True),
117
+    Column('story_key', Integer, ForeignKey('story.key'), nullable=True),
118
+    #Column('worklist_key', Integer, ForeignKey('worklist.key'), nullable=True),
119
+    #Column('board_key', Integer, ForeignKey('board.key'), nullable=True),
120
+    Column('created', DateTime, index=True),
121
+    Column('comment_key', Integer, ForeignKey('comment.key'), nullable=True),
122
+    Column('user_key', ForeignKey('user.key'), nullable=True),
123
+    Column('info', Text),
124
+)
125
+comment_table = Table(
126
+    'comment', metadata,
127
+    Column('key', Integer, primary_key=True),
128
+    Column('id', Integer, index=True),
129
+    Column('parent_comment_key', Integer, ForeignKey('comment.key'), nullable=True),
130
+    Column('content', Text),
131
+    Column('draft', Boolean, index=True, nullable=False),
132
+    Column('pending', Boolean, index=True, nullable=False),
133
+    Column('pending_delete', Boolean, index=True, nullable=False),
134
+)
135
+user_table = Table(
136
+    'user', metadata,
137
+    Column('key', Integer, primary_key=True),
138
+    Column('id', Integer, index=True),
139
+    Column('name', String(255), index=True),
140
+    Column('email', String(255), index=True),
141
+    )
142
+sync_query_table = Table(
143
+    'sync_query', metadata,
144
+    Column('key', Integer, primary_key=True),
145
+    Column('name', String(255), index=True, unique=True, nullable=False),
146
+    Column('updated', DateTime, index=True),
147
+    )
148
+
149
+class System(object):
150
+    def __init__(self, user_id=None):
151
+        self.user_id = user_id
152
+
153
+class User(object):
154
+    def __init__(self, id, name=None, email=None):
155
+        self.id = id
156
+        self.name = name
157
+        self.email = email
158
+
159
+class Project(object):
160
+    def __init__(self, id, name, subscribed=False, description=''):
161
+        self.id = id
162
+        self.name = name
163
+        self.subscribed = subscribed
164
+        self.description = description
165
+
166
+    def createChange(self, *args, **kw):
167
+        session = Session.object_session(self)
168
+        args = [self] + list(args)
169
+        c = Change(*args, **kw)
170
+        self.changes.append(c)
171
+        session.add(c)
172
+        session.flush()
173
+        return c
174
+
175
+    def createBranch(self, *args, **kw):
176
+        session = Session.object_session(self)
177
+        args = [self] + list(args)
178
+        b = Branch(*args, **kw)
179
+        self.branches.append(b)
180
+        session.add(b)
181
+        session.flush()
182
+        return b
183
+
184
+class ProjectTopic(object):
185
+    def __init__(self, project, topic, sequence):
186
+        self.project_key = project.key
187
+        self.topic_key = topic.key
188
+        self.sequence = sequence
189
+
190
+class Topic(object):
191
+    def __init__(self, name, sequence):
192
+        self.name = name
193
+        self.sequence = sequence
194
+
195
+    def addProject(self, project):
196
+        session = Session.object_session(self)
197
+        seq = max([x.sequence for x in self.project_topics] + [0])
198
+        pt = ProjectTopic(project, self, seq+1)
199
+        self.project_topics.append(pt)
200
+        self.projects.append(project)
201
+        session.add(pt)
202
+        session.flush()
203
+
204
+    def removeProject(self, project):
205
+        session = Session.object_session(self)
206
+        for pt in self.project_topics:
207
+            if pt.project_key == project.key:
208
+                self.project_topics.remove(pt)
209
+                session.delete(pt)
210
+        self.projects.remove(project)
211
+        session.flush()
212
+
213
+def format_name(self):
214
+    name = 'Anonymous Coward'
215
+    if self.creator:
216
+        if self.creator.name:
217
+            name = self.creator.name
218
+        elif self.creator.email:
219
+            name = self.creator.email
220
+    return name
221
+
222
+class Story(object):
223
+    def __init__(self, id=None, creator=None, created=None, title=None,
224
+                 description=None, pending=False):
225
+        self.id = id
226
+        self.creator = creator
227
+        self.title = title
228
+        self.description = description
229
+        self.status = 'active'
230
+        self.created = created
231
+        self.private = False
232
+        self.outdated = False
233
+        self.hidden = False
234
+        self.subscribed = False
235
+        self.pending = pending
236
+        self.pending_delete = False
237
+
238
+    @property
239
+    def creator_name(self):
240
+        return format_name(self)
241
+
242
+    def addEvent(self, *args, **kw):
243
+        session = Session.object_session(self)
244
+        e = Event(*args, **kw)
245
+        e.story_key = self.key
246
+        self.events.append(e)
247
+        session.add(e)
248
+        session.flush()
249
+        return e
250
+
251
+    def addTask(self, *args, **kw):
252
+        session = Session.object_session(self)
253
+        t = Task(*args, **kw)
254
+        t.story_key = self.key
255
+        self.tasks.append(t)
256
+        session.add(t)
257
+        session.flush()
258
+        return t
259
+
260
+    def getDraftCommentEvent(self, parent):
261
+        for event in self.events:
262
+            if (event.comment and event.comment.draft and
263
+                event.comment.parent==parent):
264
+                return event
265
+        return None
266
+
267
+    def setDraftComment(self, creator, parent, content):
268
+        event = self.getDraftCommentEvent(parent)
269
+        if event is None:
270
+            event = self.addEvent(type='user_comment', creator=creator)
271
+            event.addComment()
272
+        event.comment.content = content
273
+        event.comment.draft = True
274
+        event.comment.parent = parent
275
+        return event
276
+
277
+class Tag(object):
278
+    def __init__(self, id, name):
279
+        self.id = id
280
+        self.name = name
281
+
282
+class StoryTag(object):
283
+    def __init__(self, story, tag):
284
+        self.story_key = story.key
285
+        self.tag_key = tag.key
286
+
287
+class Task(object):
288
+    def __init__(self, id=None, title=None, status=None, creator=None,
289
+                 created=None, pending=False, pending_delete=False,
290
+                 project=None):
291
+        self.id = id
292
+        self.title = title
293
+        self.status = status
294
+        self.pending = pending
295
+        self.pending_delete = pending_delete
296
+        self.creator = creator
297
+        self.created = created
298
+        self.project = project
299
+
300
+class Event(object):
301
+    def __init__(self, id=None, type=None, creator=None, created=None, info=None):
302
+        self.id = id
303
+        self.type = type
304
+        self.creator = creator
305
+        if created is None:
306
+            created = datetime.datetime.utcnow()
307
+        self.created = created
308
+        self.info = info
309
+
310
+    @property
311
+    def creator_name(self):
312
+        return format_name(self)
313
+
314
+    @property
315
+    def description(self):
316
+        return re.sub('_', ' ', self.type)
317
+
318
+    def addComment(self, *args, **kw):
319
+        session = Session.object_session(self)
320
+        c = Comment(*args, **kw)
321
+        session.add(c)
322
+        session.flush()
323
+        self.comment_key = c.key
324
+        return c
325
+
326
+class Comment(object):
327
+    def __init__(self, id=None, content=None, parent=None, draft=False,
328
+                 pending=False, pending_delete=False):
329
+        self.id = id
330
+        self.content = content
331
+        self.parent = parent
332
+        self.pending = pending
333
+        self.pending_delete = pending_delete
334
+        self.draft = draft
335
+
336
+class SyncQuery(object):
337
+    def __init__(self, name):
338
+        self.name = name
339
+
340
+mapper(System, system_table)
341
+mapper(User, user_table)
342
+mapper(Project, project_table, properties=dict(
343
+    topics=relationship(Topic,
344
+                        secondary=project_topic_table,
345
+                        order_by=topic_table.c.name,
346
+                        viewonly=True),
347
+    active_stories=relationship(Story,
348
+                                secondary=task_table,
349
+                                primaryjoin=and_(project_table.c.key==task_table.c.project_key,
350
+                                                 story_table.c.key==task_table.c.story_key,
351
+                                                 story_table.c.status=='active'),
352
+                                order_by=story_table.c.id,
353
+                            ),
354
+    stories=relationship(Story,
355
+                         secondary=task_table,
356
+                         order_by=story_table.c.id,
357
+                     ),
358
+))
359
+mapper(Topic, topic_table, properties=dict(
360
+    projects=relationship(Project,
361
+                          secondary=project_topic_table,
362
+                          order_by=project_table.c.name,
363
+                          viewonly=True),
364
+    project_topics=relationship(ProjectTopic),
365
+))
366
+mapper(ProjectTopic, project_topic_table)
367
+mapper(Story, story_table, properties=dict(
368
+        creator=relationship(User),
369
+        tags=relationship(Tag,
370
+                          secondary=story_tag_table,
371
+                          order_by=tag_table.c.name,
372
+                          #viewonly=True
373
+                          ),
374
+        tasks=relationship(Task, backref='story',
375
+                           cascade='all, delete-orphan'),
376
+        events=relationship(Event, backref='story',
377
+                            cascade='all, delete-orphan'),
378
+))
379
+mapper(Tag, tag_table)
380
+mapper(StoryTag, story_tag_table)
381
+mapper(Task, task_table, properties=dict(
382
+    project=relationship(Project),
383
+    assignee=relationship(User, foreign_keys=task_table.c.assignee_user_key),
384
+    creator=relationship(User, foreign_keys=task_table.c.creator_user_key),
385
+))
386
+mapper(Event, event_table, properties=dict(
387
+    creator=relationship(User),
388
+    comment=relationship(Comment, backref='event'),
389
+))
390
+mapper(Comment, comment_table, properties=dict(
391
+    parent=relationship(Comment, remote_side=[comment_table.c.key],backref='children'),
392
+))
393
+mapper(SyncQuery, sync_query_table)
394
+
395
+def match(expr, item):
396
+    if item is None:
397
+        return False
398
+    return re.match(expr, item) is not None
399
+
400
+@sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "connect")
401
+def add_sqlite_match(dbapi_connection, connection_record):
402
+    dbapi_connection.create_function("matches", 2, match)
403
+
404
+class Database(object):
405
+    def __init__(self, app, dburi, search):
406
+        self.log = logging.getLogger('boartty.db')
407
+        self.dburi = dburi
408
+        self.search = search
409
+        self.engine = create_engine(self.dburi)
410
+        #metadata.create_all(self.engine)
411
+        self.migrate(app)
412
+        # If we want the objects returned from query() to be usable
413
+        # outside of the session, we need to expunge them from the session,
414
+        # and since the DatabaseSession always calls commit() on the session
415
+        # when the context manager exits, we need to inform the session to
416
+        # expire objects when it does so.
417
+        self.session_factory = sessionmaker(bind=self.engine,
418
+                                            expire_on_commit=False,
419
+                                            autoflush=False)
420
+        self.session = scoped_session(self.session_factory)
421
+        self.lock = threading.Lock()
422
+
423
+    def getSession(self):
424
+        return DatabaseSession(self)
425
+
426
+    def migrate(self, app):
427
+        conn = self.engine.connect()
428
+        context = alembic.migration.MigrationContext.configure(conn)
429
+        current_rev = context.get_current_revision()
430
+        self.log.debug('Current migration revision: %s' % current_rev)
431
+
432
+        has_table = self.engine.dialect.has_table(conn, "project")
433
+
434
+        config = alembic.config.Config()
435
+        config.set_main_option("script_location", "boartty:alembic")
436
+        config.set_main_option("sqlalchemy.url", self.dburi)
437
+        config.boartty_app = app
438
+
439
+        if current_rev is None and has_table:
440
+            self.log.debug('Stamping database as initial revision')
441
+            alembic.command.stamp(config, "44402069e137")
442
+        alembic.command.upgrade(config, 'head')
443
+
444
+class DatabaseSession(object):
445
+    def __init__(self, database):
446
+        self.database = database
447
+        self.session = database.session
448
+        self.search = database.search
449
+
450
+    def __enter__(self):
451
+        self.database.lock.acquire()
452
+        self.start = time.time()
453
+        return self
454
+
455
+    def __exit__(self, etype, value, tb):
456
+        if etype:
457
+            self.session().rollback()
458
+        else:
459
+            self.session().commit()
460
+        self.session().close()
461
+        self.session = None
462
+        end = time.time()
463
+        self.database.log.debug("Database lock held %s seconds" % (end-self.start,))
464
+        self.database.lock.release()
465
+
466
+    def abort(self):
467
+        self.session().rollback()
468
+
469
+    def commit(self):
470
+        self.session().commit()
471
+
472
+    def delete(self, obj):
473
+        self.session().delete(obj)
474
+
475
+    def vacuum(self):
476
+        self.session().execute("VACUUM")
477
+
478
+    def getProjects(self, subscribed=False, active=False, topicless=False):
479
+        """Retrieve projects.
480
+
481
+        :param subscribed: If True limit to only subscribed projects.
482
+        :param active: If True limit to only projects with active
483
+            stories.
484
+        :param topicless: If True limit to only projects without topics.
485
+        """
486
+        query = self.session().query(Project)
487
+        if subscribed:
488
+            query = query.filter_by(subscribed=subscribed)
489
+            if active:
490
+                query = query.filter(exists().where(Project.active_stories))
491
+        if topicless:
492
+            query = query.filter_by(topics=None)
493
+        return query.order_by(Project.name).all()
494
+
495
+    def getTopics(self):
496
+        return self.session().query(Topic).order_by(Topic.sequence).all()
497
+
498
+    def getProject(self, key):
499
+        try:
500
+            return self.session().query(Project).filter_by(key=key).one()
501
+        except sqlalchemy.orm.exc.NoResultFound:
502
+            return None
503
+
504
+    def getProjectByName(self, name):
505
+        try:
506
+            return self.session().query(Project).filter_by(name=name).one()
507
+        except sqlalchemy.orm.exc.NoResultFound:
508
+            return None
509
+
510
+    def getProjectByID(self, id):
511
+        try:
512
+            return self.session().query(Project).filter_by(id=id).one()
513
+        except sqlalchemy.orm.exc.NoResultFound:
514
+            return None
515
+
516
+    def getTopic(self, key):
517
+        try:
518
+            return self.session().query(Topic).filter_by(key=key).one()
519
+        except sqlalchemy.orm.exc.NoResultFound:
520
+            return None
521
+
522
+    def getTopicByName(self, name):
523
+        try:
524
+            return self.session().query(Topic).filter_by(name=name).one()
525
+        except sqlalchemy.orm.exc.NoResultFound:
526
+            return None
527
+
528
+    def getSyncQueryByName(self, name):
529
+        try:
530
+            return self.session().query(SyncQuery).filter_by(name=name).one()
531
+        except sqlalchemy.orm.exc.NoResultFound:
532
+            return self.createSyncQuery(name)
533
+
534
+    def getStory(self, key):
535
+        query = self.session().query(Story).filter_by(key=key)
536
+        try:
537
+            return query.one()
538
+        except sqlalchemy.orm.exc.NoResultFound:
539
+            return None
540
+
541
+    def getStoryByID(self, id):
542
+        try:
543
+            return self.session().query(Story).filter_by(id=id).one()
544
+        except sqlalchemy.orm.exc.NoResultFound:
545
+            return None
546
+
547
+    def getStories(self, query, active, sort_by='number'):
548
+        self.database.log.debug("Search query: %s sort: %s" % (query, sort_by))
549
+        q = self.session().query(Story)
550
+        if query:
551
+            q = q.filter(self.search.parse(query))
552
+        if active:
553
+            q = q.filter(story_table.c.hidden==False, story_table.c.status=='active')
554
+        if sort_by == 'updated':
555
+            q = q.order_by(story_table.c.updated)
556
+        elif sort_by == 'last-seen':
557
+            q = q.order_by(story_table.c.last_seen)
558
+        else:
559
+            q = q.order_by(story_table.c.id)
560
+        self.database.log.debug("Search SQL: %s" % q)
561
+        try:
562
+            return q.all()
563
+        except sqlalchemy.orm.exc.NoResultFound:
564
+            return []
565
+
566
+    def getTagByID(self, id):
567
+        try:
568
+            return self.session().query(Tag).filter_by(id=id).one()
569
+        except sqlalchemy.orm.exc.NoResultFound:
570
+            return None
571
+
572
+    def getTask(self, key):
573
+        try:
574
+            return self.session().query(Task).filter_by(key=key).one()
575
+        except sqlalchemy.orm.exc.NoResultFound:
576
+            return None
577
+
578
+    def getTaskByID(self, id):
579
+        try:
580
+            return self.session().query(Task).filter_by(id=id).one()
581
+        except sqlalchemy.orm.exc.NoResultFound:
582
+            return None
583
+
584
+    def getComment(self, key):
585
+        try:
586
+            return self.session().query(Comment).filter_by(key=key).one()
587
+        except sqlalchemy.orm.exc.NoResultFound:
588
+            return None
589
+
590
+    def getCommentByID(self, id):
591
+        try:
592
+            return self.session().query(Comment).filter_by(id=id).one()
593
+        except sqlalchemy.orm.exc.NoResultFound:
594
+            return None
595
+
596
+    def getHeld(self):
597
+        return self.session().query(Story).filter_by(held=True).all()
598
+
599
+    def getOutdated(self):
600
+        return self.session().query(Story).filter_by(outdated=True).all()
601
+
602
+    def getPendingStories(self):
603
+        return self.session().query(Story).filter_by(pending=True).all()
604
+
605
+    def getPendingTasks(self):
606
+        return self.session().query(Task).filter_by(pending=True).all()
607
+
608
+    def getUsers(self):
609
+        return self.session().query(User).all()
610
+
611
+    def getUser(self, key):
612
+        try:
613
+            return self.session().query(User).filter_by(key=key).one()
614
+        except sqlalchemy.orm.exc.NoResultFound:
615
+            return None
616
+
617
+    def getUserByID(self, id):
618
+        try:
619
+            return self.session().query(User).filter_by(id=id).one()
620
+        except sqlalchemy.orm.exc.NoResultFound:
621
+            return None
622
+
623
+    def getSystem(self):
624
+        try:
625
+            return self.session().query(System).one()
626
+        except sqlalchemy.orm.exc.NoResultFound:
627
+            return None
628
+
629
+    def getEvent(self, key):
630
+        try:
631
+            return self.session().query(Event).filter_by(key=key).one()
632
+        except sqlalchemy.orm.exc.NoResultFound:
633
+            return None
634
+
635
+    def createProject(self, *args, **kw):
636
+        o = Project(*args, **kw)
637
+        self.session().add(o)
638
+        self.session().flush()
639
+        return o
640
+
641
+    def createStory(self, *args, **kw):
642
+        s = Story(*args, **kw)
643
+        self.session().add(s)
644
+        self.session().flush()
645
+        return s
646
+
647
+    def createUser(self, *args, **kw):
648
+        a = User(*args, **kw)
649
+        self.session().add(a)
650
+        self.session().flush()
651
+        return a
652
+
653
+    def createSyncQuery(self, *args, **kw):
654
+        o = SyncQuery(*args, **kw)
655
+        self.session().add(o)
656
+        self.session().flush()
657
+        return o
658
+
659
+    def createTopic(self, *args, **kw):
660
+        o = Topic(*args, **kw)
661
+        self.session().add(o)
662
+        self.session().flush()
663
+        return o
664
+
665
+    def createTag(self, *args, **kw):
666
+        o = Tag(*args, **kw)
667
+        self.session().add(o)
668
+        self.session().flush()
669
+        return o
670
+
671
+    def createSystem(self, *args, **kw):
672
+        o = System(*args, **kw)
673
+        self.session().add(o)
674
+        self.session().flush()
675
+        return o
676
+

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

@@ -135,17 +135,7 @@ def sqlite_drop_columns(table_name, drop_columns):
135 135
     for key in meta.tables[table_name].foreign_keys:
136 136
         # If this is a single column constraint for a dropped column,
137 137
         # don't copy it.
138
-        if isinstance(key.constraint.columns, sqlalchemy.sql.base.ColumnCollection):
139
-            # This is needed for SQLAlchemy >= 1.0.4
140
-            columns = [c.name for c in key.constraint.columns]
141
-        else:
142
-            # This is needed for SQLAlchemy <= 0.9.9.  This is
143
-            # backwards compat code just in case someone updates
144
-            # Gertty without updating SQLAlchemy.  This is simple
145
-            # enough to check and will hopefully avoid leaving the
146
-            # user's db in an inconsistent state.  Remove this after
147
-            # Gertty 1.2.0.
148
-            columns = key.constraint.columns
138
+        columns = [c.name for c in key.constraint.columns]
149 139
         if (len(columns)==1 and columns[0] in drop_columns):
150 140
             continue
151 141
         # Otherwise, recreate the constraint.

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

@@ -30,7 +30,7 @@ CURSOR_PAGE_DOWN = urwid.CURSOR_PAGE_DOWN
30 30
 CURSOR_MAX_LEFT = urwid.CURSOR_MAX_LEFT
31 31
 CURSOR_MAX_RIGHT = urwid.CURSOR_MAX_RIGHT
32 32
 ACTIVATE = urwid.ACTIVATE
33
-# Global gertty commands:
33
+# Global boartty commands:
34 34
 KILL = 'kill'
35 35
 YANK = 'yank'
36 36
 YANK_POP = 'yank pop'
@@ -38,37 +38,31 @@ PREV_SCREEN = 'previous screen'
38 38
 TOP_SCREEN = 'top screen'
39 39
 HELP = 'help'
40 40
 QUIT = 'quit'
41
-CHANGE_SEARCH = 'change search'
42
-REFINE_CHANGE_SEARCH = 'refine change search'
43
-LIST_HELD = 'list held changes'
44
-# Change screen:
45
-TOGGLE_REVIEWED = 'toggle reviewed'
41
+STORY_SEARCH = 'story search'
42
+REFINE_STORY_SEARCH = 'refine story search'
43
+LIST_HELD = 'list held stories'
44
+NEW_STORY = 'new story'
45
+# Story screen:
46 46
 TOGGLE_HIDDEN = 'toggle hidden'
47 47
 TOGGLE_STARRED = 'toggle starred'
48 48
 TOGGLE_HELD = 'toggle held'
49 49
 TOGGLE_MARK = 'toggle process mark'
50
-REVIEW = 'review'
51
-DIFF = 'diff'
52
-LOCAL_CHECKOUT = 'local checkout'
53
-LOCAL_CHERRY_PICK = 'local cherry pick'
50
+LEAVE_COMMENT = 'leave comment'
54 51
 SEARCH_RESULTS = 'search results'
55
-NEXT_CHANGE = 'next change'
56
-PREV_CHANGE = 'previous change'
52
+NEXT_STORY = 'next story'
53
+PREV_STORY = 'previous story'
57 54
 TOGGLE_HIDDEN_COMMENTS = 'toggle hidden comments'
58
-ABANDON_CHANGE = 'abandon change'
59
-RESTORE_CHANGE = 'restore change'
60
-REBASE_CHANGE = 'rebase change'
61
-CHERRY_PICK_CHANGE = 'cherry pick change'
55
+NEW_TASK = 'new task'
56
+DELETE_TASK = 'delete task'
62 57
 REFRESH = 'refresh'
63
-EDIT_TOPIC = 'edit topic'
64
-EDIT_COMMIT_MESSAGE = 'edit commit message'
65
-SUBMIT_CHANGE = 'submit change'
58
+EDIT_TITLE = 'edit title'
59
+EDIT_DESCRIPTION = 'edit description'
66 60
 SORT_BY_NUMBER = 'sort by number'
67 61
 SORT_BY_UPDATED = 'sort by updated'
68 62
 SORT_BY_LAST_SEEN = 'sort by last seen'
69 63
 SORT_BY_REVERSE = 'reverse the sort'
70 64
 # Project list screen:
71
-TOGGLE_LIST_REVIEWED = 'toggle list reviewed'
65
+TOGGLE_LIST_ACTIVE = 'toggle list active'
72 66
 TOGGLE_LIST_SUBSCRIBED = 'toggle list subscribed'
73 67
 TOGGLE_SUBSCRIBED = 'toggle subscribed'
74 68
 NEW_PROJECT_TOPIC = 'new project topic'
@@ -77,13 +71,11 @@ MOVE_PROJECT_TOPIC = 'move to project topic'
77 71
 COPY_PROJECT_TOPIC = 'copy to project topic'
78 72
 REMOVE_PROJECT_TOPIC = 'remove from project topic'
79 73
 RENAME_PROJECT_TOPIC = 'rename project topic'
80
-# Diff screens:
81
-SELECT_PATCHSETS = 'select patchsets'
74
+# Special:
75
+FURTHER_INPUT = 'further input'
82 76
 NEXT_SELECTABLE = 'next selectable'
83 77
 PREV_SELECTABLE = 'prev selectable'
84 78
 INTERACTIVE_SEARCH = 'interactive search'
85
-# Special:
86
-FURTHER_INPUT = 'further input'
87 79
 
88 80
 DEFAULT_KEYMAP = {
89 81
     REDRAW_SCREEN: 'ctrl l',
@@ -104,37 +96,31 @@ DEFAULT_KEYMAP = {
104 96
     TOP_SCREEN: 'meta home',
105 97
     HELP: ['f1', '?'],
106 98
     QUIT: ['ctrl q'],
107
-    CHANGE_SEARCH: 'ctrl o',
108
-    REFINE_CHANGE_SEARCH: 'meta o',
99
+    STORY_SEARCH: 'ctrl o',
100
+    REFINE_STORY_SEARCH: 'meta o',
109 101
     LIST_HELD: 'f12',
102
+    NEW_STORY: 'ctrl n',
110 103
 
111
-    TOGGLE_REVIEWED: 'v',
112 104
     TOGGLE_HIDDEN: 'k',
113 105
     TOGGLE_STARRED: '*',
114 106
     TOGGLE_HELD: '!',
115 107
     TOGGLE_MARK: '%',
116
-    REVIEW: 'r',
117
-    DIFF: 'd',
118
-    LOCAL_CHECKOUT: 'c',
119
-    LOCAL_CHERRY_PICK: 'x',
108
+    LEAVE_COMMENT: 'r',
120 109
     SEARCH_RESULTS: 'u',
121
-    NEXT_CHANGE: 'n',
122
-    PREV_CHANGE: 'p',
110
+    NEXT_STORY: 'n',
111
+    PREV_STORY: 'p',
123 112
     TOGGLE_HIDDEN_COMMENTS: 't',
124
-    ABANDON_CHANGE: 'ctrl a',
125
-    RESTORE_CHANGE: 'ctrl e',
126
-    REBASE_CHANGE: 'ctrl b',
127
-    CHERRY_PICK_CHANGE: 'ctrl x',
113
+    NEW_TASK: 'N',
114
+    DELETE_TASK: 'delete',
128 115
     REFRESH: 'ctrl r',
129
-    EDIT_TOPIC: 'ctrl t',
130
-    EDIT_COMMIT_MESSAGE: 'ctrl d',
131
-    SUBMIT_CHANGE: 'ctrl u',
116
+    EDIT_TITLE: 'ctrl t',
117
+    EDIT_DESCRIPTION: 'ctrl d',
132 118
     SORT_BY_NUMBER: [['S', 'n']],
133 119
     SORT_BY_UPDATED: [['S', 'u']],
134 120
     SORT_BY_LAST_SEEN: [['S', 's']],
135 121
     SORT_BY_REVERSE: [['S', 'r']],
136 122
 
137
-    TOGGLE_LIST_REVIEWED: 'l',
123
+    TOGGLE_LIST_ACTIVE: 'l',
138 124
     TOGGLE_LIST_SUBSCRIBED: 'L',
139 125
     TOGGLE_SUBSCRIBED: 's',
140 126
     NEW_PROJECT_TOPIC: [['T', 'n']],
@@ -144,7 +130,6 @@ DEFAULT_KEYMAP = {
144 130
     REMOVE_PROJECT_TOPIC: [['T', 'D']],
145 131
     RENAME_PROJECT_TOPIC: [['T', 'r']],
146 132
 
147
-    SELECT_PATCHSETS: 'p',
148 133
     NEXT_SELECTABLE: 'tab',
149 134
     PREV_SELECTABLE: 'shift tab',
150 135
     INTERACTIVE_SEARCH: 'ctrl s',

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

@@ -14,8 +14,8 @@
14 14
 
15 15
 import urwid
16 16
 
17
-from gertty import keymap
18
-from gertty.view import mouse_scroll_decorator
17
+from boartty import keymap
18
+from boartty.view import mouse_scroll_decorator
19 19
 
20 20
 GLOBAL_HELP = (
21 21
     (keymap.HELP,
@@ -25,11 +25,11 @@ GLOBAL_HELP = (
25 25
     (keymap.TOP_SCREEN,
26 26
      "Back to project list"),
27 27
     (keymap.QUIT,
28
-     "Quit Gertty"),
29
-    (keymap.CHANGE_SEARCH,
30
-     "Search for changes"),
28
+     "Quit Boardtty"),
29
+    (keymap.STORY_SEARCH,
30
+     "Search for stories"),
31 31
     (keymap.LIST_HELD,
32
-     "List held changes"),
32
+     "List held stories"),
33 33
     (keymap.KILL,
34 34
      "Kill to end of line (editing)"),
35 35
     (keymap.YANK,
@@ -192,7 +192,8 @@ class LineEditDialog(ButtonDialog):
192 192
 
193 193
 class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
194 194
     signals = ['save', 'cancel']
195
-    def __init__(self, title, prompt, button, text, ring=None):
195
+    def __init__(self, app, title, prompt, button, text, ring=None):
196
+        self.app = app
196 197
         save_button = FixedButton(button)
197 198
         cancel_button = FixedButton('Cancel')
198 199
         urwid.connect_signal(save_button, 'click',
@@ -212,6 +213,16 @@ class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
212 213
         fill = urwid.Filler(pile, valign='top')
213 214
         super(TextEditDialog, self).__init__(urwid.LineBox(fill, title))
214 215
 
216
+    def keypress(self, size, key):
217
+        if not self.app.input_buffer:
218
+            key = super(TextEditDialog, self).keypress(size, key)
219
+        keys = self.app.input_buffer + [key]
220
+        commands = self.app.config.keymap.getCommands(keys)
221
+        if keymap.PREV_SCREEN in commands:
222
+            self._emit('cancel')
223
+            return None
224
+        return key
225
+
215 226
 class MessageDialog(ButtonDialog):
216 227
     signals = ['close']
217 228
     def __init__(self, title, message):
@@ -530,3 +541,76 @@ class MyGridFlow(urwid.GridFlow):
530 541
                                 c.focus_position = i
531 542
                                 break
532 543
         return p
544
+
545
+class SearchSelectInnerButton(urwid.Button):
546
+    def __init__(self, key, value):
547
+        self.key = key
548
+        self.value = value
549
+        super(SearchSelectInnerButton, self).__init__(value)
550
+
551
+class SearchSelectDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
552
+    """
553
+    A dialog that allows a user to select one item from a list, and
554
+    interactively refine the list by searching.
555
+    """
556
+
557
+    signals = ['save']
558
+    def __init__(self, app, title, current_key, values):
559
+        self.app = app
560
+
561
+        rows = []
562
+        self.key = None
563
+        self.value = None
564
+        for key, value in values():
565
+            button = SearchSelectInnerButton(key, value)
566
+            urwid.connect_signal(button, 'click',
567
+                                 lambda b:self.onSelected(b))
568
+            rows.append(button)
569
+
570
+        pile = urwid.Pile(rows)
571
+        fill = urwid.Filler(pile, valign='top')
572
+        super(SearchSelectDialog, self).__init__(urwid.LineBox(fill, title))
573
+
574
+    def onSelected(self, b):
575
+        self.key = b.key
576
+        self.value = b.value
577
+        self._emit('save')
578
+        self.app.backScreen()
579
+
580
+class SearchSelectButton(TextButton):
581
+    """
582
+    A button that displays a value; when clicked, a SearchSelectDialog
583
+    is opened to select a new value.
584
+    """
585
+    signals = ['changed']
586
+    def __init__(self, app, title, key, value, values):
587
+        self.app = app
588
+        self.title = title
589
+        self.values = values
590
+        urwid.connect_signal(self, 'click',
591
+                             lambda button:self.onClick())
592
+        super(SearchSelectButton, self).__init__(u'')
593
+        self.update(key, value)
594
+
595
+    def onClick(self):
596
+        dialog = SearchSelectDialog(self.app, self.title, self.key, self.values)
597
+        urwid.connect_signal(dialog, 'save',
598
+                             lambda d:self.onChanged(d))
599
+        self.app.popup(dialog,
600
+                       relative_width=30, relative_height=75,
601
+                       min_width=30, min_height=20)
602
+
603
+    def update(self, key, value):
604
+        self.key = key
605
+        self.value = value
606
+        if self.value is None:
607
+            label = u'Select'
608
+        else:
609
+            label = self.value
610
+        self.text.set_text(label)
611
+
612
+    def onChanged(self, dialog):
613
+        if dialog.key is None:
614
+            return
615
+        self.update(dialog.key, dialog.value)
616
+        self._emit('changed')

+ 103
- 0
boartty/palette.py View File

@@ -0,0 +1,103 @@
1
+# Copyright 2014 OpenStack Foundation
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+DEFAULT_PALETTE={
16
+    'focused': ['default,standout', ''],
17
+    'header': ['white,bold', 'dark blue'],
18
+    'error': ['light red', 'dark blue'],
19
+    'table-header': ['white,bold', ''],
20
+    'link': ['dark blue', ''],
21
+    'focused-link': ['light blue', ''],
22
+    'footer': ['light gray', 'dark gray'],
23
+    'search-result': ['default,standout', ''],
24
+    # Story view
25
+    'story-data': ['dark cyan', ''],
26
+    'focused-story-data': ['light cyan', ''],
27
+    'story-header': ['light blue', ''],
28
+    'task-id': ['dark cyan', ''],
29
+    'task-title': ['light green', ''],
30
+    'task-project': ['light blue', ''],
31
+    'task-status': ['yellow', ''],
32
+    'task-assignee': ['light cyan', ''],
33
+    'task-note': ['default', ''],
34
+    'focused-task-id': ['dark cyan,standout', ''],
35
+    'focused-task-title': ['light green,standout', ''],
36
+    'focused-task-project': ['light blue,standout', ''],
37
+    'focused-task-status': ['yellow,standout', ''],
38
+    'focused-task-assignee': ['dark cyan,standout', ''],
39
+    'focused-task-note': ['default', ''],
40
+    'story-event-name': ['yellow', ''],
41
+    'story-event-own-name': ['light cyan', ''],
42
+    'story-event-header': ['brown', ''],
43
+    'story-event-own-header': ['dark cyan', ''],
44
+    'story-event-draft': ['dark red', ''],
45
+    'story-event-button': ['dark magenta', ''],
46
+    'focused-story-event-button': ['light magenta', ''],
47
+    # project list
48
+    'active-project': ['white', ''],
49
+    'subscribed-project': ['default', ''],
50
+    'unsubscribed-project': ['dark gray', ''],
51
+    'marked-project': ['light cyan', ''],
52
+    'focused-active-project': ['white,standout', ''],
53
+    'focused-subscribed-project': ['default,standout', ''],
54
+    'focused-unsubscribed-project': ['dark gray,standout', ''],
55
+    'focused-marked-project': ['light cyan,standout', ''],
56
+    # story list
57
+    'active-story': ['default', ''],
58
+    'inactive-story': ['dark gray', ''],
59
+    'focused-active-story': ['default,standout', ''],
60
+    'focused-inactive-story': ['dark gray,standout', ''],
61
+    'starred-story': ['light cyan', ''],
62
+    'focused-starred-story': ['light cyan,standout', ''],
63
+    'held-story': ['light red', ''],
64
+    'focused-held-story': ['light red,standout', ''],
65
+    'marked-story': ['dark cyan', ''],
66
+    'focused-marked-story': ['dark cyan,standout', ''],
67
+    }
68
+
69
+# A delta from the default palette
70
+LIGHT_PALETTE = {
71
+    'table-header': ['black,bold', ''],
72
+    'active-project': ['black', ''],
73
+    'subscribed-project': ['dark gray', ''],
74
+    'unsubscribed-project': ['dark gray', ''],
75
+    'focused-active-project': ['black,standout', ''],
76
+    'focused-subscribed-project': ['dark gray,standout', ''],
77
+    'focused-unsubscribed-project': ['dark gray,standout', ''],
78
+    'story-data': ['dark blue,bold', ''],
79
+    'focused-story-data': ['dark blue,standout', ''],
80
+    'story-event-name': ['brown', ''],
81
+    'story-event-own-name': ['dark blue,bold', ''],
82
+    'story-event-header': ['black', ''],
83
+    'story-event-own-header': ['black,bold', ''],
84
+    'focused-link': ['dark blue,bold', ''],
85
+    }
86
+
87
+class Palette(object):
88
+    def __init__(self, config):
89
+        self.palette = {}
90
+        self.palette.update(DEFAULT_PALETTE)
91
+        self.update(config)
92
+
93
+    def update(self, config):
94
+        d = config.copy()
95
+        if 'name' in d:
96
+            del d['name']
97
+        self.palette.update(d)
98
+
99
+    def getPalette(self):
100
+        ret = []
101
+        for k,v in self.palette.items():
102
+            ret.append(tuple([k]+v))
103
+        return ret

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


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

@@ -15,8 +15,8 @@
15 15
 import sqlalchemy.sql.expression
16 16
 from sqlalchemy.sql.expression import and_
17 17
 
18
-from gertty.search import tokenizer, parser
19
-import gertty.db
18
+from boartty.search import tokenizer, parser
19
+import boartty.db
20 20
 
21 21
 
22 22
 class SearchSyntaxError(Exception):
@@ -35,7 +35,7 @@ class SearchCompiler(object):
35 35
         while stack:
36 36
             x = stack.pop()
37 37
             if hasattr(x, 'table'):
38
-                if (x.table != gertty.db.change_table
38
+                if (x.table != boartty.db.story_table
39 39
                     and hasattr(x.table, 'name')):
40 40
                     tables.add(x.table)
41 41
             for child in x.get_children():
@@ -47,19 +47,19 @@ class SearchCompiler(object):
47 47
         self.parser.username = self.username
48 48
         result = self.parser.parse(data, lexer=self.lexer)
49 49
         tables = self.findTables(result)
50
-        if gertty.db.project_table in tables:
51
-            result = and_(gertty.db.change_table.c.project_key == gertty.db.project_table.c.key,
50
+        if boartty.db.project_table in tables:
51
+            result = and_(boartty.db.story_table.c.project_key == boartty.db.project_table.c.key,
52 52
                           result)
53
-            tables.remove(gertty.db.project_table)
54
-        if gertty.db.account_table in tables:
55
-            result = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key,
53
+            tables.remove(boartty.db.project_table)
54
+        if boartty.db.user_table in tables:
55
+            result = and_(boartty.db.story_table.c.user_key == boartty.db.user_table.c.key,
56 56
                           result)
57
-            tables.remove(gertty.db.account_table)
58
-        if gertty.db.file_table in tables:
59
-            result = and_(gertty.db.file_table.c.revision_key == gertty.db.revision_table.c.key,
60
-                          gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key,
61
-                          result)
62
-            tables.remove(gertty.db.file_table)
57
+            tables.remove(boartty.db.user_table)
58
+        #if boartty.db.file_table in tables:
59
+        #    result = and_(boartty.db.file_table.c.revision_key == boartty.db.revision_table.c.key,
60
+        #                  boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key,
61
+        #                  result)
62
+        #    tables.remove(boartty.db.file_table)
63 63
         if tables:
64 64
             raise Exception("Unknown table in search: %s" % tables)
65 65
         return result

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

@@ -18,9 +18,9 @@ import re
18 18
 import ply.yacc as yacc
19 19
 from sqlalchemy.sql.expression import and_, or_, not_, select, func
20 20
 
21
-import gertty.db
22
-import gertty.search
23
-from gertty.search.tokenizer import tokens  # NOQA
21
+import boartty.db
22
+import boartty.search
23
+from boartty.search.tokenizer import tokens  # NOQA
24 24
 
25 25
 def age_to_delta(delta, unit):
26 26
     if unit in ['seconds', 'second', 'sec', 's']:
@@ -68,7 +68,7 @@ def SearchParser():
68 68
         elif p[2].lower() == 'or':
69 69
             p[0] = or_(p[1], p[3])
70 70
         else:
71
-            raise gertty.search.SearchSyntaxError("Boolean %s not recognized" % p[2])
71
+            raise boartty.search.SearchSyntaxError("Boolean %s not recognized" % p[2])
72 72
 
73 73
     def p_negative_expr(p):
74 74
         '''negative_expr : NOT expression
@@ -78,7 +78,7 @@ def SearchParser():
78 78
     def p_term(p):
79 79
         '''term : age_term
80 80
                 | recentlyseen_term
81
-                | change_term
81
+                | story_term
82 82
                 | owner_term
83 83
                 | reviewer_term
84 84
                 | commit_term
@@ -111,102 +111,107 @@ def SearchParser():
111 111
         delta = p[2]
112 112
         unit = p[3]
113 113
         delta = age_to_delta(delta, unit)
114
-        p[0] = gertty.db.change_table.c.updated < (now-datetime.timedelta(seconds=delta))
114
+        p[0] = boartty.db.story_table.c.updated < (now-datetime.timedelta(seconds=delta))
115 115
 
116 116
     def p_recentlyseen_term(p):
117 117
         '''recentlyseen_term : OP_RECENTLYSEEN NUMBER string'''
118
-        # A gertty extension
118
+        # A boartty extension
119 119
         now = datetime.datetime.utcnow()
120 120
         delta = p[2]
121 121
         unit = p[3]
122 122
         delta = age_to_delta(delta, unit)
123
-        s = select([func.datetime(func.max(gertty.db.change_table.c.last_seen), '-%s seconds' % delta)],
123
+        s = select([func.datetime(func.max(boartty.db.story_table.c.last_seen), '-%s seconds' % delta)],
124 124
                    correlate=False)
125
-        p[0] = gertty.db.change_table.c.last_seen >= s
125
+        p[0] = boartty.db.story_table.c.last_seen >= s
126 126
 
127
-    def p_change_term(p):
128
-        '''change_term : OP_CHANGE CHANGE_ID
129
-                       | OP_CHANGE NUMBER'''
127
+    def p_story_term(p):
128
+        '''story_term : OP_STORY STORY_ID
129
+                       | OP_STORY NUMBER'''
130 130
         if type(p[2]) == int:
131
-            p[0] = gertty.db.change_table.c.number == p[2]
131
+            p[0] = boartty.db.story_table.c.number == p[2]
132 132
         else:
133
-            p[0] = gertty.db.change_table.c.change_id == p[2]
133
+            p[0] = boartty.db.story_table.c.story_id == p[2]
134 134
 
135 135
     def p_owner_term(p):
136 136
         '''owner_term : OP_OWNER string'''
137 137
         if p[2] == 'self':
138 138
             username = p.parser.username
139
-            p[0] = gertty.db.account_table.c.username == username
139
+            p[0] = boartty.db.user_table.c.username == username
140 140
         else:
141
-            p[0] = or_(gertty.db.account_table.c.username == p[2],
142
-                       gertty.db.account_table.c.email == p[2],
143
-                       gertty.db.account_table.c.name == p[2])
141
+            p[0] = or_(boartty.db.user_table.c.username == p[2],
142
+                       boartty.db.user_table.c.email == p[2],
143
+                       boartty.db.user_table.c.name == p[2])
144 144
 
145 145
     def p_reviewer_term(p):
146 146
         '''reviewer_term : OP_REVIEWER string
147 147
                          | OP_REVIEWER NUMBER'''
148 148
         filters = []
149
-        filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
150
-        filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
149
+        filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
150
+        filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
151 151
         try:
152 152
             number = int(p[2])
153 153
         except:
154 154
             number = None
155 155
         if number is not None:
156
-            filters.append(gertty.db.account_table.c.id == number)
156
+            filters.append(boartty.db.user_table.c.id == number)
157 157
         elif p[2] == 'self':
158 158
             username = p.parser.username
159
-            filters.append(gertty.db.account_table.c.username == username)
159
+            filters.append(boartty.db.user_table.c.username == username)
160 160
         else:
161
-            filters.append(or_(gertty.db.account_table.c.username == p[2],
162
-                               gertty.db.account_table.c.email == p[2],
163
-                               gertty.db.account_table.c.name == p[2]))
164
-        s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
165
-        p[0] = gertty.db.change_table.c.key.in_(s)
161
+            filters.append(or_(boartty.db.user_table.c.username == p[2],
162
+                               boartty.db.user_table.c.email == p[2],
163
+                               boartty.db.user_table.c.name == p[2]))
164
+        s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
165
+        p[0] = boartty.db.story_table.c.key.in_(s)
166 166
 
167 167
     def p_commit_term(p):
168 168
         '''commit_term : OP_COMMIT string'''
169 169
         filters = []
170
-        filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
171
-        filters.append(gertty.db.revision_table.c.commit == p[2])
172
-        s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
173
-        p[0] = gertty.db.change_table.c.key.in_(s)
170
+        filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
171
+        filters.append(boartty.db.revision_table.c.commit == p[2])
172
+        s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
173
+        p[0] = boartty.db.story_table.c.key.in_(s)
174 174
 
175 175
     def p_project_term(p):
176 176
         '''project_term : OP_PROJECT string'''
177 177
         if p[2].startswith('^'):
178
-            p[0] = func.matches(p[2], gertty.db.project_table.c.name)
178
+            p[0] = func.matches(p[2], boartty.db.project_table.c.name)
179 179
         else:
180
-            p[0] = gertty.db.project_table.c.name == p[2]
180
+            p[0] = boartty.db.project_table.c.name == p[2]
181 181
 
182 182
     def p_projects_term(p):
183 183
         '''projects_term : OP_PROJECTS string'''
184
-        p[0] = gertty.db.project_table.c.name.like('%s%%' % p[2])
184
+        p[0] = boartty.db.project_table.c.name.like('%s%%' % p[2])
185 185
 
186 186
     def p_project_key_term(p):
187 187
         '''project_key_term : OP_PROJECT_KEY NUMBER'''
188
-        p[0] = gertty.db.change_table.c.project_key == p[2]
188
+        #p[0] = boartty.db.story_table.c.project_key == p[2]
189
+        filters = []
190
+        filters.append(boartty.db.task_table.c.story_key == boartty.db.story_table.c.key)
191
+        filters.append(boartty.db.task_table.c.project_key == p[2])
192
+        s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
193
+        p[0] = boartty.db.story_table.c.key.in_(s)
189 194
 
190 195
     def p_branch_term(p):
191 196
         '''branch_term : OP_BRANCH string'''
192 197
         if p[2].startswith('^'):
193
-            p[0] = func.matches(p[2], gertty.db.change_table.c.branch)
198
+            p[0] = func.matches(p[2], boartty.db.story_table.c.branch)
194 199
         else:
195
-            p[0] = gertty.db.change_table.c.branch == p[2]
200
+            p[0] = boartty.db.story_table.c.branch == p[2]
196 201
 
197 202
     def p_topic_term(p):
198 203
         '''topic_term : OP_TOPIC string'''
199 204
         if p[2].startswith('^'):
200
-            p[0] = func.matches(p[2], gertty.db.change_table.c.topic)
205
+            p[0] = func.matches(p[2], boartty.db.story_table.c.topic)
201 206
         else:
202
-            p[0] = gertty.db.change_table.c.topic == p[2]
207
+            p[0] = boartty.db.story_table.c.topic == p[2]
203 208
 
204 209
     def p_ref_term(p):
205 210
         '''ref_term : OP_REF string'''
206 211
         if p[2].startswith('^'):
207
-            p[0] = func.matches(p[2], 'refs/heads/'+gertty.db.change_table.c.branch)
212
+            p[0] = func.matches(p[2], 'refs/heads/'+boartty.db.story_table.c.branch)
208 213
         else:
209
-            p[0] = gertty.db.change_table.c.branch == p[2][len('refs/heads/'):]
214
+            p[0] = boartty.db.story_table.c.branch == p[2][len('refs/heads/'):]
210 215
 
211 216
     label_re = re.compile(r'(?P<label>[a-zA-Z0-9_-]+([a-zA-Z]|((?<![-+])[0-9])))'
212 217
                           r'(?P<operator>[<>]?=?)(?P<value>[-+]?[0-9]+)'
@@ -221,60 +226,60 @@ def SearchParser():
221 226
         user = args.group('user')
222 227
 
223 228
         filters = []
224
-        filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
225
-        filters.append(gertty.db.approval_table.c.category == label)
229
+        filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
230
+        filters.append(boartty.db.approval_table.c.category == label)
226 231
         if op == '=':
227
-            filters.append(gertty.db.approval_table.c.value == value)
232
+            filters.append(boartty.db.approval_table.c.value == value)
228 233
         elif op == '>=':
229
-            filters.append(gertty.db.approval_table.c.value >= value)
234
+            filters.append(boartty.db.approval_table.c.value >= value)
230 235
         elif op == '<=':
231
-            filters.append(gertty.db.approval_table.c.value <= value)
236
+            filters.append(boartty.db.approval_table.c.value <= value)
232 237
         if user is not None:
233
-            filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
238
+            filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
234 239
             if user == 'self':
235