
Rather than requiring that the name be entered. Change-Id: I187fa7be2c83601f3962258c7818836fff08758d
577 lines
22 KiB
Python
577 lines
22 KiB
Python
# Copyright 2014 OpenStack Foundation
|
|
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import logging
|
|
import urwid
|
|
|
|
from gertty import keymap
|
|
from gertty import mywid
|
|
from gertty import sync
|
|
from gertty.view import change_list as view_change_list
|
|
from gertty.view import mouse_scroll_decorator
|
|
|
|
class TopicSelectDialog(urwid.WidgetWrap):
|
|
signals = ['ok', 'cancel']
|
|
|
|
def __init__(self, title, topics):
|
|
button_widgets = []
|
|
ok_button = mywid.FixedButton('OK')
|
|
cancel_button = mywid.FixedButton('Cancel')
|
|
urwid.connect_signal(ok_button, 'click',
|
|
lambda button:self._emit('ok'))
|
|
urwid.connect_signal(cancel_button, 'click',
|
|
lambda button:self._emit('cancel'))
|
|
button_widgets.append(('pack', ok_button))
|
|
button_widgets.append(('pack', cancel_button))
|
|
button_columns = urwid.Columns(button_widgets, dividechars=2)
|
|
|
|
self.topic_buttons = []
|
|
self.topic_keys = {}
|
|
rows = []
|
|
for key, name in topics:
|
|
button = mywid.FixedRadioButton(self.topic_buttons, name)
|
|
self.topic_keys[button] = key
|
|
rows.append(button)
|
|
|
|
rows.append(urwid.Divider())
|
|
rows.append(button_columns)
|
|
pile = urwid.Pile(rows)
|
|
fill = urwid.Filler(pile, valign='top')
|
|
super(TopicSelectDialog, self).__init__(urwid.LineBox(fill, title))
|
|
|
|
def getSelected(self):
|
|
for b in self.topic_buttons:
|
|
if b.state:
|
|
return self.topic_keys[b]
|
|
return None
|
|
|
|
class ProjectRow(urwid.Button):
|
|
project_focus_map = {None: 'focused',
|
|
'unreviewed-project': 'focused-unreviewed-project',
|
|
'subscribed-project': 'focused-subscribed-project',
|
|
'unsubscribed-project': 'focused-unsubscribed-project',
|
|
'marked-project': 'focused-marked-project',
|
|
}
|
|
|
|
def selectable(self):
|
|
return True
|
|
|
|
def _setName(self, name):
|
|
self.project_name = name
|
|
if self.mark:
|
|
name = '%'+name
|
|
else:
|
|
name = ' '+name
|
|
self.name.set_text(name)
|
|
|
|
def __init__(self, app, project, topic, callback=None):
|
|
super(ProjectRow, self).__init__('', on_press=callback,
|
|
user_data=(project.key, project.name))
|
|
self.app = app
|
|
self.mark = False
|
|
self._style = None
|
|
self.project_key = project.key
|
|
if topic:
|
|
self.topic_key = topic.key
|
|
else:
|
|
self.topic_key = None
|
|
self.project_name = project.name
|
|
self.name = urwid.Text('')
|
|
self._setName(project.name)
|
|
self.name.set_wrap_mode('clip')
|
|
self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
|
|
self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
|
|
col = urwid.Columns([
|
|
self.name,
|
|
('fixed', 11, self.unreviewed_changes),
|
|
('fixed', 5, self.open_changes),
|
|
])
|
|
self.row_style = urwid.AttrMap(col, '')
|
|
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
|
|
self.update(project)
|
|
|
|
def update(self, project):
|
|
cache = self.app.project_cache.get(project)
|
|
if project.subscribed:
|
|
if cache['unreviewed_changes'] > 0:
|
|
style = 'unreviewed-project'
|
|
else:
|
|
style = 'subscribed-project'
|
|
else:
|
|
style = 'unsubscribed-project'
|
|
self._style = style
|
|
if self.mark:
|
|
style = 'marked-project'
|
|
self.row_style.set_attr_map({None: style})
|
|
self.unreviewed_changes.set_text('%i ' % cache['unreviewed_changes'])
|
|
self.open_changes.set_text('%i ' % cache['open_changes'])
|
|
|
|
def toggleMark(self):
|
|
self.mark = not self.mark
|
|
if self.mark:
|
|
style = 'marked-project'
|
|
else:
|
|
style = self._style
|
|
self.row_style.set_attr_map({None: style})
|
|
self._setName(self.project_name)
|
|
|
|
class TopicRow(urwid.Button):
|
|
project_focus_map = {None: 'focused',
|
|
'subscribed-project': 'focused-subscribed-project',
|
|
'marked-project': 'focused-marked-project',
|
|
}
|
|
|
|
def selectable(self):
|
|
return True
|
|
|
|
def _setName(self, name):
|
|
self.topic_name = name
|
|
name = '[[ '+name+' ]]'
|
|
if self.mark:
|
|
name = '%'+name
|
|
else:
|
|
name = ' '+name
|
|
self.name.set_text(name)
|
|
|
|
def __init__(self, topic, callback=None):
|
|
super(TopicRow, self).__init__('', on_press=callback,
|
|
user_data=(topic.key, topic.name))
|
|
self.mark = False
|
|
self._style = None
|
|
self.topic_key = topic.key
|
|
self.name = urwid.Text('')
|
|
self._setName(topic.name)
|
|
self.name.set_wrap_mode('clip')
|
|
self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
|
|
self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
|
|
col = urwid.Columns([
|
|
self.name,
|
|
('fixed', 11, self.unreviewed_changes),
|
|
('fixed', 5, self.open_changes),
|
|
])
|
|
self.row_style = urwid.AttrMap(col, '')
|
|
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
|
|
self._style = 'subscribed-project'
|
|
self.row_style.set_attr_map({None: self._style})
|
|
self.update(topic)
|
|
|
|
def update(self, topic, unreviewed_changes=None, open_changes=None):
|
|
self._setName(topic.name)
|
|
if unreviewed_changes is None:
|
|
self.unreviewed_changes.set_text('')
|
|
else:
|
|
self.unreviewed_changes.set_text('%i ' % unreviewed_changes)
|
|
if open_changes is None:
|
|
self.open_changes.set_text('')
|
|
else:
|
|
self.open_changes.set_text('%i ' % open_changes)
|
|
|
|
def toggleMark(self):
|
|
self.mark = not self.mark
|
|
if self.mark:
|
|
style = 'marked-project'
|
|
else:
|
|
style = self._style
|
|
self.row_style.set_attr_map({None: style})
|
|
self._setName(self.topic_name)
|
|
|
|
class ProjectListHeader(urwid.WidgetWrap):
|
|
def __init__(self):
|
|
cols = [urwid.Text(u' Project'),
|
|
(11, urwid.Text(u'Unreviewed')),
|
|
(5, urwid.Text(u'Open'))]
|
|
super(ProjectListHeader, self).__init__(urwid.Columns(cols))
|
|
|
|
@mouse_scroll_decorator.ScrollByWheel
|
|
class ProjectListView(urwid.WidgetWrap):
|
|
def help(self):
|
|
key = self.app.config.keymap.formatKeys
|
|
return [
|
|
(key(keymap.TOGGLE_LIST_SUBSCRIBED),
|
|
"Toggle whether only subscribed projects or all projects are listed"),
|
|
(key(keymap.TOGGLE_LIST_REVIEWED),
|
|
"Toggle listing of projects with unreviewed changes"),
|
|
(key(keymap.TOGGLE_SUBSCRIBED),
|
|
"Toggle the subscription flag for the selected project"),
|
|
(key(keymap.REFRESH),
|
|
"Sync subscribed projects"),
|
|
(key(keymap.TOGGLE_MARK),
|
|
"Toggle the process mark for the selected project"),
|
|
(key(keymap.NEW_PROJECT_TOPIC),
|
|
"Create project topic"),
|
|
(key(keymap.DELETE_PROJECT_TOPIC),
|
|
"Delete selected project topic"),
|
|
(key(keymap.MOVE_PROJECT_TOPIC),
|
|
"Move selected project to topic"),
|
|
(key(keymap.COPY_PROJECT_TOPIC),
|
|
"Copy selected project to topic"),
|
|
(key(keymap.REMOVE_PROJECT_TOPIC),
|
|
"Remove selected project from topic"),
|
|
(key(keymap.RENAME_PROJECT_TOPIC),
|
|
"Rename selected project topic"),
|
|
]
|
|
|
|
def __init__(self, app):
|
|
super(ProjectListView, self).__init__(urwid.Pile([]))
|
|
self.log = logging.getLogger('gertty.view.project_list')
|
|
self.app = app
|
|
self.unreviewed = True
|
|
self.subscribed = True
|
|
self.project_rows = {}
|
|
self.topic_rows = {}
|
|
self.open_topics = set()
|
|
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
|
|
self.header = ProjectListHeader()
|
|
self.refresh()
|
|
self._w.contents.append((app.header, ('pack', 1)))
|
|
self._w.contents.append((urwid.Divider(),('pack', 1)))
|
|
self._w.contents.append((urwid.AttrWrap(self.header, 'table-header'), ('pack', 1)))
|
|
self._w.contents.append((self.listbox, ('weight', 1)))
|
|
self._w.set_focus(3)
|
|
|
|
def interested(self, event):
|
|
if not (isinstance(event, sync.ProjectAddedEvent)
|
|
or
|
|
isinstance(event, sync.ChangeAddedEvent)
|
|
or
|
|
(isinstance(event, sync.ChangeUpdatedEvent) and
|
|
(event.status_changed or event.review_flag_changed))):
|
|
self.log.debug("Ignoring refresh project list due to event %s" % (event,))
|
|
return False
|
|
self.log.debug("Refreshing project list due to event %s" % (event,))
|
|
return True
|
|
|
|
def advance(self):
|
|
pos = self.listbox.focus_position
|
|
if pos < len(self.listbox.body)-1:
|
|
pos += 1
|
|
self.listbox.focus_position = pos
|
|
|
|
def _deleteRow(self, row):
|
|
if row in self.listbox.body:
|
|
self.listbox.body.remove(row)
|
|
if isinstance(row, ProjectRow):
|
|
del self.project_rows[(row.topic_key, row.project_key)]
|
|
else:
|
|
del self.topic_rows[row.topic_key]
|
|
|
|
def _projectRow(self, i, project, topic):
|
|
# Ensure that the row at i is the given project. If the row
|
|
# already exists somewhere in the list, delete all rows
|
|
# between i and the row and then update the row. If the row
|
|
# does not exist, insert the row at position i.
|
|
topic_key = topic and topic.key or None
|
|
key = (topic_key, project.key)
|
|
row = self.project_rows.get(key)
|
|
while row: # This is "if row: while True:".
|
|
if i >= len(self.listbox.body):
|
|
break
|
|
current_row = self.listbox.body[i]
|
|
if (isinstance(current_row, ProjectRow) and
|
|
current_row.project_key == project.key):
|
|
break
|
|
self._deleteRow(current_row)
|
|
if not row:
|
|
row = ProjectRow(self.app, project, topic, self.onSelect)
|
|
self.listbox.body.insert(i, row)
|
|
self.project_rows[key] = row
|
|
else:
|
|
row.update(project)
|
|
return i+1
|
|
|
|
def _topicRow(self, i, topic):
|
|
row = self.topic_rows.get(topic.key)
|
|
while row: # This is "if row: while True:".
|
|
if i >= len(self.listbox.body):
|
|
break
|
|
current_row = self.listbox.body[i]
|
|
if (isinstance(current_row, TopicRow) and
|
|
current_row.topic_key == topic.key):
|
|
break
|
|
self._deleteRow(current_row)
|
|
if not row:
|
|
row = TopicRow(topic, self.onSelectTopic)
|
|
self.listbox.body.insert(i, row)
|
|
self.topic_rows[topic.key] = row
|
|
else:
|
|
row.update(topic)
|
|
return i + 1
|
|
|
|
def refresh(self):
|
|
if self.subscribed:
|
|
self.title = u'Subscribed projects'
|
|
if self.unreviewed:
|
|
self.title += u' with unreviewed changes'
|
|
else:
|
|
self.title = u'All projects'
|
|
self.app.status.update(title=self.title)
|
|
with self.app.db.getSession() as session:
|
|
i = 0
|
|
for project in session.getProjects(topicless=True,
|
|
subscribed=self.subscribed, unreviewed=self.unreviewed):
|
|
#self.log.debug("project: %s" % project.name)
|
|
i = self._projectRow(i, project, None)
|
|
for topic in session.getTopics():
|
|
#self.log.debug("topic: %s" % topic.name)
|
|
i = self._topicRow(i, topic)
|
|
topic_unreviewed = 0
|
|
topic_open = 0
|
|
for project in topic.projects:
|
|
#self.log.debug(" project: %s" % project.name)
|
|
cache = self.app.project_cache.get(project)
|
|
topic_unreviewed += cache['unreviewed_changes']
|
|
topic_open += cache['open_changes']
|
|
if self.subscribed:
|
|
if not project.subscribed:
|
|
continue
|
|
if self.unreviewed and not cache['unreviewed_changes']:
|
|
continue
|
|
if topic.key in self.open_topics:
|
|
i = self._projectRow(i, project, topic)
|
|
topic_row = self.topic_rows.get(topic.key)
|
|
topic_row.update(topic, topic_unreviewed, topic_open)
|
|
while i < len(self.listbox.body):
|
|
current_row = self.listbox.body[i]
|
|
self._deleteRow(current_row)
|
|
|
|
def toggleSubscribed(self, project_key):
|
|
with self.app.db.getSession() as session:
|
|
project = session.getProject(project_key)
|
|
project.subscribed = not project.subscribed
|
|
ret = project.subscribed
|
|
return ret
|
|
|
|
def onSelect(self, button, data):
|
|
project_key, project_name = data
|
|
self.app.changeScreen(view_change_list.ChangeListView(
|
|
self.app,
|
|
"_project_key:%s %s" % (project_key, self.app.config.project_change_list_query),
|
|
project_name, project_key=project_key, unreviewed=True))
|
|
|
|
def onSelectTopic(self, button, data):
|
|
topic_key = data[0]
|
|
self.open_topics ^= set([topic_key])
|
|
self.refresh()
|
|
|
|
def toggleMark(self):
|
|
if not len(self.listbox.body):
|
|
return
|
|
pos = self.listbox.focus_position
|
|
row = self.listbox.body[pos]
|
|
row.toggleMark()
|
|
self.advance()
|
|
|
|
def createTopic(self):
|
|
dialog = mywid.LineEditDialog(self.app, 'Topic', 'Create a new topic.',
|
|
'Topic: ', '', self.app.ring)
|
|
urwid.connect_signal(dialog, 'save',
|
|
lambda button: self.closeCreateTopic(dialog, True))
|
|
urwid.connect_signal(dialog, 'cancel',
|
|
lambda button: self.closeCreateTopic(dialog, False))
|
|
self.app.popup(dialog)
|
|
|
|
def closeCreateTopic(self, dialog, save):
|
|
if save:
|
|
last_topic_key = None
|
|
for row in self.listbox.body:
|
|
if isinstance(row, TopicRow):
|
|
last_topic_key = row.topic_key
|
|
with self.app.db.getSession() as session:
|
|
if last_topic_key:
|
|
last_topic = session.getTopic(last_topic_key)
|
|
seq = last_topic.sequence + 1
|
|
else:
|
|
seq = 0
|
|
t = session.createTopic(dialog.entry.edit_text, seq)
|
|
self.app.backScreen()
|
|
|
|
def deleteTopic(self):
|
|
rows = self.getSelectedRows(TopicRow)
|
|
if not rows:
|
|
return
|
|
with self.app.db.getSession() as session:
|
|
for row in rows:
|
|
topic = session.getTopic(row.topic_key)
|
|
session.delete(topic)
|
|
self.refresh()
|
|
|
|
def renameTopic(self):
|
|
pos = self.listbox.focus_position
|
|
row = self.listbox.body[pos]
|
|
if not isinstance(row, TopicRow):
|
|
return
|
|
with self.app.db.getSession() as session:
|
|
topic = session.getTopic(row.topic_key)
|
|
name = topic.name
|
|
key = topic.key
|
|
dialog = mywid.LineEditDialog(self.app, 'Topic', 'Rename a new topic.',
|
|
'Topic: ', name, self.app.ring)
|
|
urwid.connect_signal(dialog, 'save',
|
|
lambda button: self.closeRenameTopic(dialog, True, key))
|
|
urwid.connect_signal(dialog, 'cancel',
|
|
lambda button: self.closeRenameTopic(dialog, False, key))
|
|
self.app.popup(dialog)
|
|
|
|
def closeRenameTopic(self, dialog, save, key):
|
|
if save:
|
|
with self.app.db.getSession() as session:
|
|
topic = session.getTopic(key)
|
|
topic.name = dialog.entry.edit_text
|
|
self.app.backScreen()
|
|
|
|
def getSelectedRows(self, cls):
|
|
ret = []
|
|
for row in self.listbox.body:
|
|
if isinstance(row, cls) and row.mark:
|
|
ret.append(row)
|
|
if ret:
|
|
return ret
|
|
pos = self.listbox.focus_position
|
|
row = self.listbox.body[pos]
|
|
if isinstance(row, cls):
|
|
return [row]
|
|
return []
|
|
|
|
def copyMoveToTopic(self, move):
|
|
if move:
|
|
verb = 'Move'
|
|
else:
|
|
verb = 'Copy'
|
|
rows = self.getSelectedRows(ProjectRow)
|
|
if not rows:
|
|
return
|
|
|
|
with self.app.db.getSession() as session:
|
|
topics = [(t.key, t.name) for t in session.getTopics()]
|
|
|
|
dialog = TopicSelectDialog('%s to Topic' % verb, topics)
|
|
urwid.connect_signal(dialog, 'ok',
|
|
lambda button: self.closeCopyMoveToTopic(dialog, True, rows, move))
|
|
urwid.connect_signal(dialog, 'cancel',
|
|
lambda button: self.closeCopyMoveToTopic(dialog, False, rows, move))
|
|
self.app.popup(dialog)
|
|
|
|
def closeCopyMoveToTopic(self, dialog, save, rows, move):
|
|
error = None
|
|
if save:
|
|
with self.app.db.getSession() as session:
|
|
key = dialog.getSelected()
|
|
new_topic = session.getTopic(key)
|
|
if not new_topic:
|
|
error = "Unable to find topic %s" % topic_name
|
|
else:
|
|
for row in rows:
|
|
project = session.getProject(row.project_key)
|
|
if move and row.topic_key:
|
|
old_topic = session.getTopic(row.topic_key)
|
|
self.log.debug("Remove %s from %s" % (project, old_topic))
|
|
old_topic.removeProject(project)
|
|
self.log.debug("Add %s to %s" % (project, new_topic))
|
|
new_topic.addProject(project)
|
|
self.app.backScreen()
|
|
if error:
|
|
self.app.error(error)
|
|
|
|
def moveToTopic(self):
|
|
self.copyMoveToTopic(True)
|
|
|
|
def copyToTopic(self):
|
|
self.copyMoveToTopic(False)
|
|
|
|
def removeFromTopic(self):
|
|
rows = self.getSelectedRows(ProjectRow)
|
|
rows = [r for r in rows if r.topic_key]
|
|
if not rows:
|
|
return
|
|
with self.app.db.getSession() as session:
|
|
for row in rows:
|
|
project = session.getProject(row.project_key)
|
|
topic = session.getTopic(row.topic_key)
|
|
self.log.debug("Remove %s from %s" % (project, topic))
|
|
topic.removeProject(project)
|
|
self.refresh()
|
|
|
|
def toggleSubscribed(self):
|
|
rows = self.getSelectedRows(ProjectRow)
|
|
if not rows:
|
|
return
|
|
keys = [row.project_key for row in rows]
|
|
subscribed_keys = []
|
|
with self.app.db.getSession() as session:
|
|
for key in keys:
|
|
project = session.getProject(key)
|
|
project.subscribed = not project.subscribed
|
|
if project.subscribed:
|
|
subscribed_keys.append(key)
|
|
for row in rows:
|
|
if row.mark:
|
|
row.toggleMark()
|
|
for key in subscribed_keys:
|
|
self.app.sync.submitTask(sync.SyncProjectTask(key))
|
|
self.refresh()
|
|
|
|
def keypress(self, size, key):
|
|
if not self.app.input_buffer:
|
|
key = super(ProjectListView, self).keypress(size, key)
|
|
keys = self.app.input_buffer + [key]
|
|
commands = self.app.config.keymap.getCommands(keys)
|
|
ret = self.handleCommands(commands)
|
|
if ret is True:
|
|
if keymap.FURTHER_INPUT not in commands:
|
|
self.app.clearInputBuffer()
|
|
return None
|
|
return key
|
|
|
|
def handleCommands(self, commands):
|
|
if keymap.TOGGLE_LIST_REVIEWED in commands:
|
|
self.unreviewed = not self.unreviewed
|
|
self.refresh()
|
|
return True
|
|
if keymap.TOGGLE_LIST_SUBSCRIBED in commands:
|
|
self.subscribed = not self.subscribed
|
|
self.refresh()
|
|
return True
|
|
if keymap.TOGGLE_SUBSCRIBED in commands:
|
|
self.toggleSubscribed()
|
|
return True
|
|
if keymap.TOGGLE_MARK in commands:
|
|
self.toggleMark()
|
|
return True
|
|
if keymap.NEW_PROJECT_TOPIC in commands:
|
|
self.createTopic()
|
|
return True
|
|
if keymap.DELETE_PROJECT_TOPIC in commands:
|
|
self.deleteTopic()
|
|
return True
|
|
if keymap.COPY_PROJECT_TOPIC in commands:
|
|
self.copyToTopic()
|
|
return True
|
|
if keymap.MOVE_PROJECT_TOPIC in commands:
|
|
self.moveToTopic()
|
|
return True
|
|
if keymap.REMOVE_PROJECT_TOPIC in commands:
|
|
self.removeFromTopic()
|
|
return True
|
|
if keymap.RENAME_PROJECT_TOPIC in commands:
|
|
self.renameTopic()
|
|
return True
|
|
if keymap.REFRESH in commands:
|
|
self.app.sync.submitTask(
|
|
sync.SyncSubscribedProjectsTask(sync.HIGH_PRIORITY))
|
|
self.app.status.update()
|
|
self.refresh()
|
|
return True
|
|
return False
|