Merge lp:~vds/desktopcouch/remove_delete_flag into lp:desktopcouch

Proposed by Vincenzo Di Somma
Status: Merged
Approved by: John Lenton
Approved revision: 198
Merged at revision: 197
Proposed branch: lp:~vds/desktopcouch/remove_delete_flag
Merge into: lp:desktopcouch
Diff against target: 316 lines (+81/-49)
3 files modified
desktopcouch/records/server.py (+41/-4)
desktopcouch/records/server_base.py (+12/-13)
desktopcouch/records/tests/test_server.py (+28/-32)
To merge this branch: bzr merge lp:~vds/desktopcouch/remove_delete_flag
Reviewer Review Type Date Requested Status
John Lenton (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+40038@code.launchpad.net

Commit message

Instead of deleting records we are marking as deleted in private application annotation, but we don't want to maintain "deleted" records in the database for ever, it's bad for couchdb both for views performance and coding the views.
What we want to do is move the records to a "trash" database that can be replicated and can offer undo functionalities.

Description of the change

Instead of deleting records we are marking as deleted in private application annotation, but we don't want to maintain "deleted" records in the database for ever, it's bad for couchdb both for views performance and coding the views.
What we want to do is move the records to a "trash" database that can be replicated and can offer undo functionalities.

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

I LOVE it! Also, the tests pass... +1000

review: Approve
Revision history for this message
John Lenton (chipaca) wrote :

A migration path would be nice.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'desktopcouch/records/server.py'
2--- desktopcouch/records/server.py 2010-10-31 21:37:35 +0000
3+++ desktopcouch/records/server.py 2010-11-04 00:03:52 +0000
4@@ -20,7 +20,9 @@
5 # Chad Miller <chad.miller@canonical.com>
6
7 """The Desktop Couch Records API."""
8-
9+
10+import copy, uuid
11+
12 from couchdb import Server
13 from couchdb.client import Resource
14 import desktopcouch
15@@ -28,7 +30,12 @@
16 from desktopcouch.platform import find_port
17 import urlparse
18
19+DCTRASH = 'dctrash'
20+
21 class OAuthCapableServer(Server):
22+ """Subclass Server to provide oauth magic"""
23+ # pylint: disable-msg=W0231
24+ # __init__ method from base class is not called
25 def __init__(self, uri, oauth_tokens=None, ctx=None):
26 """Subclass of couchdb.client.Server which creates a custom
27 httplib2.Http subclass which understands OAuth"""
28@@ -41,9 +48,12 @@
29 (consumer_key, consumer_secret, token, token_secret) = (
30 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"],
31 oauth_tokens["token"], oauth_tokens["token_secret"])
32- http.add_oauth_tokens(consumer_key, consumer_secret, token, token_secret)
33+ http.add_oauth_tokens(
34+ consumer_key, consumer_secret, token, token_secret)
35 self.resource = Resource(http, uri)
36-
37+ # pylint: enable-msg=W0231
38+
39+
40 class CouchDatabase(server_base.CouchDatabaseBase):
41 """An small records specific abstraction over a couch db database."""
42
43@@ -56,6 +66,33 @@
44 database, uri, record_factory=record_factory, create=create,
45 server_class=server_class, oauth_tokens=oauth_tokens, ctx=ctx)
46
47+ # pylint: disable-msg=W0212
48+ #Access to a protected member
49+ def delete_record(self, record_id):
50+ """Delete record with given id"""
51+ record = self.get_record(record_id)
52+ new_record = copy.deepcopy(record)
53+ dctrash = CouchDatabase(
54+ database=DCTRASH,
55+ uri=self.server_uri,
56+ create=True,
57+ **self._server_class_extras)
58+ new_record.record_id = str(uuid.uuid4())
59+ del new_record._data['_rev']
60+ try:
61+ del new_record._data['_attachments']
62+ except KeyError:
63+ pass
64+ new_record.application_annotations['desktopcouch'] = {
65+ 'private_application_annotations':
66+ {'original_database_name': self._database_name,
67+ 'original_id': record_id}}
68+ del self.db[record_id]
69+ return dctrash.put_record(new_record)
70+ # pylint: enable-msg=W0212
71+
72+ # pylint: disable-msg=W0221
73+ # Arguments number differs from overridden method
74 def _reconnect(self):
75 if not self.server_uri:
76 port = find_port(ctx=self.ctx)
77@@ -63,4 +100,4 @@
78 else:
79 uri = self.server_uri
80 super(CouchDatabase, self)._reconnect(uri=uri)
81-
82+ # pylint: enable-msg=W0221
83
84=== modified file 'desktopcouch/records/server_base.py'
85--- desktopcouch/records/server_base.py 2010-11-02 21:20:35 +0000
86+++ desktopcouch/records/server_base.py 2010-11-04 00:03:52 +0000
87@@ -1,4 +1,4 @@
88-# Copyright 2009 Canonical Ltd.
89+# Copyright 2009-2010 Canonical Ltd.
90 #
91 # This file is part of desktopcouch.
92 #
93@@ -18,6 +18,7 @@
94 # Mark G. Saye <mark.saye@canonical.com>
95 # Stuart Langridge <stuart.langridge@canonical.com>
96 # Chad Miller <chad.miller@canonical.com>
97+# Vincenzo Di Somma <vincenzo.di.somma@canonical.com>
98
99 """The Desktop Couch Records API."""
100
101@@ -44,10 +45,8 @@
102 from record import Record
103 import logging
104
105-#DEFAULT_DESIGN_DOCUMENT = "design"
106 DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
107
108-
109 def get_changes(self, changes_since):
110 """This method is used to monkey patch the database to provide a
111 get_changes method"""
112@@ -161,12 +160,12 @@
113
114 def __init__(self, database, uri, record_factory=None, create=False,
115 server_class=Server, **server_class_extras):
116+ self.server_uri = uri
117 self._database_name = database
118+ self.record_factory = record_factory or Record
119 self._create = create
120 self._server_class = server_class
121 self._server_class_extras = server_class_extras
122- self.record_factory = record_factory or Record
123- self.server_uri = uri
124 self._server = None
125 self.db = None
126 self._reconnect()
127@@ -378,6 +377,14 @@
128 # take a well deserved break.
129 break
130
131+ def record_exists(self, record_id):
132+ """Check if record with given id exists."""
133+ try:
134+ record = self.db[record_id]
135+ except ResourceNotFound:
136+ return False
137+ return not row_is_deleted(record)
138+
139 def delete_record(self, record_id):
140 """Delete record with given id"""
141 record = self.db[record_id]
142@@ -386,14 +393,6 @@
143 'deleted'] = True
144 self.db[record_id] = record
145
146- def record_exists(self, record_id):
147- """Check if record with given id exists."""
148- try:
149- record = self.db[record_id]
150- except ResourceNotFound:
151- return False
152- return not row_is_deleted(record)
153-
154 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
155 """Remove a view, given its name. Raises a KeyError on a unknown
156 view. Returns a dict of functions the deleted view defined."""
157
158=== modified file 'desktopcouch/records/tests/test_server.py'
159--- desktopcouch/records/tests/test_server.py 2010-11-03 02:47:32 +0000
160+++ desktopcouch/records/tests/test_server.py 2010-11-04 00:03:52 +0000
161@@ -1,4 +1,4 @@
162-# Copyright 2009 Canonical Ltd.
163+# Copyright 2009-2010 Canonical Ltd.
164 #
165 # This file is part of desktopcouch.
166 #
167@@ -16,6 +16,7 @@
168 #
169 # Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
170 # Chad Miller <chad.miller@canonical.com>
171+# Vincenzo Di Somma <vincenzo.di.somma@canonical.com>
172
173 """testing database/contact.py module"""
174 import testtools
175@@ -38,6 +39,8 @@
176 from cStringIO import StringIO as StringIO
177 # pylint: enable-msg=F0401
178
179+DCTRASH = 'dctrash'
180+
181 FAKE_RECORD_TYPE = "http://example.org/test"
182
183 js = """
184@@ -48,8 +51,11 @@
185 }""" % FAKE_RECORD_TYPE
186
187
188+def get_test_context():
189+ return test_environment.test_context
190+
191 class TestCouchDatabaseDeprecated(testtools.TestCase):
192- """tests specific for CouchDatabase"""
193+ """Test specific for CouchDatabase"""
194
195 def setUp(self):
196 """setup each test"""
197@@ -57,7 +63,7 @@
198 # Connect to CouchDB server
199 self.dbname = self._testMethodName
200 self.database = CouchDatabase(self.dbname, create=True,
201- ctx=self.get_test_context())
202+ ctx=get_test_context())
203 #create some records to pull out and test
204 self.database.put_record(Record({
205 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
206@@ -72,21 +78,18 @@
207 def tearDown(self):
208 """tear down each test"""
209 del self.database._server[self.database._database_name]
210- this_context = self.get_test_context()
211+ this_context = get_test_context()
212 if this_context != test_environment.test_context:
213 stop_couchdb(ctx=this_context)
214 super(TestCouchDatabaseDeprecated, self).tearDown()
215
216- def get_test_context(self):
217- return test_environment.test_context
218-
219 def maybe_die(self):
220 pass
221
222 def wait_until_server_dead(self, pid=None):
223 if pid is not None:
224 pid = find_pid(
225- start_if_not_running=False, ctx=self.get_test_context())
226+ start_if_not_running=False, ctx=get_test_context())
227 if pid is None:
228 return
229 while True:
230@@ -163,7 +166,7 @@
231 # Connect to CouchDB server
232 self.dbname = self._testMethodName
233 self.database = CouchDatabase(self.dbname, create=True,
234- ctx=self.get_test_context())
235+ ctx=get_test_context())
236 #create some records to pull out and test
237 self.database.put_record(Record({
238 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
239@@ -178,21 +181,18 @@
240 def tearDown(self):
241 """tear down each test"""
242 del self.database._server[self.database._database_name]
243- this_context = self.get_test_context()
244+ this_context = get_test_context()
245 if this_context != test_environment.test_context:
246 stop_couchdb(ctx=this_context)
247 super(TestCouchDatabase, self).tearDown()
248
249- def get_test_context(self):
250- return test_environment.test_context
251-
252 def maybe_die(self):
253 pass
254
255 def wait_until_server_dead(self, pid=None):
256 if pid is not None:
257 pid = find_pid(
258- start_if_not_running=False, ctx=self.get_test_context())
259+ start_if_not_running=False, ctx=get_test_context())
260 if pid is None:
261 return
262 while True:
263@@ -221,23 +221,13 @@
264 self.assertEqual(0, retrieved_record['record_number'])
265
266 def test_get_deleted_record(self):
267- """Test getting a record."""
268+ """Test getting a deleted record."""
269 record = Record({'record_number': 0}, record_type="http://example.com/")
270 record_id = self.database.put_record(record)
271 self.database.delete_record(record_id)
272 retrieved_record = self.database.get_record(record_id)
273- #FIXME the record has to be None
274 self.assertEqual(retrieved_record, None)
275
276- def test_get_deleted_record_not_hidden(self):
277- """Test getting a record."""
278- record = Record({'record_number': 0}, record_type="http://example.com/")
279- record_id = self.database.put_record(record)
280- self.database.delete_record(record_id)
281- retrieved_record = self.database.get_record(record_id=record_id,
282- hide_deleted=False)
283- self.assertEqual(0, retrieved_record['record_number'])
284-
285 def test_put_record(self):
286 """Test putting a record."""
287 record = Record({'record_number': 0}, record_type="http://example.com/")
288@@ -269,15 +259,21 @@
289 self.assertFalse(self.database._server[self.dbname][docid])
290
291 def test_delete_record(self):
292- """Test deletion of records."""
293+ """Test getting a record."""
294 record = Record({'record_number': 0}, record_type="http://example.com/")
295 record_id = self.database.put_record(record)
296- self.database.delete_record(record_id)
297- self.maybe_die() # should be able to survive couchdb death
298- deleted_record = self.database.get_record(record_id=record_id,
299- hide_deleted=False)
300- self.assert_(deleted_record.application_annotations['Ubuntu One'][
301- 'private_application_annotations']['deleted'])
302+ trash_record_id = self.database.delete_record(record_id)
303+ self.assertIn(DCTRASH, self.database._server)
304+ dctrash = CouchDatabase(DCTRASH, create=False,
305+ ctx=get_test_context())
306+ deleted_record = dctrash.get_record(trash_record_id)
307+ self.assertNotEqual(record_id, deleted_record.record_id)
308+ ann = deleted_record.application_annotations['desktopcouch']
309+ priv_ann = ann['private_application_annotations']
310+ original_database_name = priv_ann['original_database_name']
311+ original_id = priv_ann['original_id']
312+ self.assertEqual(record_id, original_id)
313+ self.assertEqual(self.dbname, original_database_name)
314
315 def test_delete_and_recreate_record(self):
316 """Test deletion of records."""

Subscribers

People subscribed via source and target branches