Merge lp:~cjcurran/friends/another-test-fix-contacts-fb-import into lp:friends

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
Reviewer Review Type Date Requested Status
Robert Bruce Park Approve
Review via email: mp+132327@code.launchpad.net
To post a comment you must log in.
50. By Conor Curran

rebase to pick up facebook id

Revision history for this message
Robert Bruce Park (robru) wrote :

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!

review: Approve

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):

Subscribers

People subscribed via source and target branches

to all changes: