Merge lp:~thisfred/u1db/u1todo-whats-up-doc into lp:u1db
- u1todo-whats-up-doc
- Merge into trunk
Proposed by
Eric Casteleijn
on 2012-05-04
| Status: | Merged |
|---|---|
| Approved by: | Eric Casteleijn on 2012-05-09 |
| Approved revision: | 267 |
| Merged at revision: | 273 |
| Proposed branch: | lp:~thisfred/u1db/u1todo-whats-up-doc |
| Merge into: | lp:u1db |
| Prerequisite: | lp:~thisfred/u1db/u1todo-4 |
| Diff against target: |
402 lines (+136/-13) 2 files modified
u1todo/u1todo.py (+39/-4) u1todo/ui.py (+97/-9) |
| To merge this branch: | bzr merge lp:~thisfred/u1db/u1todo-whats-up-doc |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Lucio Torre (community) | 2012-05-04 | Approve on 2012-05-09 | |
|
Review via email:
|
|||
Commit Message
Description of the Change
added a lot of running commentary
To post a comment you must log in.
lp:~thisfred/u1db/u1todo-whats-up-doc
updated
on 2012-05-09
- 267. By Eric Casteleijn on 2012-05-09
-
merged trunk, resolved conflicts
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
| 1 | === modified file 'u1todo/u1todo.py' |
| 2 | --- u1todo/u1todo.py 2012-05-08 18:27:32 +0000 |
| 3 | +++ u1todo/u1todo.py 2012-05-09 15:00:25 +0000 |
| 4 | @@ -43,6 +43,8 @@ |
| 5 | |
| 6 | def extract_tags(text): |
| 7 | """Extract the tags from the text.""" |
| 8 | + # This looks for all "tags" in a text, indicated with a '#' at the |
| 9 | + # start of the tag, or enclosed in square brackets. |
| 10 | return [t[0] if t[0] else t[1] for t in TAGS.findall(text)] |
| 11 | |
| 12 | |
| 13 | @@ -54,7 +56,9 @@ |
| 14 | |
| 15 | def initialize_db(self): |
| 16 | """Initialize the database.""" |
| 17 | + # Ask the database for currently existing indexes. |
| 18 | db_indexes = dict(self.db.list_indexes()) |
| 19 | + # Loop through the indexes we expect to find. |
| 20 | for name, expression in INDEXES.items(): |
| 21 | if name not in db_indexes: |
| 22 | # The index does not yet exist. |
| 23 | @@ -63,7 +67,8 @@ |
| 24 | if expression == db_indexes[name]: |
| 25 | # The index exists and is up to date. |
| 26 | continue |
| 27 | - # The index exists but the definition is out of date. |
| 28 | + # The index exists but the definition is not what expected, so we |
| 29 | + # delete it and add the proper index expression. |
| 30 | self.db.delete_index(name) |
| 31 | self.db.create_index(name, expression) |
| 32 | |
| 33 | @@ -72,33 +77,48 @@ |
| 34 | task.tags = tags |
| 35 | |
| 36 | def get_all_tags(self): |
| 37 | - """Get all the tags in use.""" |
| 38 | + """Get all tags in use in the entire database.""" |
| 39 | return self.db.get_index_keys(TAGS_INDEX) |
| 40 | |
| 41 | def get_tasks_by_tags(self, tags): |
| 42 | + """Get all tasks that have every tag in tags.""" |
| 43 | if not tags: |
| 44 | + # No tags specified, so return all tasks. |
| 45 | return self.get_all_tasks() |
| 46 | + # Get all tasks for the first tag. |
| 47 | results = { |
| 48 | doc.doc_id: doc for doc in |
| 49 | self.db.get_from_index(TAGS_INDEX, [(tags[0],)])} |
| 50 | + # Now loop over the rest of the tags (if any) and remove from the |
| 51 | + # results any document that does not have that particular tag. |
| 52 | for tag in tags[1:]: |
| 53 | + # Get the ids of all documents with this tag. |
| 54 | ids = [ |
| 55 | doc.doc_id for doc in |
| 56 | self.db.get_from_index(TAGS_INDEX, [(tag,)])] |
| 57 | for key in results.keys(): |
| 58 | if key not in ids: |
| 59 | + # Remove the document from result, because it does not have |
| 60 | + # this particular tag. |
| 61 | del results[key] |
| 62 | if not results: |
| 63 | + # If results is empty, we're done: there are no |
| 64 | + # documents with all tags. |
| 65 | return [] |
| 66 | + # Wrap each document in results in a Task object, and return them. |
| 67 | return [Task(doc) for doc in results.values()] |
| 68 | |
| 69 | def get_task(self, task_id): |
| 70 | """Get a task from the database.""" |
| 71 | document = self.db.get_doc(task_id) |
| 72 | if document is None: |
| 73 | + # No document with that id exists in the database. |
| 74 | raise KeyError("No task with id '%s'." % (task_id,)) |
| 75 | if document.content is None: |
| 76 | + # The document id exists, but the document's content was previously |
| 77 | + # deleted. |
| 78 | raise KeyError("Task with id %s was deleted." % (task_id,)) |
| 79 | + # Wrap the document in a Task object and return it. |
| 80 | return Task(document) |
| 81 | |
| 82 | def delete_task(self, task): |
| 83 | @@ -107,15 +127,23 @@ |
| 84 | |
| 85 | def new_task(self, title=None, tags=None): |
| 86 | """Create a new task document.""" |
| 87 | - # Create the document in the u1db database |
| 88 | if tags is None: |
| 89 | tags = [] |
| 90 | + # We copy the JSON string representing a pristine task with no title |
| 91 | + # and no tags. |
| 92 | content = EMPTY_TASK |
| 93 | + # If we were passed a title or tags, or both, we set them in the object |
| 94 | + # before storing it in the database. |
| 95 | if title or tags: |
| 96 | + # Load the json string into a Python object. |
| 97 | content_object = json.loads(content) |
| 98 | content_object['title'] = title |
| 99 | content_object['tags'] = tags |
| 100 | + # Convert the Python object back into a JSON string. |
| 101 | content = json.dumps(content_object) |
| 102 | + # Store the document in the database. Since we did not set a document |
| 103 | + # id, the database will store it as a new document, and generate |
| 104 | + # a valid id. |
| 105 | document = self.db.create_doc(content=content) |
| 106 | # Wrap the document in a Task object. |
| 107 | return Task(document) |
| 108 | @@ -127,6 +155,9 @@ |
| 109 | self.db.put_doc(task.document) |
| 110 | |
| 111 | def get_all_tasks(self): |
| 112 | + # Since the DONE_INDEX indexes anything that has a value in the field |
| 113 | + # "done", and all tasks do (either True or False), it's a good way to |
| 114 | + # get all tasks out of the database. |
| 115 | return [ |
| 116 | Task(doc) for doc in self.db.get_from_index(DONE_INDEX, ["*"])] |
| 117 | |
| 118 | @@ -141,6 +172,8 @@ |
| 119 | @property |
| 120 | def task_id(self): |
| 121 | """The u1db id of the task.""" |
| 122 | + # Since we won't ever change this, but it's handy for comparing |
| 123 | + # different Task objects, it's a read only property. |
| 124 | return self._document.doc_id |
| 125 | |
| 126 | def _get_title(self): |
| 127 | @@ -148,7 +181,7 @@ |
| 128 | return self._content['title'] |
| 129 | |
| 130 | def _set_title(self, title): |
| 131 | - """Set the task title and save to db.""" |
| 132 | + """Set the task title.""" |
| 133 | self._content['title'] = title |
| 134 | |
| 135 | title = property(_get_title, _set_title, doc="Title of the task.") |
| 136 | @@ -176,5 +209,7 @@ |
| 137 | @property |
| 138 | def document(self): |
| 139 | """The u1db document representing this task.""" |
| 140 | + # This brings the underlying document's JSON content back into sync |
| 141 | + # with whatever data the task currently holds. |
| 142 | self._document.content = json.dumps(self._content) |
| 143 | return self._document |
| 144 | |
| 145 | === modified file 'u1todo/ui.py' |
| 146 | --- u1todo/ui.py 2012-05-08 20:44:41 +0000 |
| 147 | +++ u1todo/ui.py 2012-05-09 15:00:25 +0000 |
| 148 | @@ -37,7 +37,9 @@ |
| 149 | def __init__(self, task): |
| 150 | super(UITask, self).__init__() |
| 151 | self.task = task |
| 152 | + # Set the list item's text to the task's title. |
| 153 | self.setText(self.task.title) |
| 154 | + # If the task is done, check off the list item. |
| 155 | self.setCheckState( |
| 156 | QtCore.Qt.Checked if task.done else QtCore.Qt.Unchecked) |
| 157 | self.update_strikethrough() |
| 158 | @@ -53,49 +55,80 @@ |
| 159 | |
| 160 | def __init__(self, in_memory=False): |
| 161 | super(Main, self).__init__() |
| 162 | - uifile = os.path.join(os.path.abspath(os.path.dirname(__file__)), |
| 163 | - 'u1todo.ui') |
| 164 | + # Dynamically load the ui file generated by QtDesigner. |
| 165 | + uifile = os.path.join( |
| 166 | + os.path.abspath(os.path.dirname(__file__)), 'u1todo.ui') |
| 167 | uic.loadUi(uifile, self) |
| 168 | + # hook up the signals to the signal handlers. |
| 169 | self.connect_events() |
| 170 | + # Load the u1todo database. |
| 171 | db = get_database() |
| 172 | + # And wrap it in a TodoStore object. |
| 173 | self.store = TodoStore(db) |
| 174 | # create or update the indexes if they are not up-to-date |
| 175 | self.store.initialize_db() |
| 176 | + # Initially the delete button is disabled, because there are no tasks |
| 177 | + # to delete. |
| 178 | self.delete_button.setEnabled(False) |
| 179 | + # Initialize some variables we will use to keep track of the tags. |
| 180 | self._tag_docs = defaultdict(list) |
| 181 | self._tag_buttons = {} |
| 182 | self._tag_filter = [] |
| 183 | + # Get all the tasks in the database, and add them to the UI. |
| 184 | for task in self.store.get_all_tasks(): |
| 185 | self.add_task(task) |
| 186 | self.task_edit.clear() |
| 187 | + # Give the edit field focus. |
| 188 | self.task_edit.setFocus() |
| 189 | + # Initialize the variable that points to the currently selected list |
| 190 | + # item. |
| 191 | self.item = None |
| 192 | |
| 193 | def connect_events(self): |
| 194 | """Hook up all the signal handlers.""" |
| 195 | + # On enter, save the task that was being edited. |
| 196 | self.task_edit.returnPressed.connect(self.update) |
| 197 | + # When the Edit/Add button is clicked, save the task that was being |
| 198 | + # edited. |
| 199 | self.edit_button.clicked.connect(self.update) |
| 200 | + # When the Delete button is clicked, delete the currently selected task |
| 201 | + # (if any.) |
| 202 | self.delete_button.clicked.connect(self.delete) |
| 203 | + # If a new row in the list is selected, change the currently selected |
| 204 | + # task, and put its contents in the edit field. |
| 205 | self.todo_list.currentRowChanged.connect(self.row_changed) |
| 206 | + # If the checked status of an item in the list changes, change the done |
| 207 | + # status of the task. |
| 208 | self.todo_list.itemChanged.connect(self.item_changed) |
| 209 | self.sync_button.clicked.connect(self.synchronize) |
| 210 | |
| 211 | def refresh_filter(self): |
| 212 | + """Remove all tasks, and show only those that satisfy the new filter. |
| 213 | + |
| 214 | + """ |
| 215 | + # Remove everything from the list. |
| 216 | while len(self.todo_list): |
| 217 | self.todo_list.takeItem(0) |
| 218 | + # Get the filtered tasks from the database. |
| 219 | for task in self.store.get_tasks_by_tags(self._tag_filter): |
| 220 | + # Add them to the UI. |
| 221 | self.add_task(task) |
| 222 | + # Clear the current selection. |
| 223 | + self.todo_list.setCurrentRow(-1) |
| 224 | + self.task_edit.clear() |
| 225 | self.item = None |
| 226 | - self.task_edit.clear() |
| 227 | |
| 228 | def item_changed(self, item): |
| 229 | + """Mark a task as done or not done.""" |
| 230 | if item.checkState() == QtCore.Qt.Checked: |
| 231 | item.task.done = True |
| 232 | else: |
| 233 | item.task.done = False |
| 234 | + # Save the task to the database. |
| 235 | item.update_strikethrough() |
| 236 | item.setText(item.task.title) |
| 237 | self.store.save_task(item.task) |
| 238 | + # Clear the current selection. |
| 239 | self.todo_list.setCurrentRow(-1) |
| 240 | self.task_edit.clear() |
| 241 | self.item = None |
| 242 | @@ -104,12 +137,16 @@ |
| 243 | """Either add a new task or update an existing one.""" |
| 244 | text = unicode(self.task_edit.text(), 'utf-8') |
| 245 | if not text: |
| 246 | + # There was no text in the edit field so do nothing. |
| 247 | return |
| 248 | if self.item is None: |
| 249 | + # No task was selected, so add a new one. |
| 250 | task = self.store.new_task(text, tags=extract_tags(text)) |
| 251 | self.add_task(task) |
| 252 | else: |
| 253 | + # A task was selected, so update it. |
| 254 | self.update_task_text(text) |
| 255 | + # Clear the current selection. |
| 256 | self.todo_list.setCurrentRow(-1) |
| 257 | self.task_edit.clear() |
| 258 | self.item = None |
| 259 | @@ -161,56 +198,102 @@ |
| 260 | |
| 261 | def delete(self): |
| 262 | """Delete a todo item.""" |
| 263 | + # Delete the item from the database. |
| 264 | row = self.todo_list.currentRow() |
| 265 | item = self.todo_list.takeItem(row) |
| 266 | if item is None: |
| 267 | return |
| 268 | self.store.delete_task(item.task) |
| 269 | + # Clear the current selection. |
| 270 | self.todo_list.setCurrentRow(-1) |
| 271 | + self.task_edit.clear() |
| 272 | + self.item = None |
| 273 | if self.todo_list.count() == 0: |
| 274 | + # If there are no tasks left, disable the delete button. |
| 275 | self.delete_button.setEnabled(False) |
| 276 | |
| 277 | def add_task(self, task): |
| 278 | """Add a new todo item.""" |
| 279 | + # Wrap the task in a UITask object. |
| 280 | item = UITask(task) |
| 281 | self.todo_list.addItem(item) |
| 282 | + # We know there is at least one item now so we enable the delete |
| 283 | + # button. |
| 284 | self.delete_button.setEnabled(True) |
| 285 | if not task.tags: |
| 286 | return |
| 287 | + # If the task has tags, we add them as filter buttons to the UI, if |
| 288 | + # they are new. |
| 289 | for tag in task.tags: |
| 290 | self.add_tag(task.task_id, tag) |
| 291 | |
| 292 | def add_tag(self, task_id, tag): |
| 293 | + """Create a link between the task with id task_id and the tag, and |
| 294 | + add a new button for tag if it was not already there. |
| 295 | + |
| 296 | + """ |
| 297 | + # Add the task id to the list of document ids associated with this tag. |
| 298 | self._tag_docs[tag].append(task_id) |
| 299 | + # If the list has more than one element the tag button was already |
| 300 | + # present. |
| 301 | if len(self._tag_docs[tag]) > 1: |
| 302 | return |
| 303 | + # Add a tag filter button for this tag to the UI. |
| 304 | button = QtGui.QPushButton(tag) |
| 305 | button._u1todo_tag = tag |
| 306 | + # Make the button an on/off button. |
| 307 | button.setCheckable(True) |
| 308 | + # Store a reference to the button in a dictionary so we can find it |
| 309 | + # back more easily if we need to delete it. |
| 310 | self._tag_buttons[tag] = button |
| 311 | |
| 312 | + # We define a function to handle the clicked signal of the button, |
| 313 | + # since each button will need its own handler. |
| 314 | def filter_toggle(checked): |
| 315 | + """Toggle the filter for the tag associated with this button.""" |
| 316 | if checked: |
| 317 | + # Add the tag to the current filter. |
| 318 | self._tag_filter.append(button._u1todo_tag) |
| 319 | else: |
| 320 | + # Remove the tag from the current filter. |
| 321 | self._tag_filter.remove(button._u1todo_tag) |
| 322 | + # Apply the new filter. |
| 323 | self.refresh_filter() |
| 324 | |
| 325 | + # Attach the handler to the button's clicked signal. |
| 326 | button.clicked.connect(filter_toggle) |
| 327 | + # Get the position where the button needs to be inserted. (We keep them |
| 328 | + # sorted alphabetically by the text of the tag. |
| 329 | index = sorted(self._tag_buttons.keys()).index(tag) |
| 330 | + # And add the button to the UI. |
| 331 | self.tag_buttons.insertWidget(index, button) |
| 332 | |
| 333 | def remove_tag(self, task_id, tag): |
| 334 | + """Remove the link between the task with id task_id and the tag, and |
| 335 | + remove the button for tag if it no longer has any tasks associated with |
| 336 | + it. |
| 337 | + |
| 338 | + """ |
| 339 | + # Remove the task id from the list of document ids associated with this |
| 340 | + # tag. |
| 341 | self._tag_docs[tag].remove(task_id) |
| 342 | + # If the list is not empty, we do not remove the button, because there |
| 343 | + # are still tasks that have this tag. |
| 344 | if self._tag_docs[tag]: |
| 345 | return |
| 346 | + # Look up the button. |
| 347 | button = self._tag_buttons[tag] |
| 348 | + # Remove it from the ui. |
| 349 | self.tag_buttons.removeWidget(button) |
| 350 | + # And remove the reference. |
| 351 | del self._tag_buttons[tag] |
| 352 | |
| 353 | def update_tags(self, task_id, old_tags, new_tags): |
| 354 | + """Process any changed tags for this task_id.""" |
| 355 | + # Process all removed tags. |
| 356 | for tag in old_tags - new_tags: |
| 357 | self.remove_tag(task_id, tag) |
| 358 | + # Process all tags newly added. |
| 359 | for tag in new_tags - old_tags: |
| 360 | self.add_tag(task_id, tag) |
| 361 | |
| 362 | @@ -218,16 +301,23 @@ |
| 363 | """Edit an existing todo item.""" |
| 364 | item = self.item |
| 365 | task = item.task |
| 366 | + # Change the task's title to the text in the edit field. |
| 367 | task.title = text |
| 368 | + # Record the current tags. |
| 369 | old_tags = set(task.tags) if task.tags else set([]) |
| 370 | + # Extract the new tags from the new text. |
| 371 | new_tags = set(extract_tags(text)) |
| 372 | + # Check if the tag filter buttons need updating. |
| 373 | self.update_tags(item.task.task_id, old_tags, new_tags) |
| 374 | + # Set the tags on the task. |
| 375 | task.tags = list(new_tags) |
| 376 | # disconnect the signal temporarily while we change the title |
| 377 | self.todo_list.itemChanged.disconnect(self.item_changed) |
| 378 | + # Change the text in the UI. |
| 379 | item.setText(text) |
| 380 | # reconnect the signal after we changed the title |
| 381 | self.todo_list.itemChanged.connect(self.item_changed) |
| 382 | + # Save the changed task to the database. |
| 383 | self.store.save_task(task) |
| 384 | |
| 385 | def row_changed(self, index): |
| 386 | @@ -235,12 +325,10 @@ |
| 387 | if index == -1: |
| 388 | self.task_edit.clear() |
| 389 | return |
| 390 | - self.edit_item(self.todo_list.item(index)) |
| 391 | - |
| 392 | - def edit_item(self, item): |
| 393 | - """Edit item in task edit box.""" |
| 394 | - self.item = item |
| 395 | - self.task_edit.setText(item.task.title) |
| 396 | + # If a row is selected, show the selected task's title in the edit |
| 397 | + # field. |
| 398 | + self.item = self.todo_list.item(index) |
| 399 | + self.task_edit.setText(self.item.task.title) |
| 400 | |
| 401 | |
| 402 | if __name__ == "__main__": |
