Merge lp:~thisfred/u1db/u1todo-design-1 into lp:u1db
- u1todo-design-1
- Merge into trunk
| Status: | Merged |
|---|---|
| Approved by: | Eric Casteleijn on 2012-08-02 |
| Approved revision: | 289 |
| Merged at revision: | 369 |
| Proposed branch: | lp:~thisfred/u1db/u1todo-design-1 |
| Merge into: | lp:u1db |
| Diff against target: |
1407 lines (+627/-411) 5 files modified
cosas/cosas.py (+32/-46) cosas/cosas.ui (+199/-156) cosas/sync.ui (+131/-0) cosas/test_cosas.py (+34/-37) cosas/ui.py (+231/-172) |
| To merge this branch: | bzr merge lp:~thisfred/u1db/u1todo-design-1 |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Roberto Alsina (community) | 2012-08-01 | Approve on 2012-08-02 | |
|
Review via email:
|
|||
Commit Message
- renamed u1todo to cosas
- added minimal design to cosas
Description of the Change
- renamed u1todo to cosas
- added minimal design to cosas
| Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
The attempt to merge lp:~thisfred/u1db/u1todo-design-1 into lp:u1db failed. Below is the output from the failed tests.
-- The C compiler identification is GNU 4.7.1
-- The CXX compiler identification is GNU 4.7.1
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Found Cython: /usr/bin/cython
-- Found CURL: /usr/lib/
-- Found PkgConfig: /usr/bin/pkg-config (found version "0.26")
-- checking for module 'oauth'
-- found oauth, version 0.9.7
-- Found OAuth: /usr/lib/
-- checking for module 'json'
-- found json, version 0.9
-- Found JSON: /usr/lib/
-- checking for module 'sqlite3'
-- found sqlite3, version 3.7.13
-- Found Sqlite3: /usr/lib/
-- Configuring done
-- Generating done
-- Build files have been written to: /mnt/tarmac/
[ 11%] Generating u1db_schema.c
Scanning dependencies of target u1db
[ 22%] Building C object src/CMakeFiles/
[ 33%] Building C object src/CMakeFiles/
[ 44%] Building C object src/CMakeFiles/
[ 55%] Building C object src/CMakeFiles/
[ 66%] Building C object src/CMakeFiles/
[ 77%] Building C object src/CMakeFiles/
[ 88%] Building C object src/CMakeFiles/
[100%] Building C object src/CMakeFiles/
Linking C static library libu1db.a
[100%] Built target u1db
Scanning dependencies of target ReplicatePython
[100%] Built target ReplicatePython
Scanning dependencies of target build-debug
running build_ext
cythoning u1db/tests/
building 'u1db.tests.
creating build
creating build/temp.
creating build/temp.
creating build/temp.
gcc -pthread -fno-strict-
gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-
[100%] Built target build-debug
Scanning dependencies of target check-valgrind
Tests running...
=======
ERROR: cosas.test_
-------
Traceback (most recent call last):
File "/mnt/tarmac/...
- 289. By Eric Casteleijn on 2012-08-02
-
fixed tests and exposed bug
Preview Diff
| 1 | === renamed directory 'u1todo' => 'cosas' |
| 2 | === renamed file 'u1todo/u1todo.py' => 'cosas/cosas.py' |
| 3 | --- u1todo/u1todo.py 2012-07-19 19:50:58 +0000 |
| 4 | +++ cosas/cosas.py 2012-08-02 13:17:19 +0000 |
| 5 | @@ -14,7 +14,7 @@ |
| 6 | # You should have received a copy of the GNU Lesser General Public License |
| 7 | # along with u1db. If not, see <http://www.gnu.org/licenses/>. |
| 8 | |
| 9 | -"""u1todo example application.""" |
| 10 | +"""cosas example application.""" |
| 11 | |
| 12 | import json |
| 13 | import os |
| 14 | @@ -28,8 +28,8 @@ |
| 15 | TAGS_INDEX = 'tags' |
| 16 | DONE_INDEX = 'done' |
| 17 | INDEXES = { |
| 18 | - TAGS_INDEX: 'tags', |
| 19 | - DONE_INDEX: 'bool(done)', |
| 20 | + TAGS_INDEX: ['tags'], |
| 21 | + DONE_INDEX: ['bool(done)'], |
| 22 | } |
| 23 | |
| 24 | TAGS = re.compile('#(\w+)|\[(.+)\]') |
| 25 | @@ -37,9 +37,12 @@ |
| 26 | |
| 27 | def get_database(): |
| 28 | """Get the path that the database is stored in.""" |
| 29 | + |
| 30 | + # setting document_factory to Task means that any database method that |
| 31 | + # would return documents, now returns Tasks instead. |
| 32 | return u1db.open( |
| 33 | - os.path.join(save_data_path("u1todo"), |
| 34 | - "u1todo.u1db"), create=True) |
| 35 | + os.path.join(save_data_path("cosas"), "cosas.u1db"), create=True, |
| 36 | + document_factory=Task) |
| 37 | |
| 38 | |
| 39 | def extract_tags(text): |
| 40 | @@ -63,7 +66,7 @@ |
| 41 | for name, expression in INDEXES.items(): |
| 42 | if name not in db_indexes: |
| 43 | # The index does not yet exist. |
| 44 | - self.db.create_index(name, expression) |
| 45 | + self.db.create_index(name, *expression) |
| 46 | continue |
| 47 | if expression == db_indexes[name]: |
| 48 | # The index exists and is up to date. |
| 49 | @@ -71,7 +74,7 @@ |
| 50 | # The index exists but the definition is not what expected, so we |
| 51 | # delete it and add the proper index expression. |
| 52 | self.db.delete_index(name) |
| 53 | - self.db.create_index(name, expression) |
| 54 | + self.db.create_index(name, *expression) |
| 55 | |
| 56 | def tag_task(self, task, tags): |
| 57 | """Set the tags of a task.""" |
| 58 | @@ -106,24 +109,24 @@ |
| 59 | # documents with all tags. |
| 60 | return [] |
| 61 | # Wrap each document in results in a Task object, and return them. |
| 62 | - return [Task(doc) for doc in results.values()] |
| 63 | + return results.values() |
| 64 | |
| 65 | - def get_task(self, task_id): |
| 66 | + def get_task(self, doc_id): |
| 67 | """Get a task from the database.""" |
| 68 | - document = self.db.get_doc(task_id) |
| 69 | - if document is None: |
| 70 | + task = self.db.get_doc(doc_id) |
| 71 | + if task is None: |
| 72 | # No document with that id exists in the database. |
| 73 | - raise KeyError("No task with id '%s'." % (task_id,)) |
| 74 | - if document.is_tombstone(): |
| 75 | + raise KeyError("No task with id '%s'." % (doc_id,)) |
| 76 | + if task.is_tombstone(): |
| 77 | # The document id exists, but the document's content was previously |
| 78 | # deleted. |
| 79 | - raise KeyError("Task with id %s was deleted." % (task_id,)) |
| 80 | + raise KeyError("Task with id %s was deleted." % (doc_id,)) |
| 81 | # Wrap the document in a Task object and return it. |
| 82 | - return Task(document) |
| 83 | + return task |
| 84 | |
| 85 | def delete_task(self, task): |
| 86 | """Delete a task from the database.""" |
| 87 | - self.db.delete_doc(task._document) |
| 88 | + self.db.delete_doc(task) |
| 89 | |
| 90 | def new_task(self, title=None, tags=None): |
| 91 | """Create a new task document.""" |
| 92 | @@ -144,69 +147,52 @@ |
| 93 | # Store the document in the database. Since we did not set a document |
| 94 | # id, the database will store it as a new document, and generate |
| 95 | # a valid id. |
| 96 | - document = self.db.create_doc_from_json(content) |
| 97 | + task = self.db.create_doc_from_json(content) |
| 98 | # Wrap the document in a Task object. |
| 99 | - return Task(document) |
| 100 | + return task |
| 101 | |
| 102 | def save_task(self, task): |
| 103 | """Save task to the database.""" |
| 104 | # Get the u1db document from the task object, and save it to the |
| 105 | # database. |
| 106 | - self.db.put_doc(task.document) |
| 107 | + self.db.put_doc(task) |
| 108 | |
| 109 | def get_all_tasks(self): |
| 110 | # Since the DONE_INDEX indexes anything that has a value in the field |
| 111 | # "done", and all tasks do (either True or False), it's a good way to |
| 112 | # get all tasks out of the database. |
| 113 | - return [Task(doc) for doc in self.db.get_from_index(DONE_INDEX, "*")] |
| 114 | - |
| 115 | - |
| 116 | -class Task(object): |
| 117 | + return self.db.get_from_index(DONE_INDEX, "*") |
| 118 | + |
| 119 | + |
| 120 | +class Task(u1db.Document): |
| 121 | """A todo item.""" |
| 122 | |
| 123 | - def __init__(self, document): |
| 124 | - self._document = document |
| 125 | - |
| 126 | - @property |
| 127 | - def task_id(self): |
| 128 | - """The u1db id of the task.""" |
| 129 | - # Since we won't ever change this, but it's handy for comparing |
| 130 | - # different Task objects, it's a read only property. |
| 131 | - return self._document.doc_id |
| 132 | - |
| 133 | def _get_title(self): |
| 134 | """Get the task title.""" |
| 135 | - return self._document.content['title'] |
| 136 | + return self.content['title'] |
| 137 | |
| 138 | def _set_title(self, title): |
| 139 | """Set the task title.""" |
| 140 | - self._document.content['title'] = title |
| 141 | + self.content['title'] = title |
| 142 | |
| 143 | title = property(_get_title, _set_title, doc="Title of the task.") |
| 144 | |
| 145 | def _get_done(self): |
| 146 | """Get the status of the task.""" |
| 147 | - return self._document.content['done'] |
| 148 | + return self.content['done'] |
| 149 | |
| 150 | def _set_done(self, value): |
| 151 | """Set the done status.""" |
| 152 | - self._document.content['done'] = value |
| 153 | + self.content['done'] = value |
| 154 | |
| 155 | done = property(_get_done, _set_done, doc="Done flag.") |
| 156 | |
| 157 | def _get_tags(self): |
| 158 | """Get tags associated with the task.""" |
| 159 | - return self._document.content['tags'] |
| 160 | + return self.content['tags'] |
| 161 | |
| 162 | def _set_tags(self, tags): |
| 163 | """Set tags associated with the task.""" |
| 164 | - self._document.content['tags'] = list(set(tags)) |
| 165 | + self.content['tags'] = list(set(tags)) |
| 166 | |
| 167 | tags = property(_get_tags, _set_tags, doc="Task tags.") |
| 168 | - |
| 169 | - @property |
| 170 | - def document(self): |
| 171 | - """The u1db document representing this task.""" |
| 172 | - # This brings the underlying document's JSON content back into sync |
| 173 | - # with whatever data the task currently holds. |
| 174 | - return self._document |
| 175 | |
| 176 | === renamed file 'u1todo/u1todo.ui' => 'cosas/cosas.ui' |
| 177 | --- u1todo/u1todo.ui 2012-05-08 18:27:32 +0000 |
| 178 | +++ cosas/cosas.ui 2012-08-02 13:17:19 +0000 |
| 179 | @@ -6,175 +6,218 @@ |
| 180 | <rect> |
| 181 | <x>0</x> |
| 182 | <y>0</y> |
| 183 | - <width>328</width> |
| 184 | - <height>349</height> |
| 185 | + <width>329</width> |
| 186 | + <height>586</height> |
| 187 | </rect> |
| 188 | </property> |
| 189 | + <property name="sizePolicy"> |
| 190 | + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> |
| 191 | + <horstretch>0</horstretch> |
| 192 | + <verstretch>0</verstretch> |
| 193 | + </sizepolicy> |
| 194 | + </property> |
| 195 | <property name="windowTitle"> |
| 196 | - <string>u1todo</string> |
| 197 | + <string>cosas</string> |
| 198 | + </property> |
| 199 | + <property name="styleSheet"> |
| 200 | + <string notr="true">QToolButton { |
| 201 | + background-color: rgb(203,203,203); |
| 202 | + border: 1px solid rgb(102, 102, 102); |
| 203 | +} |
| 204 | + |
| 205 | +QFrame#drop_frame{ |
| 206 | + border: 0px; |
| 207 | + border-top: 2px dotted rgb(102,102,102); |
| 208 | + border-bottom: 2px dotted rgb(102,102,102); |
| 209 | +} |
| 210 | + |
| 211 | +QPushButton { |
| 212 | + background-color: rgb(61, 133, 198); |
| 213 | + color: white; |
| 214 | + border: 2px solid rgb(7, 55, 99); |
| 215 | + border-radius: 6 6 px; |
| 216 | + padding: 5 px; |
| 217 | +} |
| 218 | + |
| 219 | +QComboBox { |
| 220 | + background-color: rgb(204, 204, 204); |
| 221 | + color: black; |
| 222 | + border: 2px solid rgb(102,102,102); |
| 223 | + border-radius: 6 6 px; |
| 224 | + padding: 5 px; |
| 225 | +} |
| 226 | + |
| 227 | +QFrame#buttons_frame QPushButton { |
| 228 | + background-color: rgb(204, 204, 204); |
| 229 | + color: black; |
| 230 | + border: 2px solid rgb(102,102,102); |
| 231 | + border-radius: 0px; |
| 232 | +} |
| 233 | + |
| 234 | +QLineEdit, QTextEdit { |
| 235 | + background-color: white; |
| 236 | + border-radius: 0; |
| 237 | + border: 1px solid rgb(102,102,102); |
| 238 | +} |
| 239 | + |
| 240 | +*[foo="bar"] {color:red;} |
| 241 | +</string> |
| 242 | + </property> |
| 243 | + <property name="toolButtonStyle"> |
| 244 | + <enum>Qt::ToolButtonFollowStyle</enum> |
| 245 | + </property> |
| 246 | + <property name="documentMode"> |
| 247 | + <bool>false</bool> |
| 248 | </property> |
| 249 | <widget class="QWidget" name="centralwidget"> |
| 250 | - <layout class="QVBoxLayout" name="verticalLayout_3"> |
| 251 | - <item> |
| 252 | - <widget class="QTabWidget" name="tabWidget"> |
| 253 | - <property name="currentIndex"> |
| 254 | + <layout class="QHBoxLayout" name="horizontalLayout_2"> |
| 255 | + <property name="spacing"> |
| 256 | + <number>0</number> |
| 257 | + </property> |
| 258 | + <property name="margin"> |
| 259 | + <number>0</number> |
| 260 | + </property> |
| 261 | + <item> |
| 262 | + <widget class="QFrame" name="buttons_frame"> |
| 263 | + <layout class="QVBoxLayout" name="tag_buttons"> |
| 264 | + <property name="spacing"> |
| 265 | + <number>0</number> |
| 266 | + </property> |
| 267 | + <property name="margin"> |
| 268 | + <number>0</number> |
| 269 | + </property> |
| 270 | + <item> |
| 271 | + <layout class="QVBoxLayout" name="buttons_layout"> |
| 272 | + <item> |
| 273 | + <spacer name="verticalSpacer"> |
| 274 | + <property name="orientation"> |
| 275 | + <enum>Qt::Vertical</enum> |
| 276 | + </property> |
| 277 | + <property name="sizeHint" stdset="0"> |
| 278 | + <size> |
| 279 | + <width>20</width> |
| 280 | + <height>40</height> |
| 281 | + </size> |
| 282 | + </property> |
| 283 | + </spacer> |
| 284 | + </item> |
| 285 | + </layout> |
| 286 | + </item> |
| 287 | + </layout> |
| 288 | + </widget> |
| 289 | + </item> |
| 290 | + <item> |
| 291 | + <widget class="QFrame" name="frame_2"> |
| 292 | + <property name="frameShape"> |
| 293 | + <enum>QFrame::NoFrame</enum> |
| 294 | + </property> |
| 295 | + <property name="frameShadow"> |
| 296 | + <enum>QFrame::Plain</enum> |
| 297 | + </property> |
| 298 | + <property name="lineWidth"> |
| 299 | <number>0</number> |
| 300 | </property> |
| 301 | - <widget class="QWidget" name="tab_3"> |
| 302 | - <attribute name="title"> |
| 303 | - <string>u1&todo</string> |
| 304 | - </attribute> |
| 305 | - <layout class="QVBoxLayout" name="verticalLayout_2"> |
| 306 | - <item> |
| 307 | - <layout class="QHBoxLayout" name="horizontalLayout_2"> |
| 308 | - <item> |
| 309 | - <layout class="QVBoxLayout" name="verticalLayout"> |
| 310 | - <item> |
| 311 | - <widget class="QListWidget" name="todo_list"/> |
| 312 | - </item> |
| 313 | - <item> |
| 314 | - <widget class="QLineEdit" name="task_edit"/> |
| 315 | - </item> |
| 316 | - <item> |
| 317 | - <layout class="QHBoxLayout" name="horizontalLayout"> |
| 318 | - <item> |
| 319 | - <widget class="QPushButton" name="edit_button"> |
| 320 | - <property name="text"> |
| 321 | - <string>Add/&Edit</string> |
| 322 | - </property> |
| 323 | - </widget> |
| 324 | - </item> |
| 325 | - <item> |
| 326 | - <widget class="QPushButton" name="delete_button"> |
| 327 | - <property name="text"> |
| 328 | - <string>&Delete</string> |
| 329 | - </property> |
| 330 | - </widget> |
| 331 | - </item> |
| 332 | - </layout> |
| 333 | - </item> |
| 334 | - </layout> |
| 335 | - </item> |
| 336 | - <item> |
| 337 | - <layout class="QVBoxLayout" name="tag_buttons"> |
| 338 | - <item> |
| 339 | - <spacer name="verticalSpacer"> |
| 340 | - <property name="orientation"> |
| 341 | - <enum>Qt::Vertical</enum> |
| 342 | - </property> |
| 343 | - <property name="sizeHint" stdset="0"> |
| 344 | - <size> |
| 345 | - <width>20</width> |
| 346 | - <height>40</height> |
| 347 | - </size> |
| 348 | - </property> |
| 349 | - </spacer> |
| 350 | - </item> |
| 351 | - </layout> |
| 352 | - </item> |
| 353 | - </layout> |
| 354 | - </item> |
| 355 | - </layout> |
| 356 | - </widget> |
| 357 | - <widget class="QWidget" name="tab_4"> |
| 358 | - <attribute name="title"> |
| 359 | - <string>&sync</string> |
| 360 | - </attribute> |
| 361 | - <layout class="QVBoxLayout" name="verticalLayout_4"> |
| 362 | - <item> |
| 363 | - <widget class="QGroupBox" name="target_group"> |
| 364 | - <property name="title"> |
| 365 | - <string>Synchronization target</string> |
| 366 | - </property> |
| 367 | - <layout class="QVBoxLayout" name="verticalLayout_5"> |
| 368 | - <item> |
| 369 | - <widget class="QRadioButton" name="u1_radio"> |
| 370 | - <property name="text"> |
| 371 | - <string>Ubuntu One</string> |
| 372 | - </property> |
| 373 | - <property name="checked"> |
| 374 | - <bool>true</bool> |
| 375 | - </property> |
| 376 | - <attribute name="buttonGroup"> |
| 377 | - <string notr="true">sync_targets</string> |
| 378 | - </attribute> |
| 379 | - </widget> |
| 380 | - </item> |
| 381 | - <item> |
| 382 | - <layout class="QHBoxLayout" name="horizontalLayout_3"> |
| 383 | - <item> |
| 384 | - <widget class="QRadioButton" name="url_radio"> |
| 385 | - <property name="text"> |
| 386 | - <string>url:</string> |
| 387 | - </property> |
| 388 | - <attribute name="buttonGroup"> |
| 389 | - <string notr="true">sync_targets</string> |
| 390 | - </attribute> |
| 391 | - </widget> |
| 392 | - </item> |
| 393 | - <item> |
| 394 | - <widget class="QLineEdit" name="url_edit"/> |
| 395 | - </item> |
| 396 | - </layout> |
| 397 | - </item> |
| 398 | - <item> |
| 399 | - <layout class="QHBoxLayout" name="horizontalLayout_4"> |
| 400 | - <item> |
| 401 | - <widget class="QLabel" name="label"> |
| 402 | - <property name="text"> |
| 403 | - <string>Last Synced:</string> |
| 404 | - </property> |
| 405 | - </widget> |
| 406 | - </item> |
| 407 | - <item> |
| 408 | - <widget class="QLabel" name="last_synced"> |
| 409 | - <property name="text"> |
| 410 | - <string><html><head/><body><p><br/></p></body></html></string> |
| 411 | - </property> |
| 412 | - </widget> |
| 413 | - </item> |
| 414 | - </layout> |
| 415 | - </item> |
| 416 | - </layout> |
| 417 | - </widget> |
| 418 | - </item> |
| 419 | - <item> |
| 420 | - <widget class="QPushButton" name="sync_button"> |
| 421 | + <layout class="QVBoxLayout" name="verticalLayout"> |
| 422 | + <property name="spacing"> |
| 423 | + <number>0</number> |
| 424 | + </property> |
| 425 | + <property name="margin"> |
| 426 | + <number>0</number> |
| 427 | + </property> |
| 428 | + <item> |
| 429 | + <layout class="QHBoxLayout" name="horizontalLayout_5"> |
| 430 | + <property name="margin"> |
| 431 | + <number>6</number> |
| 432 | + </property> |
| 433 | + <item> |
| 434 | + <widget class="QToolButton" name="buttons_toggle"> |
| 435 | + <property name="autoFillBackground"> |
| 436 | + <bool>false</bool> |
| 437 | + </property> |
| 438 | + <property name="text"> |
| 439 | + <string/> |
| 440 | + </property> |
| 441 | + <property name="checkable"> |
| 442 | + <bool>true</bool> |
| 443 | + </property> |
| 444 | + <property name="autoRaise"> |
| 445 | + <bool>false</bool> |
| 446 | + </property> |
| 447 | + <property name="arrowType"> |
| 448 | + <enum>Qt::LeftArrow</enum> |
| 449 | + </property> |
| 450 | + </widget> |
| 451 | + </item> |
| 452 | + <item> |
| 453 | + <widget class="QLineEdit" name="title_edit"> |
| 454 | + <property name="text"> |
| 455 | + <string>Create a new task</string> |
| 456 | + </property> |
| 457 | + </widget> |
| 458 | + </item> |
| 459 | + </layout> |
| 460 | + </item> |
| 461 | + <item> |
| 462 | + <widget class="QTreeWidget" name="todo_list"> |
| 463 | + <property name="frameShape"> |
| 464 | + <enum>QFrame::StyledPanel</enum> |
| 465 | + </property> |
| 466 | + <property name="frameShadow"> |
| 467 | + <enum>QFrame::Plain</enum> |
| 468 | + </property> |
| 469 | + <property name="indentation"> |
| 470 | + <number>0</number> |
| 471 | + </property> |
| 472 | + <property name="uniformRowHeights"> |
| 473 | + <bool>true</bool> |
| 474 | + </property> |
| 475 | + <property name="animated"> |
| 476 | + <bool>true</bool> |
| 477 | + </property> |
| 478 | + <property name="columnCount"> |
| 479 | + <number>1</number> |
| 480 | + </property> |
| 481 | + <property name="topLevelItemCount" stdset="0"> |
| 482 | + <number>0</number> |
| 483 | + </property> |
| 484 | + <attribute name="headerVisible"> |
| 485 | + <bool>false</bool> |
| 486 | + </attribute> |
| 487 | + <column> |
| 488 | <property name="text"> |
| 489 | - <string>Syn&chronize</string> |
| 490 | - </property> |
| 491 | - </widget> |
| 492 | - </item> |
| 493 | - <item> |
| 494 | - <spacer name="verticalSpacer_2"> |
| 495 | - <property name="orientation"> |
| 496 | - <enum>Qt::Vertical</enum> |
| 497 | - </property> |
| 498 | - <property name="sizeHint" stdset="0"> |
| 499 | - <size> |
| 500 | - <width>20</width> |
| 501 | - <height>40</height> |
| 502 | - </size> |
| 503 | - </property> |
| 504 | - </spacer> |
| 505 | - </item> |
| 506 | - </layout> |
| 507 | - </widget> |
| 508 | + <string>title</string> |
| 509 | + </property> |
| 510 | + </column> |
| 511 | + </widget> |
| 512 | + </item> |
| 513 | + </layout> |
| 514 | </widget> |
| 515 | </item> |
| 516 | </layout> |
| 517 | </widget> |
| 518 | - <widget class="QStatusBar" name="statusbar"/> |
| 519 | + <widget class="QMenuBar" name="menuBar"> |
| 520 | + <property name="geometry"> |
| 521 | + <rect> |
| 522 | + <x>0</x> |
| 523 | + <y>0</y> |
| 524 | + <width>329</width> |
| 525 | + <height>21</height> |
| 526 | + </rect> |
| 527 | + </property> |
| 528 | + <widget class="QMenu" name="menu_File"> |
| 529 | + <property name="title"> |
| 530 | + <string>&File</string> |
| 531 | + </property> |
| 532 | + <addaction name="action_synchronize"/> |
| 533 | + </widget> |
| 534 | + <addaction name="menu_File"/> |
| 535 | + </widget> |
| 536 | + <action name="action_synchronize"> |
| 537 | + <property name="text"> |
| 538 | + <string>&Synchronize</string> |
| 539 | + </property> |
| 540 | + </action> |
| 541 | </widget> |
| 542 | - <tabstops> |
| 543 | - <tabstop>task_edit</tabstop> |
| 544 | - <tabstop>edit_button</tabstop> |
| 545 | - <tabstop>delete_button</tabstop> |
| 546 | - <tabstop>todo_list</tabstop> |
| 547 | - </tabstops> |
| 548 | <resources/> |
| 549 | <connections/> |
| 550 | - <buttongroups> |
| 551 | - <buttongroup name="sync_targets"/> |
| 552 | - </buttongroups> |
| 553 | </ui> |
| 554 | |
| 555 | === added file 'cosas/sync.ui' |
| 556 | --- cosas/sync.ui 1970-01-01 00:00:00 +0000 |
| 557 | +++ cosas/sync.ui 2012-08-02 13:17:19 +0000 |
| 558 | @@ -0,0 +1,131 @@ |
| 559 | +<?xml version="1.0" encoding="UTF-8"?> |
| 560 | +<ui version="4.0"> |
| 561 | + <class>Dialog</class> |
| 562 | + <widget class="QDialog" name="Dialog"> |
| 563 | + <property name="geometry"> |
| 564 | + <rect> |
| 565 | + <x>0</x> |
| 566 | + <y>0</y> |
| 567 | + <width>329</width> |
| 568 | + <height>169</height> |
| 569 | + </rect> |
| 570 | + </property> |
| 571 | + <property name="windowTitle"> |
| 572 | + <string>synchronize</string> |
| 573 | + </property> |
| 574 | + <property name="styleSheet"> |
| 575 | + <string notr="true">QToolButton { |
| 576 | + background-color: rgb(203,203,203); |
| 577 | + border: 1px solid rgb(102, 102, 102); |
| 578 | +} |
| 579 | + |
| 580 | +QFrame#drop_frame{ |
| 581 | + border: 0px; |
| 582 | + border-top: 2px dotted rgb(102,102,102); |
| 583 | + border-bottom: 2px dotted rgb(102,102,102); |
| 584 | +} |
| 585 | + |
| 586 | +QPushButton { |
| 587 | + background-color: rgb(61, 133, 198); |
| 588 | + color: white; |
| 589 | + border: 2px solid rgb(7, 55, 99); |
| 590 | + border-radius: 6 6 px; |
| 591 | + padding: 5 px; |
| 592 | +} |
| 593 | + |
| 594 | +QComboBox { |
| 595 | + background-color: rgb(204, 204, 204); |
| 596 | + color: black; |
| 597 | + border: 2px solid rgb(102,102,102); |
| 598 | + border-radius: 6 6 px; |
| 599 | + padding: 5 px; |
| 600 | +} |
| 601 | + |
| 602 | +QFrame#buttons_frame QPushButton { |
| 603 | + background-color: rgb(204, 204, 204); |
| 604 | + color: black; |
| 605 | + border: 2px solid rgb(102,102,102); |
| 606 | + border-radius: 0px; |
| 607 | +} |
| 608 | + |
| 609 | +QTreeView::item{ |
| 610 | + border: 1px dotted rgb(102, 102, 102); |
| 611 | + border-top: 0px; |
| 612 | + background-color: white; |
| 613 | +} |
| 614 | + |
| 615 | +QLineEdit, QTextEdit { |
| 616 | + background-color: white; |
| 617 | + border-radius: 0; |
| 618 | + border: 1px solid rgb(102,102,102); |
| 619 | +} |
| 620 | + |
| 621 | +* { |
| 622 | + background-color: rgb(239, 239, 239); |
| 623 | +} |
| 624 | +</string> |
| 625 | + </property> |
| 626 | + <layout class="QVBoxLayout" name="verticalLayout"> |
| 627 | + <item> |
| 628 | + <widget class="QGroupBox" name="target_group"> |
| 629 | + <property name="title"> |
| 630 | + <string>Synchronization target</string> |
| 631 | + </property> |
| 632 | + <layout class="QVBoxLayout" name="verticalLayout_5"> |
| 633 | + <item> |
| 634 | + <widget class="QRadioButton" name="u1_radio"> |
| 635 | + <property name="text"> |
| 636 | + <string>Ubuntu One</string> |
| 637 | + </property> |
| 638 | + <property name="checked"> |
| 639 | + <bool>true</bool> |
| 640 | + </property> |
| 641 | + </widget> |
| 642 | + </item> |
| 643 | + <item> |
| 644 | + <layout class="QHBoxLayout" name="horizontalLayout_3"> |
| 645 | + <item> |
| 646 | + <widget class="QRadioButton" name="url_radio"> |
| 647 | + <property name="text"> |
| 648 | + <string>url:</string> |
| 649 | + </property> |
| 650 | + </widget> |
| 651 | + </item> |
| 652 | + <item> |
| 653 | + <widget class="QLineEdit" name="url_edit"/> |
| 654 | + </item> |
| 655 | + </layout> |
| 656 | + </item> |
| 657 | + <item> |
| 658 | + <layout class="QHBoxLayout" name="horizontalLayout_4"> |
| 659 | + <item> |
| 660 | + <widget class="QLabel" name="label"> |
| 661 | + <property name="text"> |
| 662 | + <string>Last Synced:</string> |
| 663 | + </property> |
| 664 | + </widget> |
| 665 | + </item> |
| 666 | + <item> |
| 667 | + <widget class="QLabel" name="last_synced"> |
| 668 | + <property name="text"> |
| 669 | + <string><html><head/><body><p><br/></p></body></html></string> |
| 670 | + </property> |
| 671 | + </widget> |
| 672 | + </item> |
| 673 | + </layout> |
| 674 | + </item> |
| 675 | + </layout> |
| 676 | + </widget> |
| 677 | + </item> |
| 678 | + <item> |
| 679 | + <widget class="QPushButton" name="sync_button"> |
| 680 | + <property name="text"> |
| 681 | + <string>Syn&chronize</string> |
| 682 | + </property> |
| 683 | + </widget> |
| 684 | + </item> |
| 685 | + </layout> |
| 686 | + </widget> |
| 687 | + <resources/> |
| 688 | + <connections/> |
| 689 | +</ui> |
| 690 | |
| 691 | === renamed file 'u1todo/test_u1todo.py' => 'cosas/test_cosas.py' |
| 692 | --- u1todo/test_u1todo.py 2012-07-19 19:50:58 +0000 |
| 693 | +++ cosas/test_cosas.py 2012-08-02 13:17:19 +0000 |
| 694 | @@ -14,10 +14,10 @@ |
| 695 | # You should have received a copy of the GNU Lesser General Public License |
| 696 | # along with u1db. If not, see <http://www.gnu.org/licenses/>. |
| 697 | |
| 698 | -"""Tests for u1todo example application.""" |
| 699 | +"""Tests for cosas example application.""" |
| 700 | |
| 701 | from testtools import TestCase |
| 702 | -from u1todo import ( |
| 703 | +from cosas import ( |
| 704 | Task, TodoStore, INDEXES, TAGS_INDEX, EMPTY_TASK, extract_tags) |
| 705 | from u1db.backends import inmemory |
| 706 | |
| 707 | @@ -26,14 +26,15 @@ |
| 708 | |
| 709 | def setUp(self): |
| 710 | super(TodoStoreTestCase, self).setUp() |
| 711 | - self.db = inmemory.InMemoryDatabase("u1todo") |
| 712 | + self.db = inmemory.InMemoryDatabase("cosas") |
| 713 | + self.db.set_document_factory(Task) |
| 714 | |
| 715 | def test_initialize_db(self): |
| 716 | """Creates indexes.""" |
| 717 | store = TodoStore(self.db) |
| 718 | store.initialize_db() |
| 719 | for key, value in self.db.list_indexes(): |
| 720 | - self.assertEqual(INDEXES[key], value[0]) |
| 721 | + self.assertEqual(INDEXES[key], value) |
| 722 | |
| 723 | def test_reinitialize_db(self): |
| 724 | """Creates indexes.""" |
| 725 | @@ -41,13 +42,13 @@ |
| 726 | store.new_task() |
| 727 | store.initialize_db() |
| 728 | for key, value in self.db.list_indexes(): |
| 729 | - self.assertEqual(INDEXES[key], value[0]) |
| 730 | + self.assertEqual(INDEXES[key], value) |
| 731 | |
| 732 | def test_indexes_are_added(self): |
| 733 | """New indexes are added when a new store is created.""" |
| 734 | store = TodoStore(self.db) |
| 735 | store.initialize_db() |
| 736 | - INDEXES['foo'] = 'bar' |
| 737 | + INDEXES['foo'] = ['bar'] |
| 738 | self.assertNotIn('foo', dict(self.db.list_indexes())) |
| 739 | store = TodoStore(self.db) |
| 740 | store.initialize_db() |
| 741 | @@ -58,7 +59,7 @@ |
| 742 | store = TodoStore(self.db) |
| 743 | store.initialize_db() |
| 744 | new_expression = 'newtags' |
| 745 | - INDEXES[TAGS_INDEX] = new_expression |
| 746 | + INDEXES[TAGS_INDEX] = [new_expression] |
| 747 | self.assertNotEqual( |
| 748 | new_expression, dict(self.db.list_indexes())['tags']) |
| 749 | store = TodoStore(self.db) |
| 750 | @@ -95,14 +96,14 @@ |
| 751 | tags2 = ['foo', 'sball'] |
| 752 | task2 = store.new_task(tags=tags2) |
| 753 | self.assertEqual( |
| 754 | - sorted([task1.task_id, task2.task_id]), |
| 755 | - sorted([t.task_id for t in store.get_tasks_by_tags(['foo'])])) |
| 756 | - self.assertEqual( |
| 757 | - [task1.task_id], |
| 758 | - [t.task_id for t in store.get_tasks_by_tags(['foo', 'bar'])]) |
| 759 | - self.assertEqual( |
| 760 | - [task2.task_id], |
| 761 | - [t.task_id for t in store.get_tasks_by_tags(['foo', 'sball'])]) |
| 762 | + sorted([task1.doc_id, task2.doc_id]), |
| 763 | + sorted([t.doc_id for t in store.get_tasks_by_tags(['foo'])])) |
| 764 | + self.assertEqual( |
| 765 | + [task1.doc_id], |
| 766 | + [t.doc_id for t in store.get_tasks_by_tags(['foo', 'bar'])]) |
| 767 | + self.assertEqual( |
| 768 | + [task2.doc_id], |
| 769 | + [t.doc_id for t in store.get_tasks_by_tags(['foo', 'sball'])]) |
| 770 | |
| 771 | def test_get_tasks_by_empty_tags(self): |
| 772 | store = TodoStore(self.db) |
| 773 | @@ -112,8 +113,8 @@ |
| 774 | tags2 = ['foo', 'sball'] |
| 775 | task2 = store.new_task(tags=tags2) |
| 776 | self.assertEqual( |
| 777 | - sorted([task1.task_id, task2.task_id]), |
| 778 | - sorted([t.task_id for t in store.get_tasks_by_tags([])])) |
| 779 | + sorted([task1.doc_id, task2.doc_id]), |
| 780 | + sorted([t.doc_id for t in store.get_tasks_by_tags([])])) |
| 781 | |
| 782 | def test_tag_task(self): |
| 783 | """Sets the tags for a task.""" |
| 784 | @@ -128,7 +129,7 @@ |
| 785 | store = TodoStore(self.db) |
| 786 | task = store.new_task() |
| 787 | self.assertTrue(isinstance(task, Task)) |
| 788 | - self.assertIsNotNone(task.task_id) |
| 789 | + self.assertIsNotNone(task.doc_id) |
| 790 | |
| 791 | def test_new_task_with_title(self): |
| 792 | """Creates a new task.""" |
| 793 | @@ -150,7 +151,7 @@ |
| 794 | task = store.new_task() |
| 795 | task.title = "This is the title." |
| 796 | store.save_task(task) |
| 797 | - task_copy = store.get_task(task.task_id) |
| 798 | + task_copy = store.get_task(task.doc_id) |
| 799 | self.assertEqual(task.title, task_copy.title) |
| 800 | |
| 801 | def test_get_non_existant_task(self): |
| 802 | @@ -163,7 +164,7 @@ |
| 803 | store = TodoStore(self.db) |
| 804 | task = store.new_task() |
| 805 | store.delete_task(task) |
| 806 | - self.assertRaises(KeyError, store.get_task, task.task_id) |
| 807 | + self.assertRaises(KeyError, store.get_task, task.doc_id) |
| 808 | |
| 809 | def test_get_all_tasks(self): |
| 810 | store = TodoStore(self.db) |
| 811 | @@ -171,9 +172,9 @@ |
| 812 | task1 = store.new_task() |
| 813 | task2 = store.new_task() |
| 814 | task3 = store.new_task() |
| 815 | - task_ids = [task.task_id for task in store.get_all_tasks()] |
| 816 | + task_ids = [task.doc_id for task in store.get_all_tasks()] |
| 817 | self.assertEqual( |
| 818 | - sorted([task1.task_id, task2.task_id, task3.task_id]), |
| 819 | + sorted([task1.doc_id, task2.doc_id, task3.doc_id]), |
| 820 | sorted(task_ids)) |
| 821 | |
| 822 | |
| 823 | @@ -182,34 +183,30 @@ |
| 824 | |
| 825 | def setUp(self): |
| 826 | super(TaskTestCase, self).setUp() |
| 827 | - self.db = inmemory.InMemoryDatabase("u1todo") |
| 828 | + self.db = inmemory.InMemoryDatabase("cosas") |
| 829 | + self.db.set_document_factory(Task) |
| 830 | self.document = self.db.create_doc_from_json(EMPTY_TASK) |
| 831 | |
| 832 | def test_task(self): |
| 833 | """Initializing a task.""" |
| 834 | - task = Task(self.document) |
| 835 | + task = self.document |
| 836 | self.assertEqual("", task.title) |
| 837 | self.assertEqual([], task.tags) |
| 838 | self.assertEqual(False, task.done) |
| 839 | |
| 840 | - def test_task_id(self): |
| 841 | - """Task id is set to document id.""" |
| 842 | - task = Task(self.document) |
| 843 | - self.assertEqual(self.document.doc_id, task.task_id) |
| 844 | - |
| 845 | def test_set_title(self): |
| 846 | """Changing the title is persistent.""" |
| 847 | - task = Task(self.document) |
| 848 | + task = self.document |
| 849 | title = "new task" |
| 850 | task.title = title |
| 851 | - self.assertEqual(title, task._document.content['title']) |
| 852 | + self.assertEqual(title, task.content['title']) |
| 853 | |
| 854 | def test_set_done(self): |
| 855 | """Changing the done property changes the underlying content.""" |
| 856 | - task = Task(self.document) |
| 857 | - self.assertEqual(False, task._document.content['done']) |
| 858 | + task = self.document |
| 859 | + self.assertEqual(False, task.content['done']) |
| 860 | task.done = True |
| 861 | - self.assertEqual(True, task._document.content['done']) |
| 862 | + self.assertEqual(True, task.content['done']) |
| 863 | |
| 864 | def test_extracts_tags(self): |
| 865 | """Tags are extracted from the item's text.""" |
| 866 | @@ -218,11 +215,11 @@ |
| 867 | |
| 868 | def test_tags(self): |
| 869 | """Tags property returns a list.""" |
| 870 | - task = Task(self.document) |
| 871 | + task = self.document |
| 872 | self.assertEqual([], task.tags) |
| 873 | |
| 874 | def set_tags(self): |
| 875 | """Setting the tags property changes the underlying content.""" |
| 876 | - task = Task(self.document) |
| 877 | + task = self.document |
| 878 | task.tags = ["foo", "bar"] |
| 879 | - self.assertEqual(["foo", "bar"], task._document.content['tags']) |
| 880 | + self.assertEqual(["foo", "bar"], task.content['tags']) |
| 881 | |
| 882 | === modified file 'cosas/ui.py' |
| 883 | --- u1todo/ui.py 2012-05-09 15:06:46 +0000 |
| 884 | +++ cosas/ui.py 2012-08-02 13:17:19 +0000 |
| 885 | @@ -14,7 +14,7 @@ |
| 886 | # You should have received a copy of the GNU Lesser General Public License |
| 887 | # along with u1db. If not, see <http://www.gnu.org/licenses/>. |
| 888 | |
| 889 | -"""User interface for the u1todo example application.""" |
| 890 | +"""User interface for the cosas example application.""" |
| 891 | |
| 892 | from collections import defaultdict |
| 893 | from datetime import datetime |
| 894 | @@ -22,7 +22,7 @@ |
| 895 | import sys |
| 896 | from PyQt4 import QtGui, QtCore, uic |
| 897 | |
| 898 | -from u1todo import TodoStore, get_database, extract_tags |
| 899 | +from cosas import TodoStore, get_database, extract_tags |
| 900 | import u1db |
| 901 | from u1db.errors import DatabaseDoesNotExist |
| 902 | from u1db.sync import Synchronizer |
| 903 | @@ -30,127 +30,114 @@ |
| 904 | from u1db.remote.http_database import HTTPDatabase |
| 905 | from ubuntuone.platform.credentials import CredentialsManagementTool |
| 906 | |
| 907 | - |
| 908 | -class UITask(QtGui.QListWidgetItem): |
| 909 | +DONE_COLOR = QtGui.QColor(183, 183, 183) |
| 910 | +NOT_DONE_COLOR = QtGui.QColor(0, 0, 0) |
| 911 | +WHITE = QtGui.QColor(255, 255, 255) |
| 912 | +TAG_COLORS = [ |
| 913 | + (234, 153, 153), |
| 914 | + (249, 203, 156), |
| 915 | + (255, 229, 153), |
| 916 | + (182, 215, 168), |
| 917 | + (162, 196, 201), |
| 918 | + (164, 194, 244), |
| 919 | + (221, 126, 107), |
| 920 | + (159, 197, 232), |
| 921 | + (180, 167, 214), |
| 922 | + (213, 166, 189)] |
| 923 | + |
| 924 | + |
| 925 | +class UITask(QtGui.QTreeWidgetItem): |
| 926 | """Task list item.""" |
| 927 | |
| 928 | - def __init__(self, task): |
| 929 | - super(UITask, self).__init__() |
| 930 | + def __init__(self, task, parent, store, font, main_window): |
| 931 | + super(UITask, self).__init__(parent) |
| 932 | self.task = task |
| 933 | - # Set the list item's text to the task's title. |
| 934 | - self.setText(self.task.title) |
| 935 | # If the task is done, check off the list item. |
| 936 | - self.setCheckState( |
| 937 | - QtCore.Qt.Checked if task.done else QtCore.Qt.Unchecked) |
| 938 | - self.update_strikethrough() |
| 939 | - |
| 940 | - def update_strikethrough(self): |
| 941 | - font = self.font() |
| 942 | - font.setStrikeOut(self.task.done) |
| 943 | - self.setFont(font) |
| 944 | - |
| 945 | - |
| 946 | -class Main(QtGui.QMainWindow): |
| 947 | - """Main window of our application.""" |
| 948 | - |
| 949 | - def __init__(self, in_memory=False): |
| 950 | - super(Main, self).__init__() |
| 951 | - # Dynamically load the ui file generated by QtDesigner. |
| 952 | + self.store = store |
| 953 | + self._bg_color = WHITE |
| 954 | + self._font = font |
| 955 | + self.main_window = main_window |
| 956 | + |
| 957 | + def set_color(self, color): |
| 958 | + self._bg_color = color |
| 959 | + |
| 960 | + def setData(self, column, role, value): |
| 961 | + if role == QtCore.Qt.CheckStateRole: |
| 962 | + if value == QtCore.Qt.Checked: |
| 963 | + self.task.done = True |
| 964 | + else: |
| 965 | + self.task.done = False |
| 966 | + self.store.save_task(self.task) |
| 967 | + if role == QtCore.Qt.EditRole: |
| 968 | + text = unicode(value.toString(), 'utf-8') |
| 969 | + if not text: |
| 970 | + # There was no text in the edit field so do nothing. |
| 971 | + return |
| 972 | + self.update_task_text(text) |
| 973 | + super(UITask, self).setData(column, role, value) |
| 974 | + |
| 975 | + def data(self, column, role): |
| 976 | + if role == QtCore.Qt.EditRole: |
| 977 | + return self.task.title |
| 978 | + if role == QtCore.Qt.DisplayRole: |
| 979 | + return self.task.title |
| 980 | + if role == QtCore.Qt.FontRole: |
| 981 | + font = self._font |
| 982 | + font.setStrikeOut(self.task.done) |
| 983 | + return font |
| 984 | + if role == QtCore.Qt.BackgroundRole: |
| 985 | + return self._bg_color |
| 986 | + if role == QtCore.Qt.ForegroundRole: |
| 987 | + return DONE_COLOR if self.task.done else NOT_DONE_COLOR |
| 988 | + if role == QtCore.Qt.CheckStateRole: |
| 989 | + return QtCore.Qt.Checked if self.task.done else QtCore.Qt.Unchecked |
| 990 | + return super(UITask, self).data(column, role) |
| 991 | + |
| 992 | + def update_task_text(self, text): |
| 993 | + """Edit an existing todo item.""" |
| 994 | + # Change the task's title to the text in the edit field. |
| 995 | + self.task.title = text |
| 996 | + # Record the current tags. |
| 997 | + old_tags = set(self.task.tags) if self.task.tags else set([]) |
| 998 | + # Extract the new tags from the new text. |
| 999 | + new_tags = set(extract_tags(text)) |
| 1000 | + # Check if the tag filter buttons need updating. |
| 1001 | + self.main_window.update_tags(self, old_tags, new_tags) |
| 1002 | + # Set the tags on the task. |
| 1003 | + self.task.tags = list(new_tags) |
| 1004 | + # Save the changed task to the database. |
| 1005 | + self.store.save_task(self.task) |
| 1006 | + |
| 1007 | + |
| 1008 | +class TaskDelegate(QtGui.QStyledItemDelegate): |
| 1009 | + """Delegate for rendering tasks.""" |
| 1010 | + |
| 1011 | + pen = QtGui.QPen(QtGui.QColor(102, 102, 102), 2, style=QtCore.Qt.DotLine) |
| 1012 | + |
| 1013 | + def paint(self, painter, option, index): |
| 1014 | + # Save current state of painter before we modify anything. |
| 1015 | + painter.save() |
| 1016 | + painter.setPen(self.pen) |
| 1017 | + painter.drawRect(option.rect) |
| 1018 | + # Return painter to original stats. |
| 1019 | + painter.restore() |
| 1020 | + super(TaskDelegate, self).paint(painter, option, index) |
| 1021 | + |
| 1022 | + |
| 1023 | +class Sync(QtGui.QDialog): |
| 1024 | + |
| 1025 | + def __init__(self, other): |
| 1026 | + super(Sync, self).__init__() |
| 1027 | uifile = os.path.join( |
| 1028 | - os.path.abspath(os.path.dirname(__file__)), 'u1todo.ui') |
| 1029 | + os.path.abspath(os.path.dirname(__file__)), 'sync.ui') |
| 1030 | uic.loadUi(uifile, self) |
| 1031 | - # hook up the signals to the signal handlers. |
| 1032 | self.connect_events() |
| 1033 | - # Load the u1todo database. |
| 1034 | - db = get_database() |
| 1035 | - # And wrap it in a TodoStore object. |
| 1036 | - self.store = TodoStore(db) |
| 1037 | - # create or update the indexes if they are not up-to-date |
| 1038 | - self.store.initialize_db() |
| 1039 | - # Initially the delete button is disabled, because there are no tasks |
| 1040 | - # to delete. |
| 1041 | - self.delete_button.setEnabled(False) |
| 1042 | - # Initialize some variables we will use to keep track of the tags. |
| 1043 | - self._tag_docs = defaultdict(list) |
| 1044 | - self._tag_buttons = {} |
| 1045 | - self._tag_filter = [] |
| 1046 | - # Get all the tasks in the database, and add them to the UI. |
| 1047 | - for task in self.store.get_all_tasks(): |
| 1048 | - self.add_task(task) |
| 1049 | - self.task_edit.clear() |
| 1050 | - # Give the edit field focus. |
| 1051 | - self.task_edit.setFocus() |
| 1052 | - # Initialize the variable that points to the currently selected list |
| 1053 | - # item. |
| 1054 | - self.item = None |
| 1055 | + self.other = other |
| 1056 | |
| 1057 | def connect_events(self): |
| 1058 | """Hook up all the signal handlers.""" |
| 1059 | - # On enter, save the task that was being edited. |
| 1060 | - self.task_edit.returnPressed.connect(self.update) |
| 1061 | - # When the Edit/Add button is clicked, save the task that was being |
| 1062 | - # edited. |
| 1063 | - self.edit_button.clicked.connect(self.update) |
| 1064 | - # When the Delete button is clicked, delete the currently selected task |
| 1065 | - # (if any.) |
| 1066 | - self.delete_button.clicked.connect(self.delete) |
| 1067 | - # If a new row in the list is selected, change the currently selected |
| 1068 | - # task, and put its contents in the edit field. |
| 1069 | - self.todo_list.currentRowChanged.connect(self.row_changed) |
| 1070 | - # If the checked status of an item in the list changes, change the done |
| 1071 | - # status of the task. |
| 1072 | - self.todo_list.itemChanged.connect(self.item_changed) |
| 1073 | self.sync_button.clicked.connect(self.synchronize) |
| 1074 | |
| 1075 | - def refresh_filter(self): |
| 1076 | - """Remove all tasks, and show only those that satisfy the new filter. |
| 1077 | - |
| 1078 | - """ |
| 1079 | - # Remove everything from the list. |
| 1080 | - while len(self.todo_list): |
| 1081 | - self.todo_list.takeItem(0) |
| 1082 | - # Get the filtered tasks from the database. |
| 1083 | - for task in self.store.get_tasks_by_tags(self._tag_filter): |
| 1084 | - # Add them to the UI. |
| 1085 | - self.add_task(task) |
| 1086 | - # Clear the current selection. |
| 1087 | - self.todo_list.setCurrentRow(-1) |
| 1088 | - self.task_edit.clear() |
| 1089 | - self.item = None |
| 1090 | - |
| 1091 | - def item_changed(self, item): |
| 1092 | - """Mark a task as done or not done.""" |
| 1093 | - if item.checkState() == QtCore.Qt.Checked: |
| 1094 | - item.task.done = True |
| 1095 | - else: |
| 1096 | - item.task.done = False |
| 1097 | - # Save the task to the database. |
| 1098 | - item.update_strikethrough() |
| 1099 | - item.setText(item.task.title) |
| 1100 | - self.store.save_task(item.task) |
| 1101 | - # Clear the current selection. |
| 1102 | - self.todo_list.setCurrentRow(-1) |
| 1103 | - self.task_edit.clear() |
| 1104 | - self.item = None |
| 1105 | - |
| 1106 | - def update(self): |
| 1107 | - """Either add a new task or update an existing one.""" |
| 1108 | - text = unicode(self.task_edit.text(), 'utf-8') |
| 1109 | - if not text: |
| 1110 | - # There was no text in the edit field so do nothing. |
| 1111 | - return |
| 1112 | - if self.item is None: |
| 1113 | - # No task was selected, so add a new one. |
| 1114 | - task = self.store.new_task(text, tags=extract_tags(text)) |
| 1115 | - self.add_task(task) |
| 1116 | - else: |
| 1117 | - # A task was selected, so update it. |
| 1118 | - self.update_task_text(text) |
| 1119 | - # Clear the current selection. |
| 1120 | - self.todo_list.setCurrentRow(-1) |
| 1121 | - self.task_edit.clear() |
| 1122 | - self.item = None |
| 1123 | - |
| 1124 | def get_ubuntuone_credentials(self): |
| 1125 | cmt = CredentialsManagementTool() |
| 1126 | return cmt.find_credentials() |
| 1127 | @@ -167,7 +154,7 @@ |
| 1128 | def _synchronize(self, creds=None): |
| 1129 | if self.u1_radio.isChecked(): |
| 1130 | # TODO: not hardcode |
| 1131 | - target = 'https://u1db.one.ubuntu.com/~/u1todo' |
| 1132 | + target = 'https://u1db.one.ubuntu.com/~/cosas' |
| 1133 | else: |
| 1134 | target = self.url_edit.text() |
| 1135 | if target.startswith('http://') or target.startswith('https://'): |
| 1136 | @@ -192,11 +179,105 @@ |
| 1137 | db.set_oauth_credentials(**oauth_creds) |
| 1138 | db.open(create=True) |
| 1139 | syncer.sync() |
| 1140 | - self.refresh_filter() |
| 1141 | + self.other.refresh_filter() |
| 1142 | self.last_synced.setText( |
| 1143 | '<span style="color:green">%s</span>' % (datetime.now())) |
| 1144 | self.sync_button.setEnabled(True) |
| 1145 | |
| 1146 | + |
| 1147 | +class Main(QtGui.QMainWindow): |
| 1148 | + """Main window of our application.""" |
| 1149 | + |
| 1150 | + def __init__(self, in_memory=False): |
| 1151 | + super(Main, self).__init__() |
| 1152 | + # Dynamically load the ui file generated by QtDesigner. |
| 1153 | + uifile = os.path.join( |
| 1154 | + os.path.abspath(os.path.dirname(__file__)), 'cosas.ui') |
| 1155 | + uic.loadUi(uifile, self) |
| 1156 | + # set the model for the treeview |
| 1157 | + self.buttons_frame.hide() |
| 1158 | + # hook up the signals to the signal handlers. |
| 1159 | + self.connect_events() |
| 1160 | + # Load the cosas database. |
| 1161 | + db = get_database() |
| 1162 | + # And wrap it in a TodoStore object. |
| 1163 | + self.store = TodoStore(db) |
| 1164 | + # create or update the indexes if they are not up-to-date |
| 1165 | + self.store.initialize_db() |
| 1166 | + # hook up the delegate |
| 1167 | + self.todo_list.setItemDelegate(TaskDelegate()) |
| 1168 | + # Initialize some variables we will use to keep track of the tags. |
| 1169 | + self._tag_docs = defaultdict(list) |
| 1170 | + self._tag_buttons = {} |
| 1171 | + self._tag_filter = [] |
| 1172 | + self._tag_colors = {} |
| 1173 | + # A list of colors to give differently tagged items different colors. |
| 1174 | + self.colors = TAG_COLORS[:] |
| 1175 | + # Get all the tasks in the database, and add them to the UI. |
| 1176 | + for task in self.store.get_all_tasks(): |
| 1177 | + self.add_task(task) |
| 1178 | + self.title_edit.clear() |
| 1179 | + # Give the edit field focus. |
| 1180 | + self.title_edit.setFocus() |
| 1181 | + |
| 1182 | + def get_tag_color(self): |
| 1183 | + """Get a color number to use for a new tag.""" |
| 1184 | + # Remove a color from the list of available ones and return it. |
| 1185 | + if not self.colors: |
| 1186 | + return WHITE |
| 1187 | + return self.colors.pop(0) |
| 1188 | + |
| 1189 | + def connect_events(self): |
| 1190 | + """Hook up all the signal handlers.""" |
| 1191 | + # On enter, save the task that was being edited. |
| 1192 | + self.title_edit.returnPressed.connect(self.update) |
| 1193 | + self.action_synchronize.triggered.connect(self.open_sync_window) |
| 1194 | + self.buttons_toggle.clicked.connect(self.show_buttons) |
| 1195 | + |
| 1196 | + def open_sync_window(self): |
| 1197 | + window = Sync(self) |
| 1198 | + window.exec_() |
| 1199 | + |
| 1200 | + def show_buttons(self): |
| 1201 | + """Show the frame with the tag buttons.""" |
| 1202 | + self.buttons_toggle.clicked.disconnect(self.show_buttons) |
| 1203 | + self.buttons_frame.show() |
| 1204 | + self.buttons_toggle.clicked.connect(self.hide_buttons) |
| 1205 | + |
| 1206 | + def hide_buttons(self): |
| 1207 | + """Show the frame with the tag buttons.""" |
| 1208 | + self.buttons_toggle.clicked.disconnect(self.hide_buttons) |
| 1209 | + self.buttons_frame.hide() |
| 1210 | + self.buttons_toggle.clicked.connect(self.show_buttons) |
| 1211 | + |
| 1212 | + def refresh_filter(self): |
| 1213 | + """Remove all tasks, and show only those that satisfy the new filter. |
| 1214 | + |
| 1215 | + """ |
| 1216 | + # Remove everything from the list. |
| 1217 | + while self.todo_list.topLevelItemCount(): |
| 1218 | + self.todo_list.takeTopLevelItem(0) |
| 1219 | + # Get the filtered tasks from the database. |
| 1220 | + for task in self.store.get_tasks_by_tags(self._tag_filter): |
| 1221 | + # Add them to the UI. |
| 1222 | + self.add_task(task) |
| 1223 | + # Clear the current selection. |
| 1224 | + self.todo_list.setCurrentItem(None) |
| 1225 | + self.title_edit.clear() |
| 1226 | + self.item = None |
| 1227 | + |
| 1228 | + def update(self): |
| 1229 | + """Either add a new task or update an existing one.""" |
| 1230 | + text = unicode(self.title_edit.text(), 'utf-8') |
| 1231 | + if not text: |
| 1232 | + # There was no text in the edit field so do nothing. |
| 1233 | + return |
| 1234 | + # No task was selected, so add a new one. |
| 1235 | + task = self.store.new_task(text, tags=extract_tags(text)) |
| 1236 | + self.add_task(task) |
| 1237 | + # Clear the current selection. |
| 1238 | + self.title_edit.clear() |
| 1239 | + |
| 1240 | def delete(self): |
| 1241 | """Delete a todo item.""" |
| 1242 | # Delete the item from the database. |
| 1243 | @@ -207,41 +288,47 @@ |
| 1244 | self.store.delete_task(item.task) |
| 1245 | # Clear the current selection. |
| 1246 | self.todo_list.setCurrentRow(-1) |
| 1247 | - self.task_edit.clear() |
| 1248 | + self.title_edit.clear() |
| 1249 | self.item = None |
| 1250 | - if self.todo_list.count() == 0: |
| 1251 | - # If there are no tasks left, disable the delete button. |
| 1252 | - self.delete_button.setEnabled(False) |
| 1253 | |
| 1254 | def add_task(self, task): |
| 1255 | """Add a new todo item.""" |
| 1256 | # Wrap the task in a UITask object. |
| 1257 | - item = UITask(task) |
| 1258 | - self.todo_list.addItem(item) |
| 1259 | - # We know there is at least one item now so we enable the delete |
| 1260 | - # button. |
| 1261 | - self.delete_button.setEnabled(True) |
| 1262 | + item = UITask( |
| 1263 | + task, self.todo_list, self.store, self.todo_list.font(), self) |
| 1264 | + item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) |
| 1265 | + self.todo_list.addTopLevelItem(item) |
| 1266 | if not task.tags: |
| 1267 | return |
| 1268 | # If the task has tags, we add them as filter buttons to the UI, if |
| 1269 | # they are new. |
| 1270 | for tag in task.tags: |
| 1271 | - self.add_tag(task.task_id, tag) |
| 1272 | + self.add_tag(task.doc_id, tag) |
| 1273 | + if task.tags: |
| 1274 | + item.set_color(self._tag_colors[task.tags[0]]['qcolor']) |
| 1275 | + else: |
| 1276 | + item.set_color(WHITE) |
| 1277 | |
| 1278 | - def add_tag(self, task_id, tag): |
| 1279 | - """Create a link between the task with id task_id and the tag, and |
| 1280 | + def add_tag(self, doc_id, tag): |
| 1281 | + """Create a link between the task with id doc_id and the tag, and |
| 1282 | add a new button for tag if it was not already there. |
| 1283 | |
| 1284 | """ |
| 1285 | # Add the task id to the list of document ids associated with this tag. |
| 1286 | - self._tag_docs[tag].append(task_id) |
| 1287 | + self._tag_docs[tag].append(doc_id) |
| 1288 | # If the list has more than one element the tag button was already |
| 1289 | # present. |
| 1290 | if len(self._tag_docs[tag]) > 1: |
| 1291 | return |
| 1292 | # Add a tag filter button for this tag to the UI. |
| 1293 | button = QtGui.QPushButton(tag) |
| 1294 | - button._u1todo_tag = tag |
| 1295 | + color = self.get_tag_color() |
| 1296 | + qcolor = QtGui.QColor(*color) |
| 1297 | + self._tag_colors[tag] = { |
| 1298 | + 'color_tuple': color, |
| 1299 | + 'qcolor': qcolor} |
| 1300 | + button.setStyleSheet('background-color: rgb(%d, %d, %d)' % color) |
| 1301 | + button._todo_tag = tag |
| 1302 | # Make the button an on/off button. |
| 1303 | button.setCheckable(True) |
| 1304 | # Store a reference to the button in a dictionary so we can find it |
| 1305 | @@ -254,10 +341,10 @@ |
| 1306 | """Toggle the filter for the tag associated with this button.""" |
| 1307 | if checked: |
| 1308 | # Add the tag to the current filter. |
| 1309 | - self._tag_filter.append(button._u1todo_tag) |
| 1310 | + self._tag_filter.append(button._todo_tag) |
| 1311 | else: |
| 1312 | # Remove the tag from the current filter. |
| 1313 | - self._tag_filter.remove(button._u1todo_tag) |
| 1314 | + self._tag_filter.remove(button._todo_tag) |
| 1315 | # Apply the new filter. |
| 1316 | self.refresh_filter() |
| 1317 | |
| 1318 | @@ -267,17 +354,17 @@ |
| 1319 | # sorted alphabetically by the text of the tag. |
| 1320 | index = sorted(self._tag_buttons.keys()).index(tag) |
| 1321 | # And add the button to the UI. |
| 1322 | - self.tag_buttons.insertWidget(index, button) |
| 1323 | + self.buttons_layout.insertWidget(index, button) |
| 1324 | |
| 1325 | - def remove_tag(self, task_id, tag): |
| 1326 | - """Remove the link between the task with id task_id and the tag, and |
| 1327 | + def remove_tag(self, doc_id, tag): |
| 1328 | + """Remove the link between the task with id doc_id and the tag, and |
| 1329 | remove the button for tag if it no longer has any tasks associated with |
| 1330 | it. |
| 1331 | |
| 1332 | """ |
| 1333 | # Remove the task id from the list of document ids associated with this |
| 1334 | # tag. |
| 1335 | - self._tag_docs[tag].remove(task_id) |
| 1336 | + self._tag_docs[tag].remove(doc_id) |
| 1337 | # If the list is not empty, we do not remove the button, because there |
| 1338 | # are still tasks that have this tag. |
| 1339 | if self._tag_docs[tag]: |
| 1340 | @@ -285,56 +372,28 @@ |
| 1341 | # Look up the button. |
| 1342 | button = self._tag_buttons[tag] |
| 1343 | # Remove it from the ui. |
| 1344 | - self.tag_buttons.removeWidget(button) |
| 1345 | + button.hide() |
| 1346 | + self.buttons_layout.removeWidget(button) |
| 1347 | # And remove the reference. |
| 1348 | del self._tag_buttons[tag] |
| 1349 | |
| 1350 | - def update_tags(self, task_id, old_tags, new_tags): |
| 1351 | - """Process any changed tags for this task_id.""" |
| 1352 | + def update_tags(self, item, old_tags, new_tags): |
| 1353 | + """Process any changed tags for this item.""" |
| 1354 | # Process all removed tags. |
| 1355 | for tag in old_tags - new_tags: |
| 1356 | - self.remove_tag(task_id, tag) |
| 1357 | + self.remove_tag(item.task.doc_id, tag) |
| 1358 | # Process all tags newly added. |
| 1359 | for tag in new_tags - old_tags: |
| 1360 | - self.add_tag(task_id, tag) |
| 1361 | - |
| 1362 | - def update_task_text(self, text): |
| 1363 | - """Edit an existing todo item.""" |
| 1364 | - item = self.item |
| 1365 | - task = item.task |
| 1366 | - # Change the task's title to the text in the edit field. |
| 1367 | - task.title = text |
| 1368 | - # Record the current tags. |
| 1369 | - old_tags = set(task.tags) if task.tags else set([]) |
| 1370 | - # Extract the new tags from the new text. |
| 1371 | - new_tags = set(extract_tags(text)) |
| 1372 | - # Check if the tag filter buttons need updating. |
| 1373 | - self.update_tags(item.task.task_id, old_tags, new_tags) |
| 1374 | - # Set the tags on the task. |
| 1375 | - task.tags = list(new_tags) |
| 1376 | - # disconnect the signal temporarily while we change the title |
| 1377 | - self.todo_list.itemChanged.disconnect(self.item_changed) |
| 1378 | - # Change the text in the UI. |
| 1379 | - item.setText(text) |
| 1380 | - # reconnect the signal after we changed the title |
| 1381 | - self.todo_list.itemChanged.connect(self.item_changed) |
| 1382 | - # Save the changed task to the database. |
| 1383 | - self.store.save_task(task) |
| 1384 | - |
| 1385 | - def row_changed(self, index): |
| 1386 | - """Edit item when row changes.""" |
| 1387 | - if index == -1: |
| 1388 | - self.task_edit.clear() |
| 1389 | + self.add_tag(item.task.doc_id, tag) |
| 1390 | + if new_tags: |
| 1391 | + item.set_color(self._tag_colors[list(new_tags)[0]]['qcolor']) |
| 1392 | return |
| 1393 | - # If a row is selected, show the selected task's title in the edit |
| 1394 | - # field. |
| 1395 | - self.item = self.todo_list.item(index) |
| 1396 | - self.task_edit.setText(self.item.task.title) |
| 1397 | + item.set_color(WHITE) |
| 1398 | |
| 1399 | |
| 1400 | if __name__ == "__main__": |
| 1401 | - # Unfortunately, to be able to use ubuntuone.platform.credentials on linux, |
| 1402 | - # we now depend on dbus. :( |
| 1403 | + # TODO: Unfortunately, to be able to use ubuntuone.platform.credentials on |
| 1404 | + # linux, we now depend on dbus. :( |
| 1405 | from dbus.mainloop.qt import DBusQtMainLoop |
| 1406 | main_loop = DBusQtMainLoop(set_as_default=True) |
| 1407 | app = QtGui.QApplication(sys.argv) |

Nice!