# Copyright 2014 OpenStack Foundation # Copyright 2014 Hewlett-Packard Development Company, L.P. # Copyright 2016 Red Hat, Inc # # 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 collections import datetime import logging try: import ordereddict except: pass import textwrap from six.moves.urllib import parse as urlparse import urwid from boartty import keymap from boartty import mywid from boartty import sync from boartty.view import mouse_scroll_decorator import boartty.view try: OrderedDict = collections.OrderedDict except AttributeError: OrderedDict = ordereddict.OrderedDict class NewStoryDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin): signals = ['save', 'cancel'] def __init__(self, app, project_key): self.app = app save_button = mywid.FixedButton(u'Save') cancel_button = mywid.FixedButton(u'Cancel') urwid.connect_signal(save_button, 'click', lambda button:self._emit('save')) urwid.connect_signal(cancel_button, 'click', lambda button:self._emit('cancel')) rows = [] buttons = [('pack', save_button), ('pack', cancel_button)] buttons = urwid.Columns(buttons, dividechars=2) if project_key: with self.app.db.getSession() as session: project = session.getProject(project_key) project_name = project.name else: project_name = None self.project_button = ProjectButton(self.app, project_key, project_name) self.title_field = mywid.MyEdit(u'', edit_text=u'', ring=app.ring) self.description_field = mywid.MyEdit(u'', edit_text='', multiline=True, ring=app.ring) for (label, w) in [ (u'Title:', self.title_field), (u'Description:', self.description_field), (u'Project:', ('pack', self.project_button)), ]: row = urwid.Columns([(12, urwid.Text(label)), w]) rows.append(row) rows.append(urwid.Divider()) rows.append(buttons) pile = urwid.Pile(rows) fill = urwid.Filler(pile, valign='top') super(NewStoryDialog, self).__init__(urwid.LineBox(fill, 'New Story')) class ProjectButton(mywid.SearchSelectButton): def __init__(self, app, key=None, value=None): self.app = app super(ProjectButton, self).__init__(app, 'Select Project', key, value, self.getValues) def getValues(self): with self.app.db.getSession() as session: projects = session.getProjects() for project in projects: yield (project.key, project.name) class StatusButton(mywid.SearchSelectButton): def __init__(self, app): self.app = app super(StatusButton, self).__init__(app, 'Select Status', 'todo', 'todo', self.getValues) def getValues(self): return [('todo', 'todo'), ('merged', 'merged'), ('invalid', 'invalid'), ('review', 'review'), ('inprogress', 'inprogress'), ] class AssigneeButton(mywid.SearchSelectButton): def __init__(self, app): self.app = app super(AssigneeButton, self).__init__(app, 'Select Assignee', None, None, self.getValues) def getValues(self): with self.app.db.getSession() as session: users = session.getUsers() for user in users: if user.name is not None: yield (user.key, user.name) class NewTaskDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin): signals = ['save', 'cancel'] def __init__(self, app): self.app = app save_button = mywid.FixedButton(u'Save') cancel_button = mywid.FixedButton(u'Cancel') urwid.connect_signal(save_button, 'click', lambda button:self._emit('save')) urwid.connect_signal(cancel_button, 'click', lambda button:self._emit('cancel')) rows = [] buttons = [('pack', save_button), ('pack', cancel_button)] buttons = urwid.Columns(buttons, dividechars=2) self.project_button = ProjectButton(self.app) self.status_button = StatusButton(self.app) self.assignee_button = AssigneeButton(self.app) self.title_field = mywid.MyEdit(u'', edit_text=u'', ring=app.ring) for (label, w) in [ (u'Project:', ('pack', self.project_button)), (u'Title:', self.title_field), (u'Status:', ('pack', self.status_button)), (u'Assignee:', ('pack', self.assignee_button)), ]: row = urwid.Columns([(12, urwid.Text(label)), w]) rows.append(row) rows.append(urwid.Divider()) rows.append(buttons) pile = urwid.Pile(rows) fill = urwid.Filler(pile, valign='top') super(NewTaskDialog, self).__init__(urwid.LineBox(fill, 'New Task')) class TaskRow(urwid.WidgetWrap): task_focus_map = { 'task-title': 'focused-task-title', 'task-project': 'focused-task-project', 'task-status': 'focused-task-status', 'task-assignee': 'focused-task-assignee', 'task-note': 'focused-task-note', } def keypress(self, size, key): if not self.app.input_buffer: key = super(TaskRow, self).keypress(size, key) keys = self.app.input_buffer + [key] commands = self.app.config.keymap.getCommands(keys) if keymap.DELETE_TASK in commands: self.delete() return None return key def __init__(self, app, story_view, task): super(TaskRow, self).__init__(urwid.Pile([])) self.app = app self.story_view = story_view self.task_key = task.key self._note = u'' self.taskid = mywid.TextButton(self._note) urwid.connect_signal(self.taskid, 'click', lambda b:self.editNote(b)) self.project = ProjectButton(self.app) urwid.connect_signal(self.project, 'changed', lambda b:self.updateProject(b)) self.status = StatusButton(self.app) urwid.connect_signal(self.status, 'changed', lambda b:self.updateStatus(b)) self._title = u'' self.title = mywid.TextButton(self._title) urwid.connect_signal(self.title, 'click', lambda b:self.editTitle(b)) self.assignee = AssigneeButton(self.app) urwid.connect_signal(self.assignee, 'changed', lambda b:self.updateAssignee(b)) self.description = urwid.Text(u'') self.columns = urwid.Columns([], dividechars=1) for (widget, attr, packing) in [ (self.taskid, 'task-id', ('given', 4, False)), (self.project, 'task-project', ('weight', 1, False)), (self.title, 'task-title', ('weight', 2, False)), (self.status, 'task-status', ('weight', 1, False)), (self.assignee, 'task-assignee', ('weight', 1, False)), ]: w = urwid.AttrMap(urwid.Padding(widget, width='pack'), attr, focus_map={'focused': 'focused-'+attr}) self.columns.contents.append((w, packing)) self.pile = urwid.Pile([self.columns]) self.note = urwid.Text(u'') self.note_visible = False self.note_columns = urwid.Columns([], dividechars=1) self.note_columns.contents.append((urwid.Text(u''), ('given', 1, False))) self.note_columns.contents.append((self.note, ('weight', 1, False))) self._w = urwid.AttrMap(self.pile, None)#, focus_map=self.task_focus_map) self.refresh(task) def setNote(self, note): if note: self._note = note self.note.set_text(('task-note', self._note)) if not self.note_visible: self.pile.contents.append((self.note_columns, ('weight', 1))) self.note_visible = True elif self.note_visible: for x in self.pile.contents[:]: if x[0] is self.note_columns: self.pile.contents.remove(x) self.note_visible = False def refresh(self, task): self.taskid.text.set_text(str(task.id)) self.project.update(task.project.key, task.project.name) self.status.update(task.status, task.status) self._title = task.title self.title.text.set_text(self._title) self.setNote(task.link) if task.assignee: self.assignee.update(task.assignee.key, task.assignee.name) else: self.assignee.update(None, 'Unassigned') def updateProject(self, project_button): with self.app.db.getSession() as session: task = session.getTask(self.task_key) project = session.getProject(project_button.key) task.project = project self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) def updateStatus(self, status_button): with self.app.db.getSession() as session: task = session.getTask(self.task_key) task.status = status_button.key self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) def updateAssignee(self, assignee_button): with self.app.db.getSession() as session: task = session.getTask(self.task_key) user = session.getUser(assignee_button.key) task.assignee = user self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) def editTitle(self, title_button): dialog = mywid.LineEditDialog(self.app, 'Edit Task Title', '', 'Title: ', self._title, ring=self.app.ring) urwid.connect_signal(dialog, 'save', lambda button: self.updateTitle(dialog, True)) urwid.connect_signal(dialog, 'cancel', lambda button: self.updateTitle(dialog, False)) self.app.popup(dialog) def updateTitle(self, dialog, save): if save: with self.app.db.getSession() as session: task = session.getTask(self.task_key) task.title = dialog.entry.edit_text self._title = task.title self.title.text.set_text(self._title) self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) self.app.backScreen() def editNote(self, note_button): dialog = mywid.LineEditDialog(self.app, 'Edit Task Note', '', 'Note: ', self._note, ring=self.app.ring) urwid.connect_signal(dialog, 'save', lambda button: self.updateNote(dialog, True)) urwid.connect_signal(dialog, 'cancel', lambda button: self.updateNote(dialog, False)) self.app.popup(dialog) def updateNote(self, dialog, save): if save: with self.app.db.getSession() as session: task = session.getTask(self.task_key) task.link = dialog.entry.edit_text or None self.setNote(task.link) self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) self.app.backScreen() def delete(self): dialog = mywid.YesNoDialog(u'Delete Task', u'Are you sure you want to delete this task?') urwid.connect_signal(dialog, 'no', lambda d: self.app.backScreen()) urwid.connect_signal(dialog, 'yes', self.finishDelete) self.app.popup(dialog) def finishDelete(self, dialog): with self.app.db.getSession() as session: task = session.getTask(self.task_key) task.pending_delete = True self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) self.app.backScreen() self.story_view.refresh() def search(self, search, attribute): if self.title.search(search, attribute): return True return False class StoryButton(urwid.Button): button_left = urwid.Text(u' ') button_right = urwid.Text(u' ') def __init__(self, story_view, story_key, text): super(StoryButton, self).__init__('') self.set_label(text) self.story_view = story_view self.story_key = story_key urwid.connect_signal(self, 'click', lambda button: self.openStory()) def set_label(self, text): super(StoryButton, self).set_label(text) def openStory(self): try: self.story_view.app.changeScreen(StoryView(self.story_view.app, self.story_key)) except boartty.view.DisplayError as e: self.story_view.app.error(e.message) class StoryEventBox(mywid.HyperText): def __init__(self, story_view, event): super(StoryEventBox, self).__init__(u'') self.story_view = story_view self.app = story_view.app self.refresh(event) def formatReply(self): text = self.comment_text pgraphs = [] pgraph_accumulator = [] wrap = True for line in text.split('\n'): if line.startswith('> '): wrap = False line = '> ' + line if not line: if pgraph_accumulator: pgraphs.append((wrap, '\n'.join(pgraph_accumulator))) pgraph_accumulator = [] wrap = True continue pgraph_accumulator.append(line) if pgraph_accumulator: pgraphs.append((wrap, '\n'.join(pgraph_accumulator))) pgraph_accumulator = [] wrap = True wrapper = textwrap.TextWrapper(initial_indent='> ', subsequent_indent='> ') wrapped_pgraphs = [] for wrap, pgraph in pgraphs: if wrap: wrapped_pgraphs.append('\n'.join(wrapper.wrap(pgraph))) else: wrapped_pgraphs.append(pgraph) return '\n>\n'.join(wrapped_pgraphs) def reply(self): reply_text = self.formatReply() if reply_text: reply_text = self.event_creator + ' wrote:\n\n' + reply_text + '\n' self.story_view.leaveComment(reply_text=reply_text) def refresh(self, event): self.event_id = event.id self.event_creator = event.creator_name description = event.description if event.comment: comment = event.comment.content else: comment = '' self.comment_text = comment created = self.app.time(event.created) lines = comment.split('\n') if event.creator.id == self.app.user_id: name_style = 'story-event-own-name' header_style = 'story-event-own-header' creator_string = event.creator.name else: name_style = 'story-event-name' header_style = 'story-event-header' if event.creator.email: creator_string = "%s <%s>" % ( event.creator.name, event.creator.email) else: creator_string = event.creator.name text = [(name_style, creator_string), (header_style, ': '+description), (header_style, created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))] if event.comment and event.comment.draft and not event.comment.pending: text.append(('story-event-draft', ' (draft)')) elif event.comment: link = mywid.Link('< Reply >', 'story-event-button', 'focused-story-event-button') urwid.connect_signal(link, 'selected', lambda link:self.reply()) text.append(' ') text.append(link) text.append('\n') if lines and lines[-1]: lines.append('') comment_text = ['\n'.join(lines)] for commentlink in self.app.config.commentlinks: comment_text = commentlink.run(self.app, comment_text) info = event.info or '' if info: info = [info + '\n'] else: info = [] self.set_text(text+comment_text+info) def search(self, search, attribute): if self.text.search(search, attribute): return True return False class DescriptionBox(mywid.HyperText): def __init__(self, app, description): self.app = app super(DescriptionBox, self).__init__(description) def set_text(self, text): text = [text] for commentlink in self.app.config.commentlinks: text = commentlink.run(self.app, text) super(DescriptionBox, self).set_text(text) @mouse_scroll_decorator.ScrollByWheel class StoryView(urwid.WidgetWrap, mywid.Searchable): def getCommands(self): return [ (keymap.TOGGLE_HIDDEN, "Toggle the hidden flag for the current story"), (keymap.NEXT_STORY, "Go to the next story in the list"), (keymap.PREV_STORY, "Go to the previous story in the list"), (keymap.LEAVE_COMMENT, "Leave a comment on the story"), (keymap.NEW_TASK, "Add a new task to the current story"), (keymap.TOGGLE_HELD, "Toggle the held flag for the current story"), (keymap.TOGGLE_HIDDEN_COMMENTS, "Toggle display of hidden comments"), (keymap.SEARCH_RESULTS, "Back to the list of stories"), (keymap.TOGGLE_STARRED, "Toggle the starred flag for the current story"), (keymap.EDIT_DESCRIPTION, "Edit the commit message of this story"), (keymap.REFRESH, "Refresh this story"), (keymap.EDIT_TITLE, "Edit the title of this story"), (keymap.EDIT_TAGS, "Edit this story's tags"), (keymap.INTERACTIVE_SEARCH, "Interactive search"), ] def help(self): key = self.app.config.keymap.formatKeys commands = self.getCommands() ret = [(c[0], key(c[0]), c[1]) for c in commands] for k in self.app.config.reviewkeys.values(): action = ', '.join(['{category}:{value}'.format(**a) for a in k['approvals']]) ret.append(('', keymap.formatKey(k['key']), action)) return ret def __init__(self, app, story_key): super(StoryView, self).__init__(urwid.Pile([])) self.log = logging.getLogger('boartty.view.story') self.searchInit() self.app = app self.story_key = story_key self.task_rows = {} self.event_rows = {} self.hide_events = True self.marked_seen = False self.title_label = urwid.Text(u'', wrap='clip') self.creator_label = mywid.TextButton(u'', on_press=self.searchCreator) self.tags_label = urwid.Text(u'', wrap='clip') self.created_label = urwid.Text(u'', wrap='clip') self.updated_label = urwid.Text(u'', wrap='clip') self.status_label = urwid.Text(u'', wrap='clip') self.permalink_label = mywid.TextButton(u'', on_press=self.openPermalink) story_info = [] story_info_map={'story-data': 'focused-story-data'} for l, v in [("Title", self.title_label), ("Creator", urwid.Padding(urwid.AttrMap(self.creator_label, None, focus_map=story_info_map), width='pack')), ("Tags", urwid.Padding(urwid.AttrMap(self.tags_label, None, focus_map=story_info_map), width='pack')), ("Created", self.created_label), ("Updated", self.updated_label), ("Status", self.status_label), ("Permalink", urwid.Padding(urwid.AttrMap(self.permalink_label, None, focus_map=story_info_map), width='pack')), ]: row = urwid.Columns([(12, urwid.Text(('story-header', l), wrap='clip')), v]) story_info.append(row) story_info = urwid.Pile(story_info) self.description = DescriptionBox(app, u'') 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(story_info) self.listbox.body.append(urwid.Divider()) self.listbox_tasks_start = len(self.listbox.body) self.listbox.body.append(urwid.Divider()) self.listbox.body.append(self.description) self.listbox.body.append(urwid.Divider()) self.refresh() self.listbox.set_focus(3) def interested(self, event): if not ((isinstance(event, sync.StoryAddedEvent) and self.story_key == event.story_key) or (isinstance(event, sync.StoryUpdatedEvent) and self.story_key == event.story_key)): self.log.debug("Ignoring refresh story due to event %s" % (event,)) return False self.log.debug("Refreshing story due to event %s" % (event,)) return True def refresh(self): with self.app.db.getSession() as session: story = session.getStory(self.story_key) # When we first open the story, update its last_seen # time. if not self.marked_seen: story.last_seen = datetime.datetime.utcnow() self.marked_seen = True hidden = starred = held = '' # storyboard #if story.hidden: # hidden = ' (hidden)' #if story.starred: # starred = '* ' #if story.held: # held = ' (held)' self.title = '%sStory %s%s%s' % (starred, story.id, hidden, held) self.app.status.update(title=self.title) self.story_rest_id = story.id self.story_title = story.title if story.creator: self.creator_email = story.creator.email else: self.creator_email = None if self.creator_email: creator_string = '%s <%s>' % (story.creator_name, story.creator.email) else: creator_string = story.creator_name self.creator_label.text.set_text(('story-data', creator_string)) self.tags_string = ' '.join([t.name for t in story.tags]) self.tags_label.set_text(('story-data', self.tags_string)) self.title_label.set_text(('story-data', story.title)) self.created_label.set_text(('story-data', str(self.app.time(story.created)))) self.updated_label.set_text(('story-data', str(self.app.time(story.updated)))) self.status_label.set_text(('story-data', story.status)) self.permalink_url = '' # storyboard urlparse.urljoin(self.app.config.url, str(story.number)) self.permalink_label.text.set_text(('story-data', self.permalink_url)) self.description.set_text(story.description) # The listbox has both tasks and events in it, so # keep track of the index separate from the loop. listbox_index = self.listbox_tasks_start # 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) if not row: row = TaskRow(self.app, self, task) self.listbox.body.insert(listbox_index, row) self.task_rows[task.key] = row else: unseen_keys.remove(task.key) row.refresh(task) listbox_index += 1 # Remove any events that should not be displayed for key in unseen_keys: row = self.task_rows.get(key) self.listbox.body.remove(row) del self.task_rows[key] listbox_index -= 1 listbox_index = len(self.listbox.body) # Get the set of events that should be displayed display_events = [] for event in story.events: if event.comment or (not self.hide_events): display_events.append(event) # The set of event keys currently displayed unseen_keys = set(self.event_rows.keys()) # Make sure all of the events that should be displayed are for event in display_events: row = self.event_rows.get(event.key) if not row: box = StoryEventBox(self, event) row = urwid.Padding(box, width=80) self.listbox.body.insert(listbox_index, row) self.event_rows[event.key] = row else: unseen_keys.remove(event.key) row.original_widget.refresh(event) listbox_index += 1 # Remove any events that should not be displayed for key in unseen_keys: row = self.event_rows.get(key) self.listbox.body.remove(row) del self.event_rows[key] listbox_index -= 1 def toggleHidden(self): with self.app.db.getSession() as session: story = session.getStory(self.story_key) story.hidden = not story.hidden self.app.project_cache.clear(story.project) def toggleStarred(self): with self.app.db.getSession() as session: story = session.getStory(self.story_key) story.starred = not story.starred story.pending_starred = True self.app.sync.submitTask( sync.StoryStarredTask(self.story_key, sync.HIGH_PRIORITY)) def toggleHeld(self): return self.app.toggleHeldStory(self.story_key) def keypress(self, size, key): if self.searchKeypress(size, key): return None if not self.app.input_buffer: key = super(StoryView, self).keypress(size, key) keys = self.app.input_buffer + [key] commands = self.app.config.keymap.getCommands(keys) if keymap.TOGGLE_HIDDEN in commands: self.toggleHidden() self.refresh() return None if keymap.TOGGLE_STARRED in commands: self.toggleStarred() self.refresh() return None if keymap.TOGGLE_HELD in commands: self.toggleHeld() self.refresh() return None if keymap.LEAVE_COMMENT in commands: self.leaveComment() return None if keymap.NEW_TASK in commands: self.newTask() return None if keymap.SEARCH_RESULTS in commands: widget = self.app.findStoryList() if widget: self.app.backScreen(widget) return None if ((keymap.NEXT_STORY in commands) or (keymap.PREV_STORY in commands)): widget = self.app.findStoryList() if widget: if keymap.NEXT_STORY in commands: new_story_key = widget.getNextStoryKey(self.story_key) else: new_story_key = widget.getPrevStoryKey(self.story_key) if new_story_key: try: view = StoryView(self.app, new_story_key) self.app.changeScreen(view, push=False) except boartty.view.DisplayError as e: self.app.error(e.message) return None if keymap.TOGGLE_HIDDEN_COMMENTS in commands: self.hide_events = not self.hide_events self.refresh() return None if keymap.EDIT_DESCRIPTION in commands: self.editDescription() return None if keymap.REFRESH in commands: self.app.sync.submitTask( sync.SyncStoryTask(self.story_rest_id, priority=sync.HIGH_PRIORITY)) self.app.status.update() return None if keymap.EDIT_TITLE in commands: self.editTitle() return None if keymap.EDIT_TAGS in commands: self.editTags() return None if keymap.INTERACTIVE_SEARCH in commands: self.searchStart() if keymap.FURTHER_INPUT not in commands: self.app.clearInputBuffer() return None return key def editDescription(self): with self.app.db.getSession() as session: story = session.getStory(self.story_key) dialog = mywid.TextEditDialog(self.app, u'Edit Description', u'Description:', u'Save', story.description) urwid.connect_signal(dialog, 'cancel', self.app.backScreen) urwid.connect_signal(dialog, 'save', lambda button: self.doEditDescription(dialog)) self.app.popup(dialog, relative_width=50, relative_height=75, min_width=60, min_height=20) def doEditDescription(self, dialog): with self.app.db.getSession() as session: story = session.getStory(self.story_key) story.description = dialog.entry.edit_text story.pending = True self.app.sync.submitTask( sync.UpdateStoryTask(self.story_key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def leaveComment(self, parent=None, reply_text=None): with self.app.db.getSession() as session: story = session.getStory(self.story_key) event = story.getDraftCommentEvent(parent) if event: text = event.comment.content else: text = u'' if reply_text: text += reply_text dialog = mywid.TextEditDialog(self.app, u'Leave Comment', u'Comment:', u'Save', text) urwid.connect_signal(dialog, 'cancel', lambda button: self.cancelLeaveComment(dialog, parent)) urwid.connect_signal(dialog, 'save', lambda button: self.saveLeaveComment(dialog, parent)) self.app.popup(dialog, relative_width=50, relative_height=75, min_width=60, min_height=20) def cancelLeaveComment(self, dialog, parent): with self.app.db.getSession() as session: story = session.getStory(self.story_key) user = session.getUser(self.app.user_id) story.setDraftComment(user, parent, dialog.entry.edit_text) self.app.backScreen() self.refresh() def saveLeaveComment(self, dialog, parent): with self.app.db.getSession() as session: story = session.getStory(self.story_key) user = session.getUser(self.app.user_id) event = story.setDraftComment(user, parent, dialog.entry.edit_text) event.comment.pending = True self.app.sync.submitTask( sync.AddCommentTask(event.key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def newTask(self): dialog = NewTaskDialog(self.app) urwid.connect_signal(dialog, 'save', lambda button: self.saveNewTask(dialog)) urwid.connect_signal(dialog, 'cancel', lambda button: self.cancelNewTask(dialog)) self.app.popup(dialog, relative_width=50, relative_height=25, min_width=60, min_height=8) def cancelNewTask(self, dialog): self.app.backScreen() def saveNewTask(self, dialog): with self.app.db.getSession() as session: story = session.getStory(self.story_key) task = story.addTask() task.project = session.getProjectByID(dialog.project_button.key) task.title = dialog.title_field.edit_text task.status = dialog.status_button.key if dialog.assignee_button.key: task.assignee = session.getUserByID(dialog.assignee_button.key) task.pending = True self.app.sync.submitTask( sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def editTitle(self): dialog = mywid.LineEditDialog(self.app, 'Edit Story Title', '', 'Title: ', self.story_title, ring=self.app.ring) urwid.connect_signal(dialog, 'save', lambda button: self.updateTitle(dialog, True)) urwid.connect_signal(dialog, 'cancel', lambda button: self.updateTitle(dialog, False)) self.app.popup(dialog) def editTags(self): dialog = mywid.LineEditDialog(self.app, 'Edit Story Tags (Space Separated)', '', 'Tags: ', self.tags_string, ring=self.app.ring) urwid.connect_signal(dialog, 'save', lambda button: self.updateTags(dialog, True)) urwid.connect_signal(dialog, 'cancel', lambda button: self.updateTags(dialog, False)) self.app.popup(dialog) def updateTitle(self, dialog, save): if save: with self.app.db.getSession() as session: story = session.getStory(self.story_key) story.title = dialog.entry.edit_text self.app.sync.submitTask( sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def updateTags(self, dialog, save): if save: with self.app.db.getSession() as session: story = session.getStory(self.story_key) new_tags = dialog.entry.edit_text.split(' ') tags = [] for tag_name in new_tags: tag = session.getTag(tag_name) if tag is None: tag = session.createTag(tag_name) tags.append(tag) story.tags = tags self.app.sync.submitTask( sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY)) self.app.backScreen() self.refresh() def openPermalink(self, widget): self.app.openURL(self.permalink_url) def searchCreator(self, widget): if self.creator_email: self.app.doSearch("status:open creator:%s" % (self.creator_email,)) def searchTags(self, widget): #storyboard if self.topic: self.app.doSearch("status:open topic:%s" % (self.topic,))