Add beginning of support for boards/worklists
Change-Id: I3bd2a564c84cf999767d6ce185fc1279ab2d2de5
This commit is contained in:
parent
93fce87599
commit
ba407b3549
@ -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:
|
||||
|
210
boartty/db.py
210
boartty/db.py
@ -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)
|
||||
|
@ -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',
|
||||
|
258
boartty/sync.py
258
boartty/sync.py
@ -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
122
boartty/view/board.py
Normal 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
301
boartty/view/board_list.py
Normal 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
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user