Merge lp:~thisfred/u1db/u1todo-2 into lp:u1db

Proposed by Eric Casteleijn on 2012-04-19
Status: Merged
Approved by: Eric Casteleijn on 2012-04-23
Approved revision: 253
Merged at revision: 252
Proposed branch: lp:~thisfred/u1db/u1todo-2
Merge into: lp:u1db
Diff against target: 330 lines (+219/-4)
4 files modified
u1todo/test_u1todo.py (+35/-2)
u1todo/u1todo.py (+32/-2)
u1todo/u1todo.ui (+58/-0)
u1todo/ui.py (+94/-0)
To merge this branch: bzr merge lp:~thisfred/u1db/u1todo-2
Reviewer Review Type Date Requested Status
Roberto Alsina (community) 2012-04-19 Approve on 2012-04-23
Review via email: mp+102770@code.launchpad.net

Commit Message

Initial UI: adding editing and deleting tasks works. Done status and tags not so much.

Description of the Change

Initial UI: adding editing and deleting tasks works. Done status and tags not so much.

to test, run:

PYTHONPATH=. python u1todo/ui.py

from the checkout root.

To post a comment you must log in.
Lucio Torre (lucio.torre) wrote :

173 +class UIMainWindow(object):
174 + def setupUi(self, MainWindow):

This function scares me a bit. I dont know the desktop+ prefences about it, but i was expecting some declarative thing taking care of that. Is that reasonable?

Lucio Torre (lucio.torre) wrote :

i know its a bit premature, but i would get some time from the people from design to get a very minimal but useful UX for the todo list. :)

Eric Casteleijn (thisfred) wrote :

> 173 +class UIMainWindow(object):
> 174 + def setupUi(self, MainWindow):
>
> This function scares me a bit. I dont know the desktop+ prefences about it,
> but i was expecting some declarative thing taking care of that. Is that
> reasonable?

I don't really know: I used QtDesigner to get off the ground, and that is what it generated. Maybe I should put it back in a separate module so that nobody thinks of it as part of the example code. ;)

Roberto Alsina (ralsina) wrote :

> > 173 +class UIMainWindow(object):
> > 174 + def setupUi(self, MainWindow):
> >
> > This function scares me a bit. I dont know the desktop+ prefences about it,
> > but i was expecting some declarative thing taking care of that. Is that
> > reasonable?
>
> I don't really know: I used QtDesigner to get off the ground, and that is what
> it generated. Maybe I should put it back in a separate module so that nobody
> thinks of it as part of the example code. ;)

Talk to me when I start working and I'll show you the problem with that :-)

Roberto Alsina (ralsina) wrote :

> > 173 +class UIMainWindow(object):
> > 174 + def setupUi(self, MainWindow):
> >
> > This function scares me a bit. I dont know the desktop+ prefences about it,
> > but i was expecting some declarative thing taking care of that. Is that
> > reasonable?
>
> I don't really know: I used QtDesigner to get off the ground, and that is what
> it generated. Maybe I should put it back in a separate module so that nobody
> thinks of it as part of the example code. ;)

Oh, well, should not look at code before waking up, I guess. Lucio, that's generated code.

Lucio Torre (lucio.torre) wrote :

That does not make it better.
The project has the generated code and no ui file, and the code already includes some functions which were not generated.
I would really prefer to read some static definition in runtime that we can also open back with qtdesigner.

John A Meinel (jameinel) wrote :

On 4/20/2012 1:23 PM, Roberto Alsina wrote:
>>> 173 +class UIMainWindow(object):
>>> 174 + def setupUi(self, MainWindow):
>>>
>>> This function scares me a bit. I dont know the desktop+ prefences about it,
>>> but i was expecting some declarative thing taking care of that. Is that
>>> reasonable?
>>
>> I don't really know: I used QtDesigner to get off the ground, and that is what
>> it generated. Maybe I should put it back in a separate module so that nobody
>> thinks of it as part of the example code. ;)
>
> Oh, well, should not look at code before waking up, I guess. Lucio, that's generated code.

Note that usually there are 2 ways to deal with QtDesigner. One is that
you create an XML (.ui) file that has the widget layout, which then gets
loaded by the Qt runtime and has objects instantiated.

The other is that it generates python/c++/$lang code directly rather
than a .ui file, and that essentially has the same code as would have
been run when loading the .ui file.

The difficulty in both is how you attach your own functionality to it. I
believe with the .ui file you have to have a proxy object, while with
the .py file you subclass it and add methods to your subclass, and just
regenerate the super class using QtDesigner from time to time.

John
=:->

John A Meinel (jameinel) wrote :

On 4/20/2012 1:40 PM, Lucio Torre wrote:
> That does not make it better.
> The project has the generated code and no ui file, and the code already includes some functions which were not generated.
> I would really prefer to read some static definition in runtime that we can also open back with qtdesigner.
>
>

I haven't looked at the code, but yes, if a given file is generated, it
should not be modified in place. Instead a separate file should inherit
from that one to provide the custom functionality.

John
=:->

lp:~thisfred/u1db/u1todo-2 updated on 2012-04-20
251. By Eric Casteleijn on 2012-04-20

dynamically load the ui.

Roberto Alsina (ralsina) wrote :

Eric, we should make the code follow the standards we have for the rest of Desktop team's projects. I amnot going to suggest pylint, but try to make pyflakes and pep8 do a clean run on the code, please.

Other than that, it looks pretty good now, there are some missing docstrings though ;-)

review: Needs Information
lp:~thisfred/u1db/u1todo-2 updated on 2012-04-23
252. By Eric Casteleijn on 2012-04-20

added docstrings

253. By Eric Casteleijn on 2012-04-23

moar docstrings

Roberto Alsina (ralsina) wrote :

+1

review: Approve
lp:~thisfred/u1db/u1todo-2 updated on 2012-04-23
254. By Eric Casteleijn on 2012-04-23

remerged trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'u1todo/test_u1todo.py'
2--- u1todo/test_u1todo.py 2012-04-18 20:09:57 +0000
3+++ u1todo/test_u1todo.py 2012-04-23 13:27:17 +0000
4@@ -1,3 +1,5 @@
5+"""Tests for u1todo example application."""
6+
7 from testtools import TestCase
8 from u1todo import Task, TodoStore, INDEXES, EMPTY_TASK, DONE, NOT_DONE
9 from u1db.backends import inmemory
10@@ -50,7 +52,21 @@
11 store = TodoStore(self.db)
12 task = store.new_task()
13 self.assertTrue(isinstance(task, Task))
14- self.assertIsNotNone(task.document.doc_id)
15+ self.assertIsNotNone(task.task_id)
16+
17+ def test_new_task_with_title(self):
18+ """Creates a new task."""
19+ store = TodoStore(self.db)
20+ title = "Un task muy importante"
21+ task = store.new_task(title=title)
22+ self.assertEqual(title, task.title)
23+
24+ def test_new_task_with_tags(self):
25+ """Creates a new task."""
26+ store = TodoStore(self.db)
27+ tags = ['foo', 'bar', 'bam']
28+ task = store.new_task(tags=tags)
29+ self.assertEqual(tags, task.tags)
30
31 def test_save_task_get_task(self):
32 """Saves a modified task and retrieves it from the db."""
33@@ -58,9 +74,21 @@
34 task = store.new_task()
35 task.title = "This is the title."
36 store.save_task(task)
37- task_copy = store.get_task(task.document.doc_id)
38+ task_copy = store.get_task(task.task_id)
39 self.assertEqual(task.title, task_copy.title)
40
41+ def test_get_non_existant_task(self):
42+ """Saves a modified task and retrieves it from the db."""
43+ store = TodoStore(self.db)
44+ self.assertRaises(KeyError, store.get_task, "nonexistant")
45+
46+ def test_delete_task(self):
47+ """Deletes a task by id."""
48+ store = TodoStore(self.db)
49+ task = store.new_task()
50+ store.delete_task(task)
51+ self.assertRaises(KeyError, store.get_task, task.task_id)
52+
53
54 class TaskTestCase(TestCase):
55 """Tests for Task."""
56@@ -77,6 +105,11 @@
57 self.assertEqual([], task.tags)
58 self.assertEqual(False, task.done)
59
60+ def test_task_id(self):
61+ """Task id is set to document id."""
62+ task = Task(self.document)
63+ self.assertEqual(self.document.doc_id, task.task_id)
64+
65 def test_set_title(self):
66 """Changing the title is persistent."""
67 task = Task(self.document)
68
69=== modified file 'u1todo/u1todo.py'
70--- u1todo/u1todo.py 2012-04-18 20:09:57 +0000
71+++ u1todo/u1todo.py 2012-04-23 13:27:17 +0000
72@@ -1,3 +1,5 @@
73+"""u1todo example application."""
74+
75 import json
76
77 DONE = 'true'
78@@ -18,6 +20,7 @@
79 self.db = db
80
81 def initialize_db(self):
82+ """Initialize the database."""
83 db_indexes = dict(self.db.list_indexes())
84 for name, expression in INDEXES.items():
85 if name not in db_indexes:
86@@ -40,14 +43,31 @@
87 document = self.db.get_doc(task_id)
88 if document is None:
89 raise KeyError("No task with id '%s'." % (task_id,))
90+ if document.content is None:
91+ raise KeyError("Task with id %s was deleted." % (task_id,))
92 return Task(document)
93
94- def new_task(self):
95- document = self.db.create_doc(content=EMPTY_TASK)
96+ def delete_task(self, task):
97+ """Delete a task from the database."""
98+ self.db.delete_doc(task._document)
99+
100+ def new_task(self, title=None, tags=None):
101+ """Create a new task document."""
102+ # Create the document in the u1db database
103+ content = EMPTY_TASK
104+ if title or tags:
105+ content_object = json.loads(content)
106+ content_object['title'] = title
107+ content_object['tags'] = tags
108+ content = json.dumps(content_object)
109+ document = self.db.create_doc(content=content)
110+ # Wrap the document in a Task object.
111 return Task(document)
112
113 def save_task(self, task):
114 """Save task to the database."""
115+ # Get the u1db document from the task object, and save it to the
116+ # database.
117 self.db.put_doc(task.document)
118
119
120@@ -58,6 +78,11 @@
121 self._document = document
122 self._content = json.loads(document.content)
123
124+ @property
125+ def task_id(self):
126+ """The u1db id of the task."""
127+ return self._document.doc_id
128+
129 def _get_title(self):
130 """Get the task title."""
131 return self._content['title']
132@@ -75,6 +100,7 @@
133 return True if self._content['done'] == DONE else False
134
135 def _set_done(self, value):
136+ """Set the done status."""
137 # Indexes on booleans are not currently possible, so we convert to and
138 # from strings.
139 self._content['done'] = DONE if value else NOT_DONE
140@@ -86,11 +112,13 @@
141 return self._content['tags']
142
143 def _set_tags(self, tags):
144+ """Set tags associated with the task."""
145 self._content['tags'] = list(set(tags))
146
147 tags = property(_get_tags, _set_tags, doc="Task tags.")
148
149 def add_tag(self, tag):
150+ """Add a single tag to the task."""
151 tags = self._content['tags']
152 if tag in tags:
153 # Tasks cannot have the same tag more than once, so ignore the
154@@ -99,6 +127,7 @@
155 tags.append(tag)
156
157 def remove_tag(self, tag):
158+ """Remove a single tag from the task."""
159 tags = self._content['tags']
160 if tag not in tags:
161 # Can't remove a tag that the task does not have.
162@@ -107,5 +136,6 @@
163
164 @property
165 def document(self):
166+ """The u1db document representing this task."""
167 self._document.content = json.dumps(self._content)
168 return self._document
169
170=== added file 'u1todo/u1todo.ui'
171--- u1todo/u1todo.ui 1970-01-01 00:00:00 +0000
172+++ u1todo/u1todo.ui 2012-04-23 13:27:17 +0000
173@@ -0,0 +1,58 @@
174+<?xml version="1.0" encoding="UTF-8"?>
175+<ui version="4.0">
176+ <class>MainWindow</class>
177+ <widget class="QMainWindow" name="MainWindow">
178+ <property name="geometry">
179+ <rect>
180+ <x>0</x>
181+ <y>0</y>
182+ <width>334</width>
183+ <height>834</height>
184+ </rect>
185+ </property>
186+ <property name="windowTitle">
187+ <string>MainWindow</string>
188+ </property>
189+ <widget class="QWidget" name="centralwidget">
190+ <layout class="QVBoxLayout" name="verticalLayout">
191+ <item>
192+ <widget class="QListWidget" name="list_widget"/>
193+ </item>
194+ <item>
195+ <layout class="QHBoxLayout" name="horizontalLayout">
196+ <item>
197+ <widget class="QLineEdit" name="line_edit"/>
198+ </item>
199+ <item>
200+ <widget class="QPushButton" name="edit_button">
201+ <property name="text">
202+ <string>Add/Edit</string>
203+ </property>
204+ </widget>
205+ </item>
206+ <item>
207+ <widget class="QPushButton" name="delete_button">
208+ <property name="text">
209+ <string>Delete</string>
210+ </property>
211+ </widget>
212+ </item>
213+ </layout>
214+ </item>
215+ </layout>
216+ </widget>
217+ <widget class="QMenuBar" name="menubar">
218+ <property name="geometry">
219+ <rect>
220+ <x>0</x>
221+ <y>0</y>
222+ <width>334</width>
223+ <height>21</height>
224+ </rect>
225+ </property>
226+ </widget>
227+ <widget class="QStatusBar" name="statusbar"/>
228+ </widget>
229+ <resources/>
230+ <connections/>
231+</ui>
232
233=== added file 'u1todo/ui.py'
234--- u1todo/ui.py 1970-01-01 00:00:00 +0000
235+++ u1todo/ui.py 2012-04-23 13:27:17 +0000
236@@ -0,0 +1,94 @@
237+"""User interface for the u1todo example application."""
238+
239+from u1db.backends import inmemory
240+from u1todo import TodoStore
241+import os
242+import sys
243+from PyQt4 import QtGui, QtCore, uic
244+
245+
246+class UITask(QtGui.QListWidgetItem):
247+ """Task list item."""
248+
249+ def __init__(self, task):
250+ super(UITask, self).__init__()
251+ self.task = task
252+ self.setText(self.task.title)
253+ self.setCheckState(
254+ QtCore.Qt.Checked if task.done else QtCore.Qt.Unchecked)
255+
256+
257+class Main(QtGui.QMainWindow):
258+ """Main window of our application."""
259+
260+ def __init__(self):
261+ super(Main, self).__init__()
262+ uifile = os.path.join(os.path.abspath(os.path.dirname(__file__)),
263+ 'u1todo.ui')
264+ uic.loadUi(uifile, self)
265+ db = inmemory.InMemoryDatabase("u1todo")
266+ self.store = TodoStore(db)
267+ self.connect_events()
268+ self.item = None
269+ self.delete_button.setEnabled(False)
270+
271+ def connect_events(self):
272+ """Hook up all the signal handlers."""
273+ self.connect(
274+ self.edit_button, QtCore.SIGNAL("clicked()"), self.update)
275+ self.connect(
276+ self.delete_button, QtCore.SIGNAL("clicked()"), self.delete)
277+ self.connect(
278+ self.list_widget, QtCore.SIGNAL("currentRowChanged(int)"),
279+ self.row_changed)
280+
281+ def update(self):
282+ """Either add a new task or update an existing one."""
283+ text = unicode(self.line_edit.text(), 'utf-8')
284+ if not text:
285+ return
286+ if self.item is None:
287+ self.add_item(text)
288+ else:
289+ self.update_item(text)
290+ self.line_edit.clear()
291+ self.item = None
292+
293+ def delete(self):
294+ """Delete a todo item."""
295+ item = self.list_widget.takeItem(self.list_widget.currentRow())
296+ self.store.delete_task(item.task)
297+ if self.list_widget.count() == 0:
298+ self.delete_button.setEnabled(False)
299+
300+ def add_item(self, text):
301+ """Add a new todo item."""
302+ task = self.store.new_task(text)
303+ item = UITask(task)
304+ self.list_widget.addItem(item)
305+ self.delete_button.setEnabled(True)
306+
307+ def update_item(self, text):
308+ """Edit an existing todo item."""
309+ self.item.task.title = text
310+ self.item.setText(text)
311+ self.store.save_task(self.item.task)
312+
313+ def row_changed(self, index):
314+ """Edit item when row changes."""
315+ if index == -1:
316+ self.line_edit.clear()
317+ return
318+ self.edit_item(self.list_widget.item(index))
319+
320+ def edit_item(self, item):
321+ """Edit item in line edit box."""
322+ self.item = item
323+ self.line_edit.setText(item.task.title)
324+
325+
326+if __name__ == "__main__":
327+ app = QtGui.QApplication(sys.argv)
328+ main = Main()
329+ main.show()
330+ app.exec_()

Subscribers

People subscribed via source and target branches