Merge lp:~robru/friends/model-cleanup into lp:friends

Proposed by Robert Bruce Park
Status: Merged
Approved by: Ken VanDine
Approved revision: 178
Merged at revision: 175
Proposed branch: lp:~robru/friends/model-cleanup
Merge into: lp:friends
Diff against target: 316 lines (+118/-22)
7 files modified
friends/protocols/facebook.py (+10/-3)
friends/protocols/twitter.py (+10/-7)
friends/tests/test_facebook.py (+14/-3)
friends/tests/test_identica.py (+1/-1)
friends/tests/test_protocols.py (+49/-0)
friends/tests/test_twitter.py (+9/-1)
friends/utils/base.py (+25/-7)
To merge this branch: bzr merge lp:~robru/friends/model-cleanup
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Ken VanDine Approve
Review via email: mp+156704@code.launchpad.net

Commit message

Thread replies properly, and fix up likes/unlikes to modify model data upon success.

Description of the change

Alright Ken, just like you asked, replies are threading and likes/unlikes are updating the model contents as you'd expect. Not sure how we overlooked this originally ;-)

Anyway, I did the model alteration stuff with a fairly simple API in the Base class, rather than having to import the Model into the protocol classes, which is a bit hacky.

As usual, excellent test coverage is included.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:178
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~robru/friends/model-cleanup/+merge/156704/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/friends-ci/16/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/friends-raring-amd64-ci/16

Click here to trigger a rebuild:
http://s-jenkins:8080/job/friends-ci/16/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Ken VanDine (ken-vandine) wrote :

Looks good and does fix the issue. To test it I connected debug_slave.py to row-changed and called the like/unlike APIs. I consistently saw the value of likes get incremented/decremented along with the success callback, very quickly. However the value of liked gets changed separately, and a good 20 to 40 seconds later, which seems weird. The value does get changed though, just not seen on the model quickly.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) :
review: Approve (continuous-integration)
Revision history for this message
Robert Bruce Park (robru) wrote :

That is quite strange. I wonder if that might actually be a dbus synchronization issue rather than anything specific to the code I wrote. Because the way I wrote it, it changes the one value right after the other, although not in an atomic way.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'friends/protocols/facebook.py'
--- friends/protocols/facebook.py 2013-03-16 00:54:45 +0000
+++ friends/protocols/facebook.py 2013-04-02 21:50:28 +0000
@@ -226,6 +226,8 @@
226 This includes messages, statuses, wall posts, events, etc.226 This includes messages, statuses, wall posts, events, etc.
227 """227 """
228 self._like(obj_id, 'POST')228 self._like(obj_id, 'POST')
229 self._inc_cell(obj_id, 'likes')
230 self._set_cell(obj_id, 'liked', True)
229 return obj_id231 return obj_id
230232
231 @feature233 @feature
@@ -235,9 +237,11 @@
235 This includes messages, statuses, wall posts, events, etc.237 This includes messages, statuses, wall posts, events, etc.
236 """238 """
237 self._like(obj_id, 'DELETE')239 self._like(obj_id, 'DELETE')
240 self._dec_cell(obj_id, 'likes')
241 self._set_cell(obj_id, 'liked', False)
238 return obj_id242 return obj_id
239243
240 def _send(self, obj_id, message, endpoint):244 def _send(self, obj_id, message, endpoint, stream='messages'):
241 url = API_BASE.format(id=obj_id) + endpoint245 url = API_BASE.format(id=obj_id) + endpoint
242 token = self._get_access_token()246 token = self._get_access_token()
243247
@@ -251,7 +255,9 @@
251255
252 url = API_BASE.format(id=new_id)256 url = API_BASE.format(id=new_id)
253 entry = Downloader(url, params=dict(access_token=token)).get_json()257 entry = Downloader(url, params=dict(access_token=token)).get_json()
254 return self._publish_entry(entry)258 return self._publish_entry(
259 stream=stream,
260 entry=entry)
255261
256 @feature262 @feature
257 def send(self, message, obj_id='me'):263 def send(self, message, obj_id='me'):
@@ -270,7 +276,8 @@
270 obj_id can be the id of any Facebook object that supports being276 obj_id can be the id of any Facebook object that supports being
271 commented on, which will generally be Posts.277 commented on, which will generally be Posts.
272 """278 """
273 return self._send(obj_id, message, '/comments')279 return self._send(obj_id, message, '/comments',
280 stream='reply_to/{}'.format(obj_id))
274281
275 @feature282 @feature
276 def delete(self, obj_id):283 def delete(self, obj_id):
277284
=== modified file 'friends/protocols/twitter.py'
--- friends/protocols/twitter.py 2013-03-20 01:24:15 +0000
+++ friends/protocols/twitter.py 2013-04-02 21:50:28 +0000
@@ -248,9 +248,10 @@
248 def send_thread(self, message_id, message):248 def send_thread(self, message_id, message):
249 """Send a reply message to message_id.249 """Send a reply message to message_id.
250250
251 Note that you have to @mention the message_id owner's screen name in251 This method takes care to prepend the @mention to the start of
252 order for Twitter to actually accept this as a reply. Otherwise it252 your tweet if you forgot it. Without this, Twitter will just
253 will just be an ordinary tweet.253 consider it a regular message, and it won't be part of any
254 conversation.
254 """255 """
255 try:256 try:
256 sender = '@{}'.format(self._fetch_cell(message_id, 'sender_nick'))257 sender = '@{}'.format(self._fetch_cell(message_id, 'sender_nick'))
@@ -261,7 +262,8 @@
261 url = self._api_base.format(endpoint='statuses/update')262 url = self._api_base.format(endpoint='statuses/update')
262 tweet = self._get_url(url, dict(in_reply_to_status_id=message_id,263 tweet = self._get_url(url, dict(in_reply_to_status_id=message_id,
263 status=message))264 status=message))
264 return self._publish_tweet(tweet)265 return self._publish_tweet(
266 tweet, stream='reply_to/{}'.format(message_id))
265267
266# https://dev.twitter.com/docs/api/1.1/post/statuses/destroy/%3Aid268# https://dev.twitter.com/docs/api/1.1/post/statuses/destroy/%3Aid
267 @feature269 @feature
@@ -303,10 +305,9 @@
303 """Announce to the world your undying love for a tweet."""305 """Announce to the world your undying love for a tweet."""
304 url = self._api_base.format(endpoint='favorites/create')306 url = self._api_base.format(endpoint='favorites/create')
305 self._get_url(url, dict(id=message_id))307 self._get_url(url, dict(id=message_id))
308 self._inc_cell(message_id, 'likes')
309 self._set_cell(message_id, 'liked', True)
306 return message_id310 return message_id
307 # I don't think we need to publish this tweet because presumably the
308 # user has clicked the 'favorite' button on the message that's already
309 # in the stream.
310311
311# https://dev.twitter.com/docs/api/1.1/post/favorites/destroy312# https://dev.twitter.com/docs/api/1.1/post/favorites/destroy
312 @feature313 @feature
@@ -314,6 +315,8 @@
314 """Renounce your undying love for a tweet."""315 """Renounce your undying love for a tweet."""
315 url = self._api_base.format(endpoint='favorites/destroy')316 url = self._api_base.format(endpoint='favorites/destroy')
316 self._get_url(url, dict(id=message_id))317 self._get_url(url, dict(id=message_id))
318 self._dec_cell(message_id, 'likes')
319 self._set_cell(message_id, 'liked', False)
317 return message_id320 return message_id
318321
319# https://dev.twitter.com/docs/api/1.1/get/search/tweets322# https://dev.twitter.com/docs/api/1.1/get/search/tweets
320323
=== modified file 'friends/tests/test_facebook.py'
--- friends/tests/test_facebook.py 2013-03-16 00:54:45 +0000
+++ friends/tests/test_facebook.py 2013-04-02 21:50:28 +0000
@@ -269,7 +269,8 @@
269 'http://facebook.com/post_id')269 'http://facebook.com/post_id')
270270
271 token.assert_called_once_with()271 token.assert_called_once_with()
272 publish.assert_called_with({'id': 'post_id'})272 publish.assert_called_with(entry={'id': 'post_id'},
273 stream='messages')
273 self.assertEqual(274 self.assertEqual(
274 dload.mock_calls,275 dload.mock_calls,
275 [mock.call(),276 [mock.call(),
@@ -298,7 +299,8 @@
298 'http://facebook.com/new_post_id')299 'http://facebook.com/new_post_id')
299300
300 token.assert_called_once_with()301 token.assert_called_once_with()
301 publish.assert_called_with({'id': 'post_id'})302 publish.assert_called_with(entry={'id': 'post_id'},
303 stream='messages')
302 self.assertEqual(304 self.assertEqual(
303 dload.mock_calls,305 dload.mock_calls,
304 [mock.call(),306 [mock.call(),
@@ -327,7 +329,8 @@
327 'http://facebook.com/private_message_id')329 'http://facebook.com/private_message_id')
328330
329 token.assert_called_once_with()331 token.assert_called_once_with()
330 publish.assert_called_with({'id': 'comment_id'})332 publish.assert_called_with(entry={'id': 'comment_id'},
333 stream='reply_to/post_id')
331 self.assertEqual(334 self.assertEqual(
332 dload.mock_calls,335 dload.mock_calls,
333 [mock.call(),336 [mock.call(),
@@ -427,9 +430,13 @@
427 dload().get_json.return_value = True430 dload().get_json.return_value = True
428 token = self.protocol._get_access_token = mock.Mock(431 token = self.protocol._get_access_token = mock.Mock(
429 return_value='face')432 return_value='face')
433 inc_cell = self.protocol._inc_cell = mock.Mock()
434 set_cell = self.protocol._set_cell = mock.Mock()
430435
431 self.assertEqual(self.protocol.like('post_id'), 'post_id')436 self.assertEqual(self.protocol.like('post_id'), 'post_id')
432437
438 inc_cell.assert_called_once_with('post_id', 'likes')
439 set_cell.assert_called_once_with('post_id', 'liked', True)
433 token.assert_called_once_with()440 token.assert_called_once_with()
434 dload.assert_called_with(441 dload.assert_called_with(
435 'https://graph.facebook.com/post_id/likes',442 'https://graph.facebook.com/post_id/likes',
@@ -441,9 +448,13 @@
441 dload.get_json.return_value = True448 dload.get_json.return_value = True
442 token = self.protocol._get_access_token = mock.Mock(449 token = self.protocol._get_access_token = mock.Mock(
443 return_value='face')450 return_value='face')
451 dec_cell = self.protocol._dec_cell = mock.Mock()
452 set_cell = self.protocol._set_cell = mock.Mock()
444453
445 self.assertEqual(self.protocol.unlike('post_id'), 'post_id')454 self.assertEqual(self.protocol.unlike('post_id'), 'post_id')
446455
456 dec_cell.assert_called_once_with('post_id', 'likes')
457 set_cell.assert_called_once_with('post_id', 'liked', False)
447 token.assert_called_once_with()458 token.assert_called_once_with()
448 dload.assert_called_once_with(459 dload.assert_called_once_with(
449 'https://graph.facebook.com/post_id/likes',460 'https://graph.facebook.com/post_id/likes',
450461
=== modified file 'friends/tests/test_identica.py'
--- friends/tests/test_identica.py 2013-03-14 19:14:03 +0000
+++ friends/tests/test_identica.py 2013-04-02 21:50:28 +0000
@@ -150,7 +150,7 @@
150 '1234',150 '1234',
151 'Why yes, I would love to respond to your tweet @pumpichank!')151 'Why yes, I would love to respond to your tweet @pumpichank!')
152152
153 publish.assert_called_with('tweet')153 publish.assert_called_with('tweet', stream='reply_to/1234')
154 get_url.assert_called_with(154 get_url.assert_called_with(
155 'http://identi.ca/api/statuses/update.json',155 'http://identi.ca/api/statuses/update.json',
156 dict(status=156 dict(status=
157157
=== modified file 'friends/tests/test_protocols.py'
--- friends/tests/test_protocols.py 2013-03-19 00:43:13 +0000
+++ friends/tests/test_protocols.py 2013-04-02 21:50:28 +0000
@@ -370,6 +370,55 @@
370 self.assertEqual(TestModel.get_row(1)[COLUMN_INDICES['sender']],370 self.assertEqual(TestModel.get_row(1)[COLUMN_INDICES['sender']],
371 'tedtholomew')371 'tedtholomew')
372372
373 @mock.patch('friends.utils.base.Model', TestModel)
374 @mock.patch('friends.utils.base._seen_ids', {})
375 def test_inc_cell(self):
376 base = Base(FakeAccount())
377 self.assertEqual(0, TestModel.get_n_rows())
378 self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
379 base._inc_cell('1234', 'likes')
380 row = TestModel.get_row(0)
381 self.assertEqual(
382 list(row),
383 ['base', 88, '1234', '', '', '', '', False, '', '', '', '', 11,
384 True, '', '', '', '', '', '', '', 0.0, 0.0])
385
386 @mock.patch('friends.utils.base.Model', TestModel)
387 @mock.patch('friends.utils.base._seen_ids', {})
388 def test_dec_cell(self):
389 base = Base(FakeAccount())
390 self.assertEqual(0, TestModel.get_n_rows())
391 self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
392 base._dec_cell('1234', 'likes')
393 row = TestModel.get_row(0)
394 self.assertEqual(
395 list(row),
396 ['base', 88, '1234', '', '', '', '', False, '', '', '', '', 9,
397 True, '', '', '', '', '', '', '', 0.0, 0.0])
398
399 @mock.patch('friends.utils.base.Model', TestModel)
400 @mock.patch('friends.utils.base._seen_ids', {})
401 def test_set_cell(self):
402 base = Base(FakeAccount())
403 self.assertEqual(0, TestModel.get_n_rows())
404 self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
405 base._set_cell('1234', 'likes', 500)
406 base._set_cell('1234', 'liked', False)
407 base._set_cell('1234', 'message_id', '5678')
408 row = TestModel.get_row(0)
409 self.assertEqual(
410 list(row),
411 ['base', 88, '5678', '', '', '', '', False, '', '', '', '', 500,
412 False, '', '', '', '', '', '', '', 0.0, 0.0])
413
414 @mock.patch('friends.utils.base.Model', TestModel)
415 @mock.patch('friends.utils.base._seen_ids', {})
416 def test_fetch_cell(self):
417 base = Base(FakeAccount())
418 self.assertEqual(0, TestModel.get_n_rows())
419 self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
420 self.assertEqual(base._fetch_cell('1234', 'likes'), 10)
421
373 def test_basic_login(self):422 def test_basic_login(self):
374 # Try to log in twice. The second login attempt returns False because423 # Try to log in twice. The second login attempt returns False because
375 # it's already logged in.424 # it's already logged in.
376425
=== modified file 'friends/tests/test_twitter.py'
--- friends/tests/test_twitter.py 2013-03-19 04:17:58 +0000
+++ friends/tests/test_twitter.py 2013-04-02 21:50:28 +0000
@@ -383,7 +383,7 @@
383 'Why yes, I would love to respond to your tweet @pumpichank!'),383 'Why yes, I would love to respond to your tweet @pumpichank!'),
384 'tweet permalink')384 'tweet permalink')
385385
386 publish.assert_called_with('tweet')386 publish.assert_called_with('tweet', stream='reply_to/1234')
387 get_url.assert_called_with(387 get_url.assert_called_with(
388 'https://api.twitter.com/1.1/statuses/update.json',388 'https://api.twitter.com/1.1/statuses/update.json',
389 dict(status='Why yes, I would love to respond to your '389 dict(status='Why yes, I would love to respond to your '
@@ -471,18 +471,26 @@
471471
472 def test_like(self):472 def test_like(self):
473 get_url = self.protocol._get_url = mock.Mock()473 get_url = self.protocol._get_url = mock.Mock()
474 inc_cell = self.protocol._inc_cell = mock.Mock()
475 set_cell = self.protocol._set_cell = mock.Mock()
474476
475 self.assertEqual(self.protocol.like('1234'), '1234')477 self.assertEqual(self.protocol.like('1234'), '1234')
476478
479 inc_cell.assert_called_once_with('1234', 'likes')
480 set_cell.assert_called_once_with('1234', 'liked', True)
477 get_url.assert_called_with(481 get_url.assert_called_with(
478 'https://api.twitter.com/1.1/favorites/create.json',482 'https://api.twitter.com/1.1/favorites/create.json',
479 dict(id='1234'))483 dict(id='1234'))
480484
481 def test_unlike(self):485 def test_unlike(self):
482 get_url = self.protocol._get_url = mock.Mock()486 get_url = self.protocol._get_url = mock.Mock()
487 dec_cell = self.protocol._dec_cell = mock.Mock()
488 set_cell = self.protocol._set_cell = mock.Mock()
483489
484 self.assertEqual(self.protocol.unlike('1234'), '1234')490 self.assertEqual(self.protocol.unlike('1234'), '1234')
485491
492 dec_cell.assert_called_once_with('1234', 'likes')
493 set_cell.assert_called_once_with('1234', 'liked', False)
486 get_url.assert_called_with(494 get_url.assert_called_with(
487 'https://api.twitter.com/1.1/favorites/destroy.json',495 'https://api.twitter.com/1.1/favorites/destroy.json',
488 dict(id='1234'))496 dict(id='1234'))
489497
=== modified file 'friends/utils/base.py'
--- friends/utils/base.py 2013-03-20 01:19:39 +0000
+++ friends/utils/base.py 2013-04-02 21:50:28 +0000
@@ -503,15 +503,33 @@
503 message = None503 message = None
504 raise FriendsError(message or str(error))504 raise FriendsError(message or str(error))
505505
506 def _calculate_row_cell(self, message_id, column_name):
507 """Find x,y coords in the model based on message_id and column_name."""
508 row_id = _seen_ids.get(message_id)
509 col_idx = COLUMN_INDICES.get(column_name)
510 if None in (row_id, col_idx):
511 raise FriendsError('Cell could not be found.')
512 return row_id, col_idx
513
506 def _fetch_cell(self, message_id, column_name):514 def _fetch_cell(self, message_id, column_name):
507 """Find a column value associated with a specific message_id."""515 """Find a column value associated with a specific message_id."""
508 row_id = _seen_ids.get(message_id)516 row_id, col_idx = self._calculate_row_cell(message_id, column_name)
509 col_idx = COLUMN_INDICES.get(column_name)517 return Model.get_row(row_id)[col_idx]
510 if None not in (row_id, col_idx):518
511 row = Model.get_row(row_id)519 def _set_cell(self, message_id, column_name, value):
512 return row[col_idx]520 """Set a column value associated with a specific message_id."""
513 else:521 row_id, col_idx = self._calculate_row_cell(message_id, column_name)
514 raise FriendsError('Value could not be found.')522 Model.get_row(row_id)[col_idx] = value
523
524 def _inc_cell(self, message_id, column_name):
525 """Increment a column value associated with a specific message_id."""
526 row_id, col_idx = self._calculate_row_cell(message_id, column_name)
527 Model.get_row(row_id)[col_idx] += 1
528
529 def _dec_cell(self, message_id, column_name):
530 """Decrement a column value associated with a specific message_id."""
531 row_id, col_idx = self._calculate_row_cell(message_id, column_name)
532 Model.get_row(row_id)[col_idx] -= 1
515533
516 def _new_book_client(self, source):534 def _new_book_client(self, source):
517 client = EBook.BookClient.new(source)535 client = EBook.BookClient.new(source)

Subscribers

People subscribed via source and target branches