Пример #1
0
 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)
Пример #2
0
 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()))
Пример #3
0
 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()))
Пример #4
0
 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()))
Пример #5
0
 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))
Пример #6
0
 def test_get_tasks_by_empty_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([])]))
Пример #7
0
 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'])
Пример #8
0
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()
Пример #9
0
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()