Merge "Add beginning of support for boards/worklists"

This commit is contained in:
Jenkins 2016-11-23 00:54:49 +00:00 committed by Gerrit Code Review
commit 272481b12a
7 changed files with 896 additions and 2 deletions

View File

@ -45,6 +45,7 @@ from boartty import search
from boartty import requestsexceptions
from boartty.view import story_list as view_story_list
from boartty.view import project_list as view_project_list
from boartty.view import board_list as view_board_list
from boartty.view import story as view_story
import boartty.view
import boartty.version
@ -676,6 +677,9 @@ class App(object):
elif keymap.TOP_SCREEN in commands:
self.clearHistory()
self.refresh(force=True)
elif keymap.BOARD_LIST in commands:
view = view_board_list.BoardListView(self)
self.changeScreen(view)
elif keymap.HELP in commands:
self.help()
elif keymap.QUIT in commands:

View File

@ -78,6 +78,63 @@ story_table = Table(
Column('pending', Boolean, index=True, nullable=False),
Column('pending_delete', Boolean, index=True, nullable=False),
)
board_table = Table(
'board', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('user_key', Integer, ForeignKey("user.key"), index=True),
Column('hidden', Boolean, index=True, nullable=False, default=False),
Column('subscribed', Boolean, index=True, nullable=False, default=False),
Column('title', String(255), index=True),
Column('private', Boolean, nullable=False, default=False),
Column('description', Text),
Column('created', DateTime, index=True),
Column('updated', DateTime, index=True),
Column('last_seen', DateTime, index=True),
Column('pending', Boolean, index=True, nullable=False, default=False),
Column('pending_delete', Boolean, index=True, nullable=False, default=False),
)
lane_table = Table(
'lane', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('board_key', Integer, ForeignKey("board.key"), index=True),
Column('worklist_key', Integer, ForeignKey("worklist.key"), index=True),
Column('position', Integer),
Column('created', DateTime, index=True),
Column('updated', DateTime, index=True),
Column('pending', Boolean, index=True, nullable=False, default=False),
Column('pending_delete', Boolean, index=True, nullable=False, default=False),
)
worklist_table = Table(
'worklist', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('user_key', Integer, ForeignKey("user.key"), index=True),
Column('hidden', Boolean, index=True, nullable=False, default=False),
Column('subscribed', Boolean, index=True, nullable=False, default=False),
Column('title', String(255), index=True),
Column('private', Boolean, nullable=False, default=False),
Column('automatic', Boolean, nullable=False, default=False),
Column('created', DateTime, index=True),
Column('updated', DateTime, index=True),
Column('last_seen', DateTime, index=True),
Column('pending', Boolean, index=True, nullable=False, default=False),
Column('pending_delete', Boolean, index=True, nullable=False, default=False),
)
worklist_item_table = Table(
'worklist_item', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('worklist_key', Integer, ForeignKey("worklist.key"), index=True),
Column('story_key', Integer, ForeignKey("story.key"), index=True),
Column('task_key', Integer, ForeignKey("task.key"), index=True),
Column('position', Integer),
Column('created', DateTime, index=True),
Column('updated', DateTime, index=True),
Column('pending', Boolean, index=True, nullable=False, default=False),
Column('pending_delete', Boolean, index=True, nullable=False, default=False),
)
tag_table = Table(
'tag', metadata,
Column('key', Integer, primary_key=True),
@ -235,6 +292,10 @@ class Story(object):
self.pending = pending
self.pending_delete = False
def __repr__(self):
return '<Story key=%s id=%s title=%s>' % (
self.key, self.id, self.title)
@property
def creator_name(self):
return format_name(self)
@ -297,6 +358,10 @@ class Task(object):
self.created = created
self.project = project
def __repr__(self):
return '<Task key=%s id=%s title=%s, project=%s>' % (
self.key, self.id, self.title, self.project)
class Event(object):
def __init__(self, id=None, type=None, creator=None, created=None, info=None):
self.id = id
@ -333,6 +398,58 @@ class Comment(object):
self.pending_delete = pending_delete
self.draft = draft
class Board(object):
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
def __repr__(self):
return '<Board key=%s id=%s title=%s>' % (
self.key, self.id, self.title)
def addLane(self, *args, **kw):
session = Session.object_session(self)
l = Lane(*args, **kw)
session.add(l)
session.flush()
l.board = self
return l
class Lane(object):
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
def __repr__(self):
return '<Lane key=%s id=%s worklist=%s>' % (
self.key, self.id, self.worklist)
class Worklist(object):
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
def __repr__(self):
return '<Worklist key=%s id=%s title=%s>' % (
self.key, self.id, self.title)
def addItem(self, *args, **kw):
session = Session.object_session(self)
i = WorklistItem(*args, **kw)
session.add(i)
session.flush()
i.worklist = self
return i
class WorklistItem(object):
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
def __repr__(self):
return '<WorklistItem key=%s id=%s story=%s task=%s>' % (
self.key, self.id, self.story, self.task)
class SyncQuery(object):
def __init__(self, name):
self.name = name
@ -390,6 +507,25 @@ mapper(Event, event_table, properties=dict(
mapper(Comment, comment_table, properties=dict(
parent=relationship(Comment, remote_side=[comment_table.c.key],backref='children'),
))
mapper(Board, board_table, properties=dict(
lanes=relationship(Lane,
order_by=lane_table.c.position),
creator=relationship(User),
))
mapper(Lane, lane_table, properties=dict(
board=relationship(Board),
worklist=relationship(Worklist),
))
mapper(Worklist, worklist_table, properties=dict(
items=relationship(WorklistItem,
order_by=worklist_item_table.c.position),
creator=relationship(User),
))
mapper(WorklistItem, worklist_item_table, properties=dict(
worklist=relationship(Worklist),
story=relationship(Story),
task=relationship(Task),
))
mapper(SyncQuery, sync_query_table)
def match(expr, item):
@ -407,8 +543,8 @@ class Database(object):
self.dburi = dburi
self.search = search
self.engine = create_engine(self.dburi)
#metadata.create_all(self.engine)
self.migrate(app)
metadata.create_all(self.engine)
#self.migrate(app)
# If we want the objects returned from query() to be usable
# outside of the session, we need to expunge them from the session,
# and since the DatabaseSession always calls commit() on the session
@ -632,6 +768,64 @@ class DatabaseSession(object):
except sqlalchemy.orm.exc.NoResultFound:
return None
def getBoards(self, subscribed=False):
query = self.session().query(Board)
if subscribed:
query = query.filter_by(subscribed=subscribed)
return query.order_by(Board.title).all()
def getBoard(self, key):
query = self.session().query(Board).filter_by(key=key)
try:
return query.one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getBoardByID(self, id):
try:
return self.session().query(Board).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getLane(self, key):
query = self.session().query(Lane).filter_by(key=key)
try:
return query.one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getLaneByID(self, id):
try:
return self.session().query(Lane).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getWorklist(self, key):
query = self.session().query(Worklist).filter_by(key=key)
try:
return query.one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getWorklistByID(self, id):
try:
return self.session().query(Worklist).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getWorklistItem(self, key):
query = self.session().query(WorklistItem).filter_by(key=key)
try:
return query.one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getWorklistItemByID(self, id):
try:
return self.session().query(WorklistItem).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def createProject(self, *args, **kw):
o = Project(*args, **kw)
self.session().add(o)
@ -644,6 +838,18 @@ class DatabaseSession(object):
self.session().flush()
return s
def createBoard(self, *args, **kw):
s = Board(*args, **kw)
self.session().add(s)
self.session().flush()
return s
def createWorklist(self, *args, **kw):
s = Worklist(*args, **kw)
self.session().add(s)
self.session().flush()
return s
def createUser(self, *args, **kw):
a = User(*args, **kw)
self.session().add(a)

View File

@ -42,6 +42,7 @@ STORY_SEARCH = 'story search'
REFINE_STORY_SEARCH = 'refine story search'
LIST_HELD = 'list held stories'
NEW_STORY = 'new story'
BOARD_LIST = 'board list'
# Story screen:
TOGGLE_HIDDEN = 'toggle hidden'
TOGGLE_STARRED = 'toggle starred'
@ -94,6 +95,7 @@ DEFAULT_KEYMAP = {
PREV_SCREEN: 'esc',
TOP_SCREEN: 'meta home',
BOARD_LIST: 'f6',
HELP: ['f1', '?'],
QUIT: ['ctrl q'],
STORY_SEARCH: 'ctrl o',

View File

@ -144,8 +144,41 @@ class StoryUpdatedEvent(UpdateEvent):
def __init__(self, story, status_changed=False):
self.story_key = story.key
self.status_changed = status_changed
self.updateRelatedProjects(story)
class BoardAddedEvent(UpdateEvent):
def __repr__(self):
return '<BoardAddedEvent board_key:%s>' % (
self.board_key)
def __init__(self, board):
self.board_key = board.key
class BoardUpdatedEvent(UpdateEvent):
def __repr__(self):
return '<BoardUpdatedEvent board_key:%s>' % (
self.board_key)
def __init__(self, board):
self.board_key = board.key
class WorklistAddedEvent(UpdateEvent):
def __repr__(self):
return '<WorklistAddedEvent worklist_key:%s>' % (
self.worklist_key)
def __init__(self, worklist):
self.worklist_key = worklist.key
class WorklistUpdatedEvent(UpdateEvent):
def __repr__(self):
return '<WorklistUpdatedEvent worklist_key:%s>' % (
self.worklist_key)
def __init__(self, worklist):
self.worklist_key = worklist.key
def parseDateTime(dt):
if dt is None:
return None
@ -550,6 +583,27 @@ class SyncStoryTask(Task):
self.results.append(StoryUpdatedEvent(story,
status_changed=status_changed))
class SyncStoryByTaskTask(Task):
def __init__(self, task_id, priority=NORMAL_PRIORITY):
super(SyncStoryByTaskTask, self).__init__(priority)
self.task_id = task_id
def __repr__(self):
return '<SyncStoryByTaskTask %s>' % (self.task_id,)
def __eq__(self, other):
if (other.__class__ == self.__class__ and
other.task_id == self.task_id):
return True
return False
def run(self, sync):
app = sync.app
remote = sync.get('v1/tasks/%s' % (self.task_id,))
self.tasks.append(sync.submitTask(SyncStoryTask(
remote['story_id'], priority=self.priority)))
class SetProjectUpdatedTask(Task):
def __init__(self, project_key, updated, priority=NORMAL_PRIORITY):
super(SetProjectUpdatedTask, self).__init__(priority)
@ -572,6 +626,206 @@ class SetProjectUpdatedTask(Task):
project = session.getProject(self.project_key)
project.updated = self.updated
class SyncBoardsTask(Task):
def __init__(self, priority=NORMAL_PRIORITY):
super(SyncBoardsTask, self).__init__(priority)
def __repr__(self):
return '<SyncBoardsTask>'
def __eq__(self, other):
if (other.__class__ == self.__class__):
return True
return False
#TODO: updated since, deleted
def run(self, sync):
app = sync.app
remote = sync.get('v1/boards')
for remote_board in remote:
t = SyncBoardTask(remote_board['id'], remote_board,
priority=self.priority)
sync.submitTask(t)
self.tasks.append(t)
class SyncWorklistsTask(Task):
def __init__(self, priority=NORMAL_PRIORITY):
super(SyncWorklistsTask, self).__init__(priority)
def __repr__(self):
return '<SyncWorklistsTask>'
def __eq__(self, other):
if (other.__class__ == self.__class__):
return True
return False
#TODO: updated since, deleted
def run(self, sync):
app = sync.app
remote = sync.get('v1/worklists')
for remote_worklist in remote:
t = SyncWorklistTask(remote_worklist['id'], remote_worklist,
priority=self.priority)
sync.submitTask(t)
self.tasks.append(t)
class SyncBoardTask(Task):
def __init__(self, board_id, data=None, priority=NORMAL_PRIORITY):
super(SyncBoardTask, self).__init__(priority)
self.board_id = board_id
self.data = data
def __repr__(self):
return '<SyncBoardTask %s>' % (self.board_id,)
def __eq__(self, other):
if (other.__class__ == self.__class__ and
other.board_id == self.board_id and
other.data == self.data):
return True
return False
def updateLanes(self, sync, session, board, remote_lanes):
local_lane_ids = set([l.id for l in board.lanes])
remote_lane_ids = set()
for remote_lane in remote_lanes:
remote_lane_ids.add(remote_lane['id'])
if remote_lane['id'] not in local_lane_ids:
self.log.debug("Adding to board id %s lane %s" %
(board.id, remote_lane,))
remote_created = parseDateTime(remote_lane['created_at'])
lane = board.addLane(id=remote_lane['id'],
position=remote_lane['position'],
created=remote_created)
else:
lane = session.getLane(remote_lane['id'])
lane.updated = parseDateTime(remote_lane['updated_at'])
t = SyncWorklistTask(remote_lane['worklist']['id'],
priority=self.priority)
t._run(sync, session, remote_lane['worklist'])
lane.worklist = session.getWorklistByID(remote_lane['worklist']['id'])
for local_lane in board.lanes[:]:
if local_lane.id not in remote_lane_ids:
session.delete(lane)
def run(self, sync):
app = sync.app
if self.data is None:
remote_board = sync.get('v1/boards/%s' % (self.board_id,))
else:
remote_board = self.data
with app.db.getSession() as session:
board = session.getBoardByID(remote_board['id'])
added = False
if not board:
board = session.createBoard(id=remote_board['id'])
sync.log.info("Created new board %s in local DB.", board.id)
added = True
board.title = remote_board['title']
board.description = remote_board['description']
board.updated = parseDateTime(remote_board['updated_at'])
board.creator = getUser(sync, session,
remote_board['creator_id'])
board.created = parseDateTime(remote_board['created_at'])
self.updateLanes(sync, session, board, remote_board['lanes'])
if added:
self.results.append(BoardAddedEvent(board))
else:
self.results.append(BoardUpdatedEvent(board))
class SyncWorklistTask(Task):
def __init__(self, worklist_id, data=None, priority=NORMAL_PRIORITY):
super(SyncWorklistTask, self).__init__(priority)
self.worklist_id = worklist_id
self.data = data
def __repr__(self):
return '<SyncWorklistTask %s>' % (self.worklist_id,)
def __eq__(self, other):
if (other.__class__ == self.__class__ and
other.worklist_id == self.worklist_id and
other.data == self.data):
return True
return False
def updateItems(self, sync, session, worklist, remote_items):
local_item_ids = set([l.id for l in worklist.items])
remote_item_ids = set()
reenqueue = False
for remote_item in remote_items:
remote_item_ids.add(remote_item['id'])
if remote_item['id'] not in local_item_ids:
self.log.debug("Adding to worklist id %s item %s" %
(worklist.id, remote_item,))
remote_created = parseDateTime(remote_item['created_at'])
self.log.debug("Create item %s", remote_item['id'])
item = worklist.addItem(id=remote_item['id'],
position=remote_item['list_position'],
created=remote_created)
else:
self.log.debug("Get item %s", remote_item['id'])
item = session.getWorklistItemByID(remote_item['id'])
self.log.debug("Using item %s", item)
item.updated = parseDateTime(remote_item['updated_at'])
if remote_item['item_type'] == 'story':
item.story = session.getStoryByID(remote_item['item_id'])
self.log.debug("Story %s", item.story)
if item.story is None:
self.tasks.append(sync.submitTask(SyncStoryTask(
remote_item['item_id'], priority=self.priority)))
reenqueue = True
if remote_item['item_type'] == 'task':
item.task = session.getTaskByID(remote_item['item_id'])
self.log.debug("Task %s", item.task)
if item.task is None:
self.tasks.append(sync.submitTask(SyncStoryByTaskTask(
remote_item['item_id'], priority=self.priority)))
reenqueue = True
if reenqueue:
self.tasks.append(sync.submitTask(SyncWorklistTask(
self.worklist_id, self.data, priority=self.priority)))
for local_item in worklist.items[:]:
if local_item.id not in remote_item_ids:
session.delete(item)
def run(self, sync):
app = sync.app
if self.data is None:
remote_worklist = sync.get('v1/worklists/%s' % (self.worklist_id,))
else:
remote_worklist = self.data
with app.db.getSession() as session:
return self._run(sync, session, remote_worklist)
def _run(self, sync, session, remote_worklist):
worklist = session.getWorklistByID(remote_worklist['id'])
added = False
if not worklist:
worklist = session.createWorklist(id=remote_worklist['id'])
sync.log.info("Created new worklist %s in local DB.", worklist.id)
added = True
worklist.title = remote_worklist['title']
worklist.updated = parseDateTime(remote_worklist['updated_at'])
worklist.creator = getUser(sync, session,
remote_worklist['creator_id'])
worklist.created = parseDateTime(remote_worklist['created_at'])
self.updateItems(sync, session, worklist, remote_worklist['items'])
if added:
self.results.append(WorklistAddedEvent(worklist))
else:
self.results.append(WorklistUpdatedEvent(worklist))
#storyboard
class SyncQueriedChangesTask(Task):
def __init__(self, query_name, query, priority=NORMAL_PRIORITY):
@ -954,6 +1208,8 @@ class Sync(object):
self.submitTask(SyncUserListTask(HIGH_PRIORITY))
self.submitTask(SyncProjectSubscriptionsTask(NORMAL_PRIORITY))
self.submitTask(SyncSubscribedProjectsTask(NORMAL_PRIORITY))
self.submitTask(SyncBoardsTask(NORMAL_PRIORITY))
self.submitTask(SyncWorklistsTask(NORMAL_PRIORITY))
#self.submitTask(SyncSubscribedProjectBranchesTask(LOW_PRIORITY))
#self.submitTask(SyncOutdatedChangesTask(LOW_PRIORITY))
#self.submitTask(PruneDatabaseTask(self.app.config.expire_age, LOW_PRIORITY))
@ -976,11 +1232,13 @@ class Sync(object):
self.log.exception('Exception in periodicSync')
def submitTask(self, task):
self.log.debug("Enqueue %s", task)
if not self.offline:
if not self.queue.put(task, task.priority):
task.complete(False)
else:
task.complete(False)
return task
def run(self, pipe):
task = None

122
boartty/view/board.py Normal file
View File

@ -0,0 +1,122 @@
# 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 boartty import keymap
from boartty import mywid
from boartty import sync
from boartty.view import mouse_scroll_decorator
# +-----listbox---+
# |table pile |
# | |
# |+------+-cols-+|
# ||+----+|+----+||
# ||| ||| |||
# |||pile|||pile|||
# ||| ||| |||
# ||+----+|+----+||
# |+------+------+|
# +---------------+
class BoardView(urwid.WidgetWrap, mywid.Searchable):
def getCommands(self):
return [
(keymap.REFRESH,
"Sync subscribed boards"),
(keymap.INTERACTIVE_SEARCH,
"Interactive search"),
]
def help(self):
key = self.app.config.keymap.formatKeys
commands = self.getCommands()
return [(c[0], key(c[0]), c[1]) for c in commands]
def interested(self, event):
if not (isinstance(event, sync.BoardAddedEvent)
or
isinstance(event, sync.StoryAddedEvent)
or
(isinstance(event, sync.StoryUpdatedEvent) and
event.status_changed)):
self.log.debug("Ignoring refresh board due to event %s" % (event,))
return False
self.log.debug("Refreshing board due to event %s" % (event,))
return True
def __init__(self, app, board_key):
super(BoardView, self).__init__(urwid.Pile([]))
self.log = logging.getLogger('boartty.view.board')
self.searchInit()
self.app = app
self.board_key = board_key
self.title_label = urwid.Text(u'', wrap='clip')
self.description_label = urwid.Text(u'', wrap='clip')
board_info = []
board_info_map={'story-data': 'focused-story-data'}
for l, v in [("Title", self.title_label),
("Description", self.description_label),
]:
row = urwid.Columns([(12, urwid.Text(('story-header', l), wrap='clip')), v])
board_info.append(row)
board_info = urwid.Pile(board_info)
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
self._w.contents.append((self.app.header, ('pack', 1)))
self._w.contents.append((urwid.Divider(), ('pack', 1)))
self._w.contents.append((self.listbox, ('weight', 1)))
self._w.set_focus(2)
self.listbox.body.append(board_info)
self.listbox.body.append(urwid.Divider())
self.listbox_board_start = len(self.listbox.body)
self.refresh()
def refresh(self):
with self.app.db.getSession() as session:
board = session.getBoard(self.board_key)
self.log.debug("Display board %s", board)
self.title = board.title
self.app.status.update(title=self.title)
self.title_label.set_text(('story-data', board.title))
self.description_label.set_text(('story-data', board.description))
columns = []
for lane in board.lanes:
items = []
self.log.debug("Display lane %s", lane)
items.append(urwid.Text(lane.worklist.title))
for item in lane.worklist.items:
self.log.debug("Display item %s", item)
items.append(urwid.Text(item.story.title))
pile = urwid.Pile(items)
columns.append(pile)
columns = urwid.Columns(columns)
self.listbox.body.append(columns)
def handleCommands(self, commands):
if keymap.REFRESH in commands:
self.app.sync.submitTask(
sync.SyncBoardTask(self.board_key, sync.HIGH_PRIORITY))
self.app.status.update()
self.refresh()
return True
if keymap.INTERACTIVE_SEARCH in commands:
self.searchStart()
return True
return False

301
boartty/view/board_list.py Normal file
View File

@ -0,0 +1,301 @@
# 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 boartty import keymap
from boartty import mywid
from boartty import sync
from boartty.view import board as view_board
from boartty.view import mouse_scroll_decorator
ACTIVE_COL_WIDTH = 7
class BoardRow(urwid.Button):
board_focus_map = {None: 'focused',
'active-project': 'focused-active-project',
'subscribed-project': 'focused-subscribed-project',
'unsubscribed-project': 'focused-unsubscribed-project',
'marked-project': 'focused-marked-project',
}
def selectable(self):
return True
def _setTitle(self, title, indent):
self.board_title = title
title = indent+title
if self.mark:
title = '%'+title
else:
title = ' '+title
self.title.set_text(title)
def __init__(self, app, board, topic, callback=None):
super(BoardRow, self).__init__('', on_press=callback,
user_data=(board.key, board.title))
self.app = app
self.mark = False
self._style = None
self.board_key = board.key
if topic:
self.topic_key = topic.key
self.indent = ' '
else:
self.topic_key = None
self.indent = ''
self.board_title = board.title
self.title = mywid.SearchableText('')
self._setTitle(board.title, self.indent)
self.title.set_wrap_mode('clip')
self.active_stories = urwid.Text(u'', align=urwid.RIGHT)
col = urwid.Columns([
self.title,
('fixed', ACTIVE_COL_WIDTH, self.active_stories),
])
self.row_style = urwid.AttrMap(col, '')
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.board_focus_map)
self.update(board)
def search(self, search, attribute):
return self.title.search(search, attribute)
def update(self, board):
if board.subscribed:
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.active_stories.set_text('%i ' % cache['active_stories'])
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._setTitle(self.board_title, self.indent)
class BoardListHeader(urwid.WidgetWrap):
def __init__(self):
cols = [urwid.Text(u' Board'),
(ACTIVE_COL_WIDTH, urwid.Text(u'Active'))]
super(BoardListHeader, self).__init__(urwid.Columns(cols))
@mouse_scroll_decorator.ScrollByWheel
class BoardListView(urwid.WidgetWrap, mywid.Searchable):
def getCommands(self):
return [
(keymap.TOGGLE_LIST_SUBSCRIBED,
"Toggle whether only subscribed boards or all boards are listed"),
(keymap.TOGGLE_LIST_ACTIVE,
"Toggle listing of boards with active changes"),
(keymap.TOGGLE_SUBSCRIBED,
"Toggle the subscription flag for the selected board"),
(keymap.REFRESH,
"Sync subscribed boards"),
(keymap.TOGGLE_MARK,
"Toggle the process mark for the selected board"),
(keymap.INTERACTIVE_SEARCH,
"Interactive search"),
]
def help(self):
key = self.app.config.keymap.formatKeys
commands = self.getCommands()
return [(c[0], key(c[0]), c[1]) for c in commands]
def __init__(self, app):
super(BoardListView, self).__init__(urwid.Pile([]))
self.log = logging.getLogger('boartty.view.board_list')
self.searchInit()
self.app = app
self.active = True
self.subscribed = False #True
self.board_rows = {}
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
self.header = BoardListHeader()
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.BoardAddedEvent)
or
isinstance(event, sync.StoryAddedEvent)
or
(isinstance(event, sync.StoryUpdatedEvent) and
event.status_changed)):
self.log.debug("Ignoring refresh board list due to event %s" % (event,))
return False
self.log.debug("Refreshing board 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, BoardRow):
del self.board_rows[(row.topic_key, row.board_key)]
else:
del self.topic_rows[row.topic_key]
def _boardRow(self, i, board, topic):
# Ensure that the row at i is the given board. 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, board.key)
row = self.board_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, BoardRow) and
current_row.board_key == board.key):
break
self._deleteRow(current_row)
if not row:
row = BoardRow(self.app, board, topic, self.onSelect)
self.listbox.body.insert(i, row)
self.board_rows[key] = row
else:
row.update(board)
return i+1
def refresh(self):
if self.subscribed:
self.title = u'Subscribed boards'
self.short_title = self.title[:]
if self.active:
self.title += u' with active stories'
else:
self.title = u'All boards'
self.short_title = self.title[:]
self.app.status.update(title=self.title)
with self.app.db.getSession() as session:
i = 0
for board in session.getBoards(subscribed=self.subscribed):
#self.log.debug("board: %s" % board.name)
i = self._boardRow(i, board, None)
while i < len(self.listbox.body):
current_row = self.listbox.body[i]
self._deleteRow(current_row)
def toggleSubscribed(self, board_key):
with self.app.db.getSession() as session:
board = session.getBoard(board_key)
board.subscribed = not board.subscribed
ret = board.subscribed
return ret
def onSelect(self, button, data):
board_key, board_name = data
self.app.changeScreen(view_board.BoardView(self.app, board_key))
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 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 toggleSubscribed(self):
rows = self.getSelectedRows(BoardRow)
if not rows:
return
keys = [row.board_key for row in rows]
subscribed_keys = []
with self.app.db.getSession() as session:
for key in keys:
board = session.getBoard(key)
board.subscribed = not board.subscribed
if board.subscribed:
subscribed_keys.append(key)
for row in rows:
if row.mark:
row.toggleMark()
for key in subscribed_keys:
self.app.sync.submitTask(sync.SyncBoardTask(key))
self.refresh()
def keypress(self, size, key):
if self.searchKeypress(size, key):
return None
if not self.app.input_buffer:
key = super(BoardListView, 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_ACTIVE in commands:
self.active = not self.active
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.REFRESH in commands:
self.app.sync.submitTask(
sync.SyncSubscribedBoardsTask(sync.HIGH_PRIORITY))
self.app.status.update()
self.refresh()
return True
if keymap.INTERACTIVE_SEARCH in commands:
self.searchStart()
return True
return False

View File

@ -591,6 +591,7 @@ class StoryView(urwid.WidgetWrap):
# The set of task keys currently displayed
unseen_keys = set(self.task_rows.keys())
for task in story.tasks:
self.log.debug(task)
if task.pending_delete:
continue
row = self.task_rows.get(task.key)