Merge lp:~thisfred/u1db/u1todo-whats-up-doc into lp:u1db
- u1todo-whats-up-doc
- Merge into trunk
Proposed by
Eric Casteleijn
Status: | Merged |
---|---|
Approved by: | Eric Casteleijn |
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) | Approve | ||
Review via email: mp+104811@code.launchpad.net |
Commit message
Description of the change
added a lot of running commentary
To post a comment you must log in.
- 267. By Eric Casteleijn
-
merged trunk, resolved conflicts
Revision history for this message
Lucio Torre (lucio.torre) : | # |
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__": |