Merge lp:~thisfred/u1db/u1todo-whats-up-doc into lp:u1db

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
Reviewer Review Type Date Requested Status
Lucio Torre (community) Approve
Review via email: mp+104811@code.launchpad.net

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__":

Subscribers

People subscribed via source and target branches