diff --git a/gertty/search/__init__.py b/gertty/search/__init__.py index 83a7185..1cf47f5 100644 --- a/gertty/search/__init__.py +++ b/gertty/search/__init__.py @@ -60,6 +60,10 @@ class SearchCompiler(object): result = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key, result) tables.remove(gertty.db.account_table) + if gertty.db.hashtag_table in tables: + result = and_(gertty.db.hashtag_table.c.change_key == gertty.db.change_table.c.key, + result) + tables.remove(gertty.db.hashtag_table) if gertty.db.file_table in tables: # We only want to look at files for the most recent # revision. diff --git a/gertty/search/parser.py b/gertty/search/parser.py index 9186290..0e4d445 100644 --- a/gertty/search/parser.py +++ b/gertty/search/parser.py @@ -87,6 +87,7 @@ def SearchParser(): | project_key_term | branch_term | topic_term + | hashtag_term | ref_term | label_term | message_term @@ -203,6 +204,13 @@ def SearchParser(): p[0] = and_(gertty.db.change_table.c.topic.isnot(None), gertty.db.change_table.c.topic == p[2]) + def p_hashtag_term(p): + '''hashtag_term : OP_HASHTAG string''' + if p[2].startswith('^'): + p[0] = func.matches(p[2], gertty.db.hashtag_table.c.name) + else: + p[0] = gertty.db.hashtag_table.c.name == p[2] + def p_ref_term(p): '''ref_term : OP_REF string''' if p[2].startswith('^'): diff --git a/gertty/search/tokenizer.py b/gertty/search/tokenizer.py index 6f6a415..3070f26 100644 --- a/gertty/search/tokenizer.py +++ b/gertty/search/tokenizer.py @@ -29,6 +29,7 @@ operators = { '_project_key': 'OP_PROJECT_KEY', # internal gertty use only 'branch': 'OP_BRANCH', 'topic': 'OP_TOPIC', + 'hashtag': 'OP_HASHTAG', 'ref': 'OP_REF', #'tr': 'OP_TR', # needs trackingids #'bug': 'OP_BUG', # needs trackingids diff --git a/gertty/view/change.py b/gertty/view/change.py index 8f38c07..69e8ede 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -595,7 +595,7 @@ class ChangeView(urwid.WidgetWrap): self.project_label = mywid.TextButton(u'', on_press=self.searchProject) self.branch_label = urwid.Text(u'', wrap='clip') self.topic_label = mywid.TextButton(u'', on_press=self.searchTopic) - self.hashtags_label = urwid.Text(u'', wrap='clip') + self.hashtags_label = mywid.HyperText(u'') self.created_label = urwid.Text(u'', wrap='clip') self.updated_label = urwid.Text(u'', wrap='clip') self.status_label = urwid.Text(u'', wrap='clip') @@ -735,7 +735,16 @@ class ChangeView(urwid.WidgetWrap): self.project_label.text.set_text(('change-data', change.project.name)) self.branch_label.set_text(('change-data', change.branch)) self.topic_label.text.set_text(('change-data', self.topic)) - self.hashtags_label.set_text(('change-data', ' '.join([x.name for x in change.hashtags]))) + hashtag_buttons = [] + for x in change.hashtags: + if hashtag_buttons: + hashtag_buttons.append(' ') + link = mywid.Link(x.name, 'change-data', 'focused-change-data') + urwid.connect_signal( + link, 'selected', + lambda link, x=x: self.searchHashtags(x.name)) + hashtag_buttons.append(link) + self.hashtags_label.set_text(('change-data', hashtag_buttons or u'')) self.created_label.set_text(('change-data', str(self.app.time(change.created)))) self.updated_label.set_text(('change-data', str(self.app.time(change.updated)))) stat = change.wip and 'WIP' or change.status @@ -1300,6 +1309,9 @@ class ChangeView(urwid.WidgetWrap): if self.topic: self.app.doSearch("status:open topic:%s" % (self.topic,)) + def searchHashtags(self, name): + self.app.doSearch("status:open hashtag:%s" % (name,)) + def reviewKey(self, reviewkey): approvals = {} for a in reviewkey['approvals']: diff --git a/gertty/view/change_list.py b/gertty/view/change_list.py index c80c05b..3522bc0 100644 --- a/gertty/view/change_list.py +++ b/gertty/view/change_list.py @@ -362,6 +362,8 @@ class ChangeListView(urwid.WidgetWrap, mywid.Searchable): "Abandon the marked changes"), (keymap.EDIT_TOPIC, "Set the topic of the marked changes"), + (keymap.EDIT_HASHTAGS, + "Edit the hashtags of this change"), (keymap.RESTORE_CHANGE, "Restore the marked changes"), (keymap.REFRESH, @@ -755,6 +757,9 @@ class ChangeListView(urwid.WidgetWrap, mywid.Searchable): if keymap.EDIT_TOPIC in commands: self.editTopic() return True + if keymap.EDIT_HASHTAGS in commands: + self.editHashtags() + return None if keymap.REFRESH in commands: if self.project_key: self.app.sync.submitTask( @@ -880,6 +885,31 @@ class ChangeListView(urwid.WidgetWrap, mywid.Searchable): self.app.backScreen() self.refresh() + def editHashtags(self): + dialog = view_change.EditHashtagsDialog(self.app, '') + urwid.connect_signal(dialog, 'save', + lambda button: self.closeEditHashtags(dialog, True)) + urwid.connect_signal(dialog, 'cancel', + lambda button: self.closeEditHashtags(dialog, False)) + self.app.popup(dialog) + + def closeEditHashtags(self, dialog, save): + if save: + rows = [row for row in self.change_rows.values() if row.mark] + if not rows: + pos = self.listbox.focus_position + rows = [self.listbox.body[pos]] + change_keys = [row.change_key for row in rows] + with self.app.db.getSession() as session: + for change_key in change_keys: + change = session.getChange(change_key) + change.setHashtags([x.strip() for x in dialog.entry.edit_text.split(',')]) + change.pending_hashtags = True + self.app.sync.submitTask( + sync.SetHashtagsTask(change_key, sync.HIGH_PRIORITY)) + self.app.backScreen() + self.refresh() + def abandonChange(self): dialog = mywid.TextEditDialog(u'Abandon Change', u'Abandon message:', u'Abandon Change', u'')