Merge lp:~cjcurran/friends/another-test-fix-contacts-fb-import into lp:friends
- another-test-fix-contacts-fb-import
- Merge into trunk
Proposed by
Conor Curran
Status: | Merged |
---|---|
Merged at revision: | 49 |
Proposed branch: | lp:~cjcurran/friends/another-test-fix-contacts-fb-import |
Merge into: | lp:friends |
Diff against target: |
376 lines (+138/-69) 5 files modified
friends/errors.py (+1/-2) friends/protocols/facebook.py (+64/-22) friends/testing/mocks.py (+5/-4) friends/tests/test_facebook.py (+26/-10) friends/utils/base.py (+42/-31) |
To merge this branch: | bzr merge lp:~cjcurran/friends/another-test-fix-contacts-fb-import |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Bruce Park | Approve | ||
Review via email: mp+132327@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 50. By Conor Curran
-
rebase to pick up facebook id
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'friends/errors.py' |
2 | --- friends/errors.py 2012-10-13 01:27:15 +0000 |
3 | +++ friends/errors.py 2012-10-31 14:00:33 +0000 |
4 | @@ -20,8 +20,7 @@ |
5 | 'FriendsError', |
6 | 'UnsupportedProtocolError', |
7 | ] |
8 | - |
9 | - |
10 | + |
11 | class FriendsError(Exception): |
12 | """Base class for all internal Friends exceptions.""" |
13 | |
14 | |
15 | === modified file 'friends/protocols/facebook.py' |
16 | --- friends/protocols/facebook.py 2012-10-31 11:38:22 +0000 |
17 | +++ friends/protocols/facebook.py 2012-10-31 14:00:33 +0000 |
18 | @@ -15,6 +15,7 @@ |
19 | |
20 | """The Facebook protocol plugin.""" |
21 | |
22 | + |
23 | __all__ = [ |
24 | 'Facebook', |
25 | ] |
26 | @@ -23,7 +24,6 @@ |
27 | import logging |
28 | |
29 | from datetime import datetime, timedelta |
30 | - |
31 | from gi.repository import EBook |
32 | |
33 | from friends.utils.avatar import Avatar |
34 | @@ -257,48 +257,90 @@ |
35 | else: |
36 | self._unpublish(obj_id) |
37 | |
38 | - def fetch_contacts(self): |
39 | - """Retrieve a list of up to 100 Facebook friends.""" |
40 | - limit = self._DOWNLOAD_LIMIT * 2 |
41 | + def _fetch_contacts(self): |
42 | + """Retrieve a list of up to 1,000 Facebook friends.""" |
43 | + limit = 1000 |
44 | access_token = self._get_access_token() |
45 | contacts = [] |
46 | url = ME_URL + '/friends' |
47 | params = dict( |
48 | access_token=access_token, |
49 | limit=limit) |
50 | + return self._follow_pagination(url, params, limit=limit) |
51 | |
52 | - return self._follow_pagination(url, params, limit) |
53 | + def _fetch_contact(self, contact_id): |
54 | + """Fetch the full, individual contact info.""" |
55 | + access_token = self._get_access_token() |
56 | + url = API_BASE.format(id=contact_id) |
57 | + params = dict(access_token=access_token) |
58 | + return get_json(url, params) |
59 | |
60 | # This method can take the minimal contact information or full |
61 | # contact info For now we only cache ID and the name in the |
62 | # addressbook. Using custom field for name because I can't figure |
63 | # out how econtact name works. |
64 | - def create_contact(self, contact_json): |
65 | + def _create_contact(self, contact_json): |
66 | + vcard = EBook.VCard.new() |
67 | + |
68 | vcafid = EBook.VCardAttribute.new( |
69 | 'social-networking-attributes', 'facebook-id') |
70 | vcafid.add_value(contact_json['id']) |
71 | vcafn = EBook.VCardAttribute.new( |
72 | 'social-networking-attributes', 'facebook-name') |
73 | vcafn.add_value(contact_json['name']) |
74 | - vcard = EBook.VCard.new() |
75 | + vcauri = EBook.VCardAttribute.new( |
76 | + 'social-networking-attributes', 'X-URIS') |
77 | + vcauri.add_value(contact_json['link']) |
78 | + |
79 | + vcaws = EBook.VCardAttribute.new( |
80 | + 'social-networking-attributes', 'X-FOLKS-WEB-SERVICES-IDS') |
81 | + vcaws_param = EBook.VCardAttributeParam.new('jabber') |
82 | + vcaws_param.add_value('-{}@chat.facebook.com'.format(contact_json['id'])) |
83 | + vcaws.add_param(vcaws_param) |
84 | + vcard.add_attribute(vcaws) |
85 | + |
86 | + if 'gender' in contact_json.keys(): |
87 | + vcag = EBook.VCardAttribute.new( |
88 | + 'social-networking-attributes', 'X-GENDER') |
89 | + vcag.add_value(contact_json['gender']) |
90 | + vcard.add_attribute(vcag) |
91 | + |
92 | + vcard.add_attribute(vcafn) |
93 | + vcard.add_attribute(vcauri) |
94 | vcard.add_attribute(vcafid) |
95 | - vcard.add_attribute(vcafn) |
96 | - c = EBook.Contact.new_from_vcard(vcard.to_string(EBook.VCardFormat(1))) |
97 | - log.debug('Creating new contact for {}'.format(contact_json['name'])) |
98 | - return c |
99 | - |
100 | + |
101 | + contact = EBook.Contact.new_from_vcard( |
102 | + vcard.to_string(EBook.VCardFormat(1))) |
103 | + contact.set_property('full-name', contact_json['name']) |
104 | + if 'username' in contact_json.keys(): |
105 | + contact.set_property('nickname', contact_json['username']) |
106 | + |
107 | + log.debug( |
108 | + 'Creating new contact for {}'.format( |
109 | + contact.get_property('full-name'))) |
110 | + return contact |
111 | + |
112 | + @feature |
113 | def contacts(self): |
114 | - contacts = self.fetch_contacts() |
115 | + contacts = self._fetch_contacts() |
116 | + log.debug('Size of the contacts returned {}'.format(len(contacts))) |
117 | source = self._get_eds_source(FACEBOOK_ADDRESS_BOOK) |
118 | + if source is None: |
119 | + source = self._create_eds_source(FACEBOOK_ADDRESS_BOOK) |
120 | for contact in contacts: |
121 | - if source is not None: |
122 | - if (Base.previously_stored_contact( |
123 | - source, 'facebook-id', contact['id'])): |
124 | - continue |
125 | - # Let's not query the full contact info for now Show some |
126 | - # respect for facebook and the chances are we won't be |
127 | - # blocked. |
128 | - eds_contact = self.create_contact(contact) |
129 | + if self._previously_stored_contact( |
130 | + source, 'facebook-id', contact['id']): |
131 | + continue |
132 | + log.debug( |
133 | + 'Fetch full contact info for {} and id {}'.format( |
134 | + contact['name'], contact['id'])) |
135 | + full_contact = self._fetch_contact(contact['id']) |
136 | + eds_contact = self._create_contact(full_contact) |
137 | if not self._push_to_eds(FACEBOOK_ADDRESS_BOOK, eds_contact): |
138 | log.error( |
139 | - 'Unable to save facebook contact {}'.format(contact['name'])) |
140 | + 'Unable to save facebook contact {}'.format( |
141 | + contact['name'])) |
142 | + |
143 | + def delete_contacts(self): |
144 | + source = self._get_eds_source(FACEBOOK_ADDRESS_BOOK) |
145 | + return self._delete_service_contacts(source) |
146 | |
147 | === modified file 'friends/testing/mocks.py' |
148 | --- friends/testing/mocks.py 2012-10-27 00:55:03 +0000 |
149 | +++ friends/testing/mocks.py 2012-10-31 14:00:33 +0000 |
150 | @@ -263,10 +263,11 @@ |
151 | return True |
152 | |
153 | def get_contacts_sync(val1, val2, val3): |
154 | - if val1: |
155 | - return [True, [{'name':'john doe', 'id': 11111}]] |
156 | - else: |
157 | - return [True, []] |
158 | + return [True, [{'name':'john doe', 'id': 11111}]] |
159 | + |
160 | + def remove_contact_sync(val1, val2): |
161 | + pass |
162 | + |
163 | |
164 | class EDSExtension: |
165 | """A Extension mocker object for testing create source.""" |
166 | |
167 | === modified file 'friends/tests/test_facebook.py' |
168 | --- friends/tests/test_facebook.py 2012-10-31 11:38:22 +0000 |
169 | +++ friends/tests/test_facebook.py 2012-10-31 14:00:33 +0000 |
170 | @@ -368,27 +368,38 @@ |
171 | return_value=True) |
172 | def test_fetch_contacts(self, *mocks): |
173 | # Receive the users friends. |
174 | - results = self.protocol.fetch_contacts() |
175 | + results = self.protocol._fetch_contacts() |
176 | self.assertEqual(len(results), 8) |
177 | self.assertEqual(results[7]['name'], 'John Smith') |
178 | self.assertEqual(results[7]['id'], '444444') |
179 | |
180 | def test_create_contact(self, *mocks): |
181 | # Receive the users friends. |
182 | - bare_contact = {'name': 'Lucy Baron', 'id': '555555555'} |
183 | - eds_contact = self.protocol.create_contact(bare_contact) |
184 | + bare_contact = {'name': 'Lucy Baron', |
185 | + 'id': '555555555', |
186 | + 'username': "lucy.baron5", |
187 | + 'link': 'http:www.facebook.com/lucy.baron5'} |
188 | + eds_contact = self.protocol._create_contact(bare_contact) |
189 | facebook_id_attr = eds_contact.get_attribute('facebook-id') |
190 | self.assertEqual(facebook_id_attr.get_value(), '555555555') |
191 | facebook_name_attr = eds_contact.get_attribute('facebook-name') |
192 | self.assertEqual(facebook_name_attr.get_value(), 'Lucy Baron') |
193 | + web_service_addrs = eds_contact.get_attribute('X-FOLKS-WEB-SERVICES-IDS') |
194 | + self.assertEqual(len(web_service_addrs.get_params()), 1) |
195 | + self.assertEqual(web_service_addrs.get_params()[0].get_name(), "jabber") |
196 | + self.assertEqual(len(web_service_addrs.get_params()[0].get_values()), 1) |
197 | + self.assertEqual(web_service_addrs.get_params()[0].get_values()[0], "-555555555@chat.facebook.com") |
198 | |
199 | @mock.patch('friends.utils.base.Base._get_eds_source', |
200 | return_value=True) |
201 | @mock.patch('gi.repository.EBook.BookClient.new', |
202 | return_value=EDSBookClientMock()) |
203 | def test_successfull_push_to_eds(self, *mocks): |
204 | - bare_contact = {'name': 'Lucy Baron', 'id': '555555555'} |
205 | - eds_contact = self.protocol.create_contact(bare_contact) |
206 | + bare_contact = {'name': 'Lucy Baron', |
207 | + 'id': '555555555', |
208 | + 'username': "lucy.baron5", |
209 | + 'link': 'http:www.facebook.com/lucy.baron5'} |
210 | + eds_contact = self.protocol._create_contact(bare_contact) |
211 | result = self.protocol._push_to_eds('test-address-book', eds_contact) |
212 | self.assertEqual(result, True) |
213 | |
214 | @@ -397,22 +408,27 @@ |
215 | @mock.patch('friends.utils.base.Base._create_eds_source', |
216 | return_value=None) |
217 | def test_unsuccessfull_push_to_eds(self, *mocks): |
218 | - bare_contact = {'name': 'Lucy Baron', 'id': '555555555'} |
219 | - eds_contact = self.protocol.create_contact(bare_contact) |
220 | + bare_contact = {'name': 'Lucy Baron', |
221 | + 'id': '555555555', |
222 | + 'username': "lucy.baron5", |
223 | + 'link': 'http:www.facebook.com/lucy.baron5'} |
224 | + eds_contact = self.protocol._create_contact(bare_contact) |
225 | result = self.protocol._push_to_eds('test-address-book', eds_contact) |
226 | self.assertEqual(result, False) |
227 | |
228 | @mock.patch('gi.repository.EDataServer.Source.new', |
229 | return_value=EDSSource('foo', 'bar')) |
230 | def test_create_eds_source(self, *mocks): |
231 | - self.protocol._source_registry = mock.Mock() |
232 | + regmock = self.protocol._source_registry = mock.Mock() |
233 | + regmock.ref_source = lambda x: x |
234 | + |
235 | result = self.protocol._create_eds_source('facebook-test-address') |
236 | self.assertEqual(result, 'test-source-uid') |
237 | |
238 | @mock.patch('gi.repository.EBook.BookClient.new', |
239 | return_value=EDSBookClientMock()) |
240 | def test_successful_previously_stored_contact(self, *mocks): |
241 | - result = Facebook.previously_stored_contact( |
242 | + result = self.protocol._previously_stored_contact( |
243 | True, 'facebook-id', '11111') |
244 | self.assertEqual(result, True) |
245 | |
246 | @@ -422,7 +438,7 @@ |
247 | return 'test-facebook-contacts' |
248 | def get_uid(self): |
249 | return 1345245 |
250 | - |
251 | + |
252 | reg_mock = self.protocol._source_registry = mock.Mock() |
253 | reg_mock.list_sources.return_value = [FakeSource()] |
254 | reg_mock.ref_source = lambda x: x |
255 | |
256 | === modified file 'friends/utils/base.py' |
257 | --- friends/utils/base.py 2012-10-31 10:42:53 +0000 |
258 | +++ friends/utils/base.py 2012-10-31 14:00:33 +0000 |
259 | @@ -31,7 +31,7 @@ |
260 | |
261 | from datetime import datetime |
262 | |
263 | -from gi.repository import EDataServer, EBook, Gio |
264 | +from gi.repository import EDataServer, EBook |
265 | |
266 | from friends.utils.authentication import Authentication |
267 | from friends.utils.model import COLUMN_INDICES, SCHEMA, DEFAULTS |
268 | @@ -94,16 +94,16 @@ |
269 | punctuation for the sake of comparing the strings. For example: |
270 | |
271 | Fred uses Friends to post identical messages on Twitter and Google+ |
272 | - (pretend that we support G+ for a moment). Fred writes "Hey jimbob, been |
273 | - to http://example.com lately?", and this message might show up on Twitter |
274 | - like "Hey @jimbob, been to example.com lately?", but it might show up on |
275 | - G+ like "Hey +jimbob, been to http://example.com lately?". So we need to |
276 | + (pretend that we support G+ for a moment). Fred writes 'Hey jimbob, been |
277 | + to http://example.com lately?', and this message might show up on Twitter |
278 | + like 'Hey @jimbob, been to example.com lately?', but it might show up on |
279 | + G+ like 'Hey +jimbob, been to http://example.com lately?'. So we need to |
280 | strip out all the possibly different bits in order to identify that these |
281 | messages are the same for our purposes. In both of these cases, the |
282 | - string is converted into "Heyjimbobbeentoexamplecomlately" and then they |
283 | + string is converted into 'Heyjimbobbeentoexamplecomlately' and then they |
284 | compare equally, so we've identified a duplicate message. |
285 | """ |
286 | - # Given a "row" of data, the sender and message fields are concatenated |
287 | + # Given a 'row' of data, the sender and message fields are concatenated |
288 | # together to form the raw key. Then we strip out details such as url |
289 | # schemes, punctuation, and whitespace, that allow for the fuzzy matching. |
290 | key = SCHEME_RE.sub('', row[SENDER_IDX] + row[MESSAGE_IDX]) |
291 | @@ -357,23 +357,12 @@ |
292 | def _push_to_eds(self, online_service, contact): |
293 | source_match = self._get_eds_source(online_service) |
294 | if source_match is None: |
295 | - new_source_uid = self._create_eds_source(online_service) |
296 | - if new_source_uid is None: |
297 | - log.error( |
298 | - 'Could not create a new source for {}'.format( |
299 | - online_service)) |
300 | - return False |
301 | - else: |
302 | - # https://bugzilla.gnome.org/show_bug.cgi?id=685986 |
303 | - # Potential race condition - need to sleep for a |
304 | - # couple of cycles to ensure the registry will return |
305 | - # a valid source object after commiting Evolution fix |
306 | - # on the way but for now we need this. |
307 | - time.sleep(1) |
308 | - source_match = self._source_registry.ref_source(new_source_uid) |
309 | + log.error("Push to EDS failed because we have been handed an online" + |
310 | + "service (%s) which does not have an address book", online_service) |
311 | + return False |
312 | client = EBook.BookClient.new(source_match) |
313 | client.open_sync(False, None) |
314 | - return client.add_contact_sync(contact, Gio.Cancellable()) |
315 | + return client.add_contact_sync(contact, None) |
316 | |
317 | def _create_eds_source(self, online_service): |
318 | source = EDataServer.Source.new(None, None) |
319 | @@ -382,26 +371,48 @@ |
320 | extension = source.get_extension( |
321 | EDataServer.SOURCE_EXTENSION_ADDRESS_BOOK) |
322 | extension.set_backend_name('local') |
323 | - if self._source_registry.commit_source_sync(source, Gio.Cancellable()): |
324 | - return source.get_uid() |
325 | + if self._source_registry.commit_source_sync(source, None): |
326 | + # https://bugzilla.gnome.org/show_bug.cgi?id=685986 |
327 | + # Potential race condition - need to sleep for a |
328 | + # couple of cycles to ensure the registry will return |
329 | + # a valid source object after commiting. Evolution fix |
330 | + # on the way but for now we need this. |
331 | + time.sleep(2) |
332 | + return self._source_registry.ref_source(source.get_uid()) |
333 | |
334 | def _get_eds_source(self, online_service): |
335 | for previous_source in self._source_registry.list_sources(None): |
336 | if previous_source.get_display_name() == online_service: |
337 | return self._source_registry.ref_source( |
338 | previous_source.get_uid()) |
339 | + return None |
340 | |
341 | - @classmethod |
342 | - def previously_stored_contact(cls, source, field, search_term): |
343 | + def _previously_stored_contact(self, source, field, search_term): |
344 | client = EBook.BookClient.new(source) |
345 | client.open_sync(False, None) |
346 | query = EBook.book_query_vcard_field_test( |
347 | field, EBook.BookQueryTest(0), search_term) |
348 | - cs = client.get_contacts_sync(query.to_string(), Gio.Cancellable()) |
349 | - log.debug('Search string: {}'.format(query.to_string())) |
350 | - if not cs[0]: |
351 | - return False # is this right ... |
352 | - return len(cs[1]) > 0 |
353 | + success, result = client.get_contacts_sync(query.to_string(), None) |
354 | + if not success: |
355 | + log.error('EDS Search failed on field %s', field) |
356 | + return False |
357 | + return len(result) > 0 |
358 | + |
359 | + def _delete_service_contacts(self, source): |
360 | + client = EBook.BookClient.new(source) |
361 | + client.open_sync(False, None) |
362 | + query = EBook.book_query_any_field_contains('') |
363 | + success, results = client.get_contacts_sync(query.to_string(), None) |
364 | + if not success: |
365 | + log.error('EDS search for delete all contacts failed') |
366 | + return False |
367 | + log.debug('Found {} contacts to delete'.format(len(results))) |
368 | + for contact in results: |
369 | + log.debug( |
370 | + 'Deleting contact {}'.format( |
371 | + contact.get_property('full-name'))) |
372 | + client.remove_contact_sync(contact, None) |
373 | + return True |
374 | |
375 | @classmethod |
376 | def get_features(cls): |
Ok Conor, I've landed your branch, it is looking a lot better and the tests are now passing. Please delete that old branch that has the stale mp still outstanding, it looks bad just sitting there with a 'needs fixing' review seeing as it's now fixed and landed ;-)
One thing I just want to mention though, the Facebook. _create_ contact method is quite long, and only about half of it seems to be specific to Facebook. Ideally, what I would like to see is that you implement a method called Base._create_ contact that handles all of the EBook logic, and then you'd have a very tiny wrapper around that from the Facebook plugin that just inserts the correct data into it. I'd like to see facebook.py not even import EBook at all (notice how it doesn't need to import any UOA modules despite implementing a ton of UOA logic).
Doing that will actually make it a lot easier to move forward with Twitter contacts, because you'll have more code re-use in the Base class and less code duplicated between protocols.
So when you start on the Twitter contacts, please don't even start importing EBook from twitter.py, first start working by dropping the EBook import from facebook.py and then work around that by wrapping it from the Base class.
Thanks!