def test_tag_task(self): """Sets the tags for a task.""" store = TodoStore(self.db) task = store.new_task() tag = "you're it" store.tag_task(task, [tag]) self.assertEqual([tag], task.tags)
def test_get_all_tags_duplicates(self): store = TodoStore(self.db) store.initialize_db() tags = ['foo', 'bar', 'bam'] store.new_task(tags=tags) self.assertEqual(sorted(tags), sorted(store.get_all_tags())) tags2 = ['foo', 'sball'] store.new_task(tags=tags2) self.assertEqual(set(tags + tags2), set(store.get_all_tags()))
def test_get_all_tags(self): store = TodoStore(self.db) store.initialize_db() tags = ['foo', 'bar', 'bam'] task = store.new_task(tags=tags) self.assertEqual(sorted(tags), sorted(store.get_all_tags())) tags = ['foo', 'sball'] task.tags = tags store.save_task(task) self.assertEqual(sorted(tags), sorted(store.get_all_tags()))
def test_indexes_are_added(self): """New indexes are added when a new store is created.""" store = TodoStore(self.db) store.initialize_db() INDEXES['foo'] = ['bar'] self.assertNotIn('foo', dict(self.db.list_indexes())) store = TodoStore(self.db) store.initialize_db() self.assertIn('foo', dict(self.db.list_indexes()))
def test_indexes_are_updated(self): """Indexes are updated when a new store is created.""" store = TodoStore(self.db) store.initialize_db() new_expression = 'newtags' INDEXES[TAGS_INDEX] = [new_expression] self.assertNotEqual( new_expression, dict(self.db.list_indexes())['tags']) store = TodoStore(self.db) store.initialize_db() self.assertEqual( [new_expression], dict(self.db.list_indexes())['tags'])
def test_get_tasks_by_tags(self): store = TodoStore(self.db) store.initialize_db() tags = ['foo', 'bar', 'bam'] task1 = store.new_task(tags=tags) tags2 = ['foo', 'sball'] task2 = store.new_task(tags=tags2) self.assertEqual( sorted([task1.doc_id, task2.doc_id]), sorted([t.doc_id for t in store.get_tasks_by_tags(['foo'])])) self.assertEqual( [task1.doc_id], [t.doc_id for t in store.get_tasks_by_tags(['foo', 'bar'])]) self.assertEqual( [task2.doc_id], [t.doc_id for t in store.get_tasks_by_tags(['foo', 'sball'])])
def __init__(self, in_memory=False): super(Main, self).__init__() # Dynamically load the ui file generated by QtDesigner. uifile = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'cosas.ui') uic.loadUi(uifile, self) self.buttons_frame.hide() # hook up the signals to the signal handlers. self.connect_events() # Load the cosas database. db = get_database() # And wrap it in a TodoStore object. self.store = TodoStore(db) # create or update the indexes if they are not up-to-date self.store.initialize_db() # hook up the delegate header = self.todo_list.header() header.setResizeMode(0, 2) # first column fixed header.setResizeMode(1, 1) # stretch second column header.setResizeMode(2, 2) # third column fixed header.setDefaultSectionSize(20) header.setStretchLastSection(False) # Initialize some variables we will use to keep track of the tags. self._tag_docs = defaultdict(list) self._tag_buttons = {} self._tag_filter = [] self._tag_colors = {} # A list of colors to give differently tagged items different colors. self.colors = TAG_COLORS[:] # Get all the tasks in the database, and add them to the UI. for task in self.store.get_all_tasks(): self.add_task(task) self.title_edit.clear() # Give the edit field focus. self.title_edit.setFocus() self.editing = False self.last_synced = None self.sync_target = U1_URL self.auto_sync = False self._timer = QtCore.QTimer()
def test_get_all_tasks(self): store = TodoStore(self.db) store.initialize_db() task1 = store.new_task() task2 = store.new_task() task3 = store.new_task() task_ids = [task.doc_id for task in store.get_all_tasks()] self.assertEqual( sorted([task1.doc_id, task2.doc_id, task3.doc_id]), sorted(task_ids))
def test_reinitialize_db(self): """Creates indexes.""" store = TodoStore(self.db) store.new_task() store.initialize_db() for key, value in self.db.list_indexes(): self.assertEqual(INDEXES[key], value)
def test_save_task_get_task(self): """Saves a modified task and retrieves it from the db.""" store = TodoStore(self.db) task = store.new_task() task.title = "This is the title." store.save_task(task) task_copy = store.get_task(task.doc_id) self.assertEqual(task.title, task_copy.title)
def __init__(self, in_memory=False): super(Main, self).__init__() # Dynamically load the ui file generated by QtDesigner. uifile = os.path.join( os.path.abspath(os.path.dirname(__file__)), 'cosas.ui') uic.loadUi(uifile, self) self.buttons_frame.hide() # hook up the signals to the signal handlers. self.connect_events() # Load the cosas database. db = get_database() # And wrap it in a TodoStore object. self.store = TodoStore(db) # create or update the indexes if they are not up-to-date self.store.initialize_db() # hook up the delegate header = self.todo_list.header() header.setResizeMode(0, 2) # first column fixed header.setResizeMode(1, 1) # stretch second column header.setResizeMode(2, 2) # third column fixed header.setDefaultSectionSize(20) header.setStretchLastSection(False) # Initialize some variables we will use to keep track of the tags. self._tag_docs = defaultdict(list) self._tag_buttons = {} self._tag_filter = [] self._tag_colors = {} # A list of colors to give differently tagged items different colors. self.colors = TAG_COLORS[:] # Get all the tasks in the database, and add them to the UI. for task in self.store.get_all_tasks(): self.add_task(task) self.title_edit.clear() # Give the edit field focus. self.title_edit.setFocus() self.editing = False self.last_synced = None self.sync_target = U1_URL self.auto_sync = False self._timer = QtCore.QTimer()
class Main(QtGui.QMainWindow): """Main window of our application.""" def __init__(self, in_memory=False): super(Main, self).__init__() # Dynamically load the ui file generated by QtDesigner. uifile = os.path.join( os.path.abspath(os.path.dirname(__file__)), 'cosas.ui') uic.loadUi(uifile, self) self.buttons_frame.hide() # hook up the signals to the signal handlers. self.connect_events() # Load the cosas database. db = get_database() # And wrap it in a TodoStore object. self.store = TodoStore(db) # create or update the indexes if they are not up-to-date self.store.initialize_db() # hook up the delegate header = self.todo_list.header() header.setResizeMode(0, 2) # first column fixed header.setResizeMode(1, 1) # stretch second column header.setResizeMode(2, 2) # third column fixed header.setDefaultSectionSize(20) header.setStretchLastSection(False) # Initialize some variables we will use to keep track of the tags. self._tag_docs = defaultdict(list) self._tag_buttons = {} self._tag_filter = [] self._tag_colors = {} # A list of colors to give differently tagged items different colors. self.colors = TAG_COLORS[:] # Get all the tasks in the database, and add them to the UI. for task in self.store.get_all_tasks(): self.add_task(task) self.title_edit.clear() # Give the edit field focus. self.title_edit.setFocus() self.editing = False self.last_synced = None self.sync_target = U1_URL self.auto_sync = False self._timer = QtCore.QTimer() def update_status_bar(self, message): self.statusBar.showMessage(message) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Delete: self.delete() return if event.key() == QtCore.Qt.Key_Return: current = self.todo_list.currentItem() if current and current.task and current.task.has_conflicts: self.open_conflicts_window(current.task.doc_id) return if not self.editing: self.editing = True self.todo_list.openPersistentEditor(current) return else: self.todo_list.closePersistentEditor(current) self.editing = False return super(Main, self).keyPressEvent(event) def get_tag_color(self): """Get a color number to use for a new tag.""" # Remove a color from the list of available ones and return it. if not self.colors: return BACKGROUND return self.colors.pop(0) def connect_events(self): """Hook up all the signal handlers.""" # On enter, save the task that was being edited. self.title_edit.returnPressed.connect(self.update) self.action_synchronize.triggered.connect(self.open_sync_window) self.buttons_toggle.clicked.connect(self.show_buttons) self.todo_list.itemClicked.connect(self.maybe_open_conflicts) def maybe_open_conflicts(self, item, column): if not item.task.has_conflicts: return self.open_conflicts_window(item.task.doc_id) def open_sync_window(self): window = Sync(self) window.exec_() def open_conflicts_window(self, doc_id): conflicts = self.store.db.get_doc_conflicts(doc_id) window = Conflicts(self, conflicts) window.exec_() def show_buttons(self): """Show the frame with the tag buttons.""" self.buttons_toggle.clicked.disconnect(self.show_buttons) self.buttons_frame.show() self.buttons_toggle.clicked.connect(self.hide_buttons) def hide_buttons(self): """Show the frame with the tag buttons.""" self.buttons_toggle.clicked.disconnect(self.hide_buttons) self.buttons_frame.hide() self.buttons_toggle.clicked.connect(self.show_buttons) def refresh_filter(self): """Remove all tasks, and show only those that satisfy the new filter. """ # Remove everything from the list. while self.todo_list.topLevelItemCount(): self.todo_list.takeTopLevelItem(0) # Get the filtered tasks from the database. for task in self.store.get_tasks_by_tags(self._tag_filter): # Add them to the UI. self.add_task(task) # Clear the current selection. self.todo_list.setCurrentItem(None) self.title_edit.clear() self.item = None def update(self): """Either add a new task or update an existing one.""" text = unicode(self.title_edit.text(), 'utf-8') if not text: # There was no text in the edit field so do nothing. return # No task was selected, so add a new one. task = self.store.new_task(text, tags=extract_tags(text)) self.add_task(task) # Clear the current selection. self.title_edit.clear() def delete(self): """Delete a todo item.""" # Delete the item from the database. index = self.todo_list.indexFromItem(self.todo_list.currentItem()) item = self.todo_list.takeTopLevelItem(index.row()) if item is None: return self.store.delete_task(item.task) # Clear the current selection. self.item = None def add_task(self, task): """Add a new todo item.""" # Wrap the task in a UITask object. item = UITask( task, self.todo_list, self.store, self.todo_list.font(), self) if not task.has_conflicts: item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) self.todo_list.addTopLevelItem(item) if not task.tags: return # If the task has tags, we add them as filter buttons to the UI, if # they are new. for tag in task.tags: self.add_tag(task.doc_id, tag) if task.tags: item.set_color(self._tag_colors[task.tags[0]]['qcolor']) else: item.set_color(BACKGROUND) def add_tag(self, doc_id, tag): """Create a link between the task with id doc_id and the tag, and add a new button for tag if it was not already there. """ # Add the task id to the list of document ids associated with this tag. self._tag_docs[tag].append(doc_id) # If the list has more than one element the tag button was already # present. if len(self._tag_docs[tag]) > 1: return # Add a tag filter button for this tag to the UI. button = QtGui.QPushButton(tag) color = self.get_tag_color() qcolor = QtGui.QColor(color) self._tag_colors[tag] = { 'color_tuple': color, 'qcolor': qcolor} button.setStyleSheet('background-color: %s' % color) button._todo_tag = tag # Make the button an on/off button. button.setCheckable(True) # Store a reference to the button in a dictionary so we can find it # back more easily if we need to delete it. self._tag_buttons[tag] = button # We define a function to handle the clicked signal of the button, # since each button will need its own handler. def filter_toggle(checked): """Toggle the filter for the tag associated with this button.""" if checked: # Add the tag to the current filter. self._tag_filter.append(button._todo_tag) else: # Remove the tag from the current filter. self._tag_filter.remove(button._todo_tag) # Apply the new filter. self.refresh_filter() # Attach the handler to the button's clicked signal. button.clicked.connect(filter_toggle) # Get the position where the button needs to be inserted. (We keep them # sorted alphabetically by the text of the tag. index = sorted(self._tag_buttons.keys()).index(tag) # And add the button to the UI. self.buttons_layout.insertWidget(index, button) def remove_tag(self, doc_id, tag): """Remove the link between the task with id doc_id and the tag, and remove the button for tag if it no longer has any tasks associated with it. """ # Remove the task id from the list of document ids associated with this # tag. self._tag_docs[tag].remove(doc_id) # If the list is not empty, we do not remove the button, because there # are still tasks that have this tag. if self._tag_docs[tag]: return # Look up the button. button = self._tag_buttons[tag] # Remove it from the ui. button.hide() self.buttons_layout.removeWidget(button) # And remove the reference. del self._tag_buttons[tag] def update_tags(self, item, old_tags, new_tags): """Process any changed tags for this item.""" # Process all removed tags. for tag in old_tags - new_tags: self.remove_tag(item.task.doc_id, tag) # Process all tags newly added. for tag in new_tags - old_tags: self.add_tag(item.task.doc_id, tag) if new_tags: item.set_color(self._tag_colors[list(new_tags)[0]]['qcolor']) return item.set_color(BACKGROUND) def get_ubuntuone_credentials(self): cmt = CredentialsManagementTool() return cmt.find_credentials() def synchronize(self, finalize): if self.sync_target == 'https://u1db.one.ubuntu.com/~/cosas': d = self.get_ubuntuone_credentials() d.addCallback(self._synchronize) d.addCallback(finalize) else: # TODO: add ui for entering creds for non u1 servers. self._synchronize() finalize() def _auto_sync(self): self._timer.stop() try: self.synchronize(lambda _: None) finally: self._timer.start(TIMEOUT) def start_auto_sync(self): self._timer.timeout.connect(self._auto_sync) self._timer.start(TIMEOUT) def stop_auto_sync(self): self._timer.stop() def _synchronize(self, creds=None): target = self.sync_target assert target.startswith('http://') or target.startswith('https://') if creds is not None: # convert into expected form creds = {'oauth': { 'token_key': creds['token'], 'token_secret': creds['token_secret'], 'consumer_key': creds['consumer_key'], 'consumer_secret': creds['consumer_secret'] }} self.store.db.sync(target, creds=creds) # refresh the UI to show changed or new tasks self.refresh_filter() self.update_status_bar("last synced: %s" % (datetime.now(),)) def resolve(self, doc, revs): self.store.db.resolve_doc(doc, revs) # refresh the UI to show the resolved version self.refresh_filter()
class Main(QtGui.QMainWindow): """Main window of our application.""" def __init__(self, in_memory=False): super(Main, self).__init__() # Dynamically load the ui file generated by QtDesigner. uifile = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'cosas.ui') uic.loadUi(uifile, self) self.buttons_frame.hide() # hook up the signals to the signal handlers. self.connect_events() # Load the cosas database. db = get_database() # And wrap it in a TodoStore object. self.store = TodoStore(db) # create or update the indexes if they are not up-to-date self.store.initialize_db() # hook up the delegate header = self.todo_list.header() header.setResizeMode(0, 2) # first column fixed header.setResizeMode(1, 1) # stretch second column header.setResizeMode(2, 2) # third column fixed header.setDefaultSectionSize(20) header.setStretchLastSection(False) # Initialize some variables we will use to keep track of the tags. self._tag_docs = defaultdict(list) self._tag_buttons = {} self._tag_filter = [] self._tag_colors = {} # A list of colors to give differently tagged items different colors. self.colors = TAG_COLORS[:] # Get all the tasks in the database, and add them to the UI. for task in self.store.get_all_tasks(): self.add_task(task) self.title_edit.clear() # Give the edit field focus. self.title_edit.setFocus() self.editing = False self.last_synced = None self.sync_target = U1_URL self.auto_sync = False self._timer = QtCore.QTimer() def update_status_bar(self, message): self.statusBar.showMessage(message) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Delete: self.delete() return if event.key() == QtCore.Qt.Key_Return: current = self.todo_list.currentItem() if current and current.task and current.task.has_conflicts: self.open_conflicts_window(current.task.doc_id) return if not self.editing: self.editing = True self.todo_list.openPersistentEditor(current) return else: self.todo_list.closePersistentEditor(current) self.editing = False return super(Main, self).keyPressEvent(event) def get_tag_color(self): """Get a color number to use for a new tag.""" # Remove a color from the list of available ones and return it. if not self.colors: return BACKGROUND return self.colors.pop(0) def connect_events(self): """Hook up all the signal handlers.""" # On enter, save the task that was being edited. self.title_edit.returnPressed.connect(self.update) self.action_synchronize.triggered.connect(self.open_sync_window) self.buttons_toggle.clicked.connect(self.show_buttons) self.todo_list.itemClicked.connect(self.maybe_open_conflicts) def maybe_open_conflicts(self, item, column): if not item.task.has_conflicts: return self.open_conflicts_window(item.task.doc_id) def open_sync_window(self): window = Sync(self) window.exec_() def open_conflicts_window(self, doc_id): conflicts = self.store.db.get_doc_conflicts(doc_id) window = Conflicts(self, conflicts) window.exec_() def show_buttons(self): """Show the frame with the tag buttons.""" self.buttons_toggle.clicked.disconnect(self.show_buttons) self.buttons_frame.show() self.buttons_toggle.clicked.connect(self.hide_buttons) def hide_buttons(self): """Show the frame with the tag buttons.""" self.buttons_toggle.clicked.disconnect(self.hide_buttons) self.buttons_frame.hide() self.buttons_toggle.clicked.connect(self.show_buttons) def refresh_filter(self): """Remove all tasks, and show only those that satisfy the new filter. """ # Remove everything from the list. while self.todo_list.topLevelItemCount(): self.todo_list.takeTopLevelItem(0) # Get the filtered tasks from the database. for task in self.store.get_tasks_by_tags(self._tag_filter): # Add them to the UI. self.add_task(task) # Clear the current selection. self.todo_list.setCurrentItem(None) self.title_edit.clear() self.item = None def update(self): """Either add a new task or update an existing one.""" text = unicode(self.title_edit.text(), 'utf-8') if not text: # There was no text in the edit field so do nothing. return # No task was selected, so add a new one. task = self.store.new_task(text, tags=extract_tags(text)) self.add_task(task) # Clear the current selection. self.title_edit.clear() def delete(self): """Delete a todo item.""" # Delete the item from the database. index = self.todo_list.indexFromItem(self.todo_list.currentItem()) item = self.todo_list.takeTopLevelItem(index.row()) if item is None: return self.store.delete_task(item.task) # Clear the current selection. self.item = None def add_task(self, task): """Add a new todo item.""" # Wrap the task in a UITask object. item = UITask(task, self.todo_list, self.store, self.todo_list.font(), self) if not task.has_conflicts: item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) self.todo_list.addTopLevelItem(item) if not task.tags: return # If the task has tags, we add them as filter buttons to the UI, if # they are new. for tag in task.tags: self.add_tag(task.doc_id, tag) if task.tags: item.set_color(self._tag_colors[task.tags[0]]['qcolor']) else: item.set_color(BACKGROUND) def add_tag(self, doc_id, tag): """Create a link between the task with id doc_id and the tag, and add a new button for tag if it was not already there. """ # Add the task id to the list of document ids associated with this tag. self._tag_docs[tag].append(doc_id) # If the list has more than one element the tag button was already # present. if len(self._tag_docs[tag]) > 1: return # Add a tag filter button for this tag to the UI. button = QtGui.QPushButton(tag) color = self.get_tag_color() qcolor = QtGui.QColor(color) self._tag_colors[tag] = {'color_tuple': color, 'qcolor': qcolor} button.setStyleSheet('background-color: %s' % color) button._todo_tag = tag # Make the button an on/off button. button.setCheckable(True) # Store a reference to the button in a dictionary so we can find it # back more easily if we need to delete it. self._tag_buttons[tag] = button # We define a function to handle the clicked signal of the button, # since each button will need its own handler. def filter_toggle(checked): """Toggle the filter for the tag associated with this button.""" if checked: # Add the tag to the current filter. self._tag_filter.append(button._todo_tag) else: # Remove the tag from the current filter. self._tag_filter.remove(button._todo_tag) # Apply the new filter. self.refresh_filter() # Attach the handler to the button's clicked signal. button.clicked.connect(filter_toggle) # Get the position where the button needs to be inserted. (We keep them # sorted alphabetically by the text of the tag. index = sorted(self._tag_buttons.keys()).index(tag) # And add the button to the UI. self.buttons_layout.insertWidget(index, button) def remove_tag(self, doc_id, tag): """Remove the link between the task with id doc_id and the tag, and remove the button for tag if it no longer has any tasks associated with it. """ # Remove the task id from the list of document ids associated with this # tag. self._tag_docs[tag].remove(doc_id) # If the list is not empty, we do not remove the button, because there # are still tasks that have this tag. if self._tag_docs[tag]: return # Look up the button. button = self._tag_buttons[tag] # Remove it from the ui. button.hide() self.buttons_layout.removeWidget(button) # And remove the reference. del self._tag_buttons[tag] def update_tags(self, item, old_tags, new_tags): """Process any changed tags for this item.""" # Process all removed tags. for tag in old_tags - new_tags: self.remove_tag(item.task.doc_id, tag) # Process all tags newly added. for tag in new_tags - old_tags: self.add_tag(item.task.doc_id, tag) if new_tags: item.set_color(self._tag_colors[list(new_tags)[0]]['qcolor']) return item.set_color(BACKGROUND) def get_ubuntuone_credentials(self): cmt = CredentialsManagementTool() return cmt.find_credentials() def synchronize(self, finalize): if self.sync_target == 'https://u1db.one.ubuntu.com/~/cosas': d = self.get_ubuntuone_credentials() d.addCallback(self._synchronize) d.addCallback(finalize) else: # TODO: add ui for entering creds for non u1 servers. self._synchronize() finalize() def _auto_sync(self): self._timer.stop() try: self.synchronize(lambda _: None) finally: self._timer.start(TIMEOUT) def start_auto_sync(self): self._timer.timeout.connect(self._auto_sync) self._timer.start(TIMEOUT) def stop_auto_sync(self): self._timer.stop() def _synchronize(self, creds=None): target = self.sync_target assert target.startswith('http://') or target.startswith('https://') if creds is not None: # convert into expected form creds = { 'oauth': { 'token_key': creds['token'], 'token_secret': creds['token_secret'], 'consumer_key': creds['consumer_key'], 'consumer_secret': creds['consumer_secret'] } } self.store.db.sync(target, creds=creds) # refresh the UI to show changed or new tasks self.refresh_filter() self.update_status_bar("last synced: %s" % (datetime.now(), )) def resolve(self, doc, revs): self.store.db.resolve_doc(doc, revs) # refresh the UI to show the resolved version self.refresh_filter()
def test_new_task(self): """Creates a new task.""" store = TodoStore(self.db) task = store.new_task() self.assertTrue(isinstance(task, Task)) self.assertIsNotNone(task.doc_id)
def test_new_task_with_title(self): """Creates a new task.""" store = TodoStore(self.db) title = "Un task muy importante" task = store.new_task(title=title) self.assertEqual(title, task.title)
def test_new_task_with_tags(self): """Creates a new task.""" store = TodoStore(self.db) tags = ['foo', 'bar', 'bam'] task = store.new_task(tags=tags) self.assertEqual(tags, task.tags)
def test_get_non_existant_task(self): """Saves a modified task and retrieves it from the db.""" store = TodoStore(self.db) self.assertRaises(KeyError, store.get_task, "nonexistant")
def test_delete_task(self): """Deletes a task by id.""" store = TodoStore(self.db) task = store.new_task() store.delete_task(task) self.assertRaises(KeyError, store.get_task, task.doc_id)