Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 249 | ||||
Proposed branch: | lp:~kai-mast/friends/links | ||||
Merge into: | lp:friends | ||||
Diff against target: |
437 lines (+361/-17) 4 files modified
friends/protocols/twitter.py (+48/-15) friends/tests/data/twitter-hashtags.dat (+94/-0) friends/tests/data/twitter-multiple-links.dat (+144/-0) friends/tests/test_twitter.py (+75/-2) |
||||
To merge this branch: | bzr merge lp:~kai-mast/friends/links | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot (community) | continuous-integration | Needs Fixing | |
Robert Bruce Park | Approve | ||
Review via email: mp+202954@code.launchpad.net |
Commit message
Description of the change
This branch likifies mentions and hashtags. Hashtags open a link with the search in a browser. Friends has support for searches but afaik there is no way to add links to them. So this is the best we can do right now. Imo that makes tweets (especially retweets) much more interakctive.
I also fixed a problem when there are multiple links in a tweet and also added a test so this stays fixed.
Kai Mast (kai-mast) wrote : | # |
Oh okay I thought dictionaries where sorted by default (I am so used to C++ ;) )
I haven't even thought of going through the message in reverse to keep track off the offset. Good point.
Kai Mast (kai-mast) wrote : | # |
Better?
I am not sure how to use just one linkify function because some have absolute addresses and some don't, so I now have linkify and linkify_mention, where the latter calls the first.
I also made the links use https which should be used whenever possible in my opinion.
Robert Bruce Park (robru) wrote : | # |
This is a ton better, thank you!
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Autolanding.
No commit message was specified in the merge proposal. Hit 'Add commit message' on the merge proposal web page or follow the link below. You can approve the merge proposal yourself to rerun.
https:/
Robert Bruce Park (robru) wrote : | # |
One thing to watch out for is that you have a ton of trailing whitespace all over. I don't like to see that in my code ;-)
Don't worry, I'm cleaning it up for this merge, but if you could start stripping trailing whitespace before committing I'd appreciate it.
Preview Diff
1 | === modified file 'friends/protocols/twitter.py' | |||
2 | --- friends/protocols/twitter.py 2013-12-10 07:04:05 +0000 | |||
3 | +++ friends/protocols/twitter.py 2014-01-24 10:56:16 +0000 | |||
4 | @@ -127,29 +127,51 @@ | |||
5 | 127 | 127 | ||
6 | 128 | #Resolve t.co | 128 | #Resolve t.co |
7 | 129 | #TODO support more than one url and/or media file | 129 | #TODO support more than one url and/or media file |
14 | 130 | for url in (entities.get('urls', []) + entities.get('media', [])): | 130 | urls = {} |
15 | 131 | begin, end = url.get('indices', (None, None)) | 131 | |
16 | 132 | 132 | for url in (entities.get('urls', []) + entities.get('media', []) + entities.get('user_mentions', []) + entities.get('hashtags', [])): | |
17 | 133 | expanded_url = url.get('expanded_url', '') | 133 | begin, end = url.get('indices', (None, None)) |
18 | 134 | display_url = url.get('display_url', '') | 134 | |
19 | 135 | other_url = url.get('url', '') | 135 | #Drop invalid entities (just to be safe) |
20 | 136 | if None not in (begin, end): | ||
21 | 137 | urls[begin] = url | ||
22 | 138 | |||
23 | 139 | for key, url in sorted(urls.items(), reverse = True): | ||
24 | 140 | begin, end = url.get('indices', (None, None)) | ||
25 | 141 | |||
26 | 142 | expanded_url = url.get('expanded_url') | ||
27 | 143 | display_url = url.get('display_url') | ||
28 | 144 | other_url = url.get('url') | ||
29 | 145 | |||
30 | 146 | mention_name = url.get('screen_name') | ||
31 | 136 | 147 | ||
32 | 137 | picture_url = url.get('media_url', picture_url) | 148 | picture_url = url.get('media_url', picture_url) |
33 | 149 | |||
34 | 150 | hashtag = url.get('text') | ||
35 | 151 | |||
36 | 152 | content = None | ||
37 | 138 | 153 | ||
38 | 139 | # Friends has no notion of display URLs, so this is handled at the protocol level | 154 | # Friends has no notion of display URLs, so this is handled at the protocol level |
40 | 140 | if None not in (begin, end): | 155 | if (other_url or expanded_url): |
41 | 156 | content = self._linkify(expanded_url or other_url, display_url or other_url) | ||
42 | 157 | |||
43 | 158 | # Linkify hashtags until supported by friends | ||
44 | 159 | if hashtag: | ||
45 | 160 | content = self._linkify('https://twitter.com/search?q=%23' + hashtag + '&src=hash', '#' + hashtag) | ||
46 | 161 | |||
47 | 162 | # Linkify a mention until they are supported natively by friends | ||
48 | 163 | if mention_name: | ||
49 | 164 | content = self._linkify_mention(mention_name) | ||
50 | 165 | |||
51 | 166 | if content: | ||
52 | 141 | message = ''.join([ | 167 | message = ''.join([ |
53 | 142 | message[:begin], | 168 | message[:begin], |
59 | 143 | '<a href="', | 169 | content, |
55 | 144 | (expanded_url or other_url), | ||
56 | 145 | '">', | ||
57 | 146 | (display_url or other_url), | ||
58 | 147 | '</a>', | ||
60 | 148 | message[end:]]) | 170 | message[end:]]) |
62 | 149 | 171 | ||
63 | 150 | if retweet: | 172 | if retweet: |
66 | 151 | message = 'RT @{}: {}'.format( | 173 | message = 'RT {}: {}'.format( |
67 | 152 | retweet.get('user', {}).get('screen_name', ''), | 174 | self._linkify_mention(retweet.get('user', {}).get('screen_name', '')), |
68 | 153 | message | 175 | message |
69 | 154 | ) | 176 | ) |
70 | 155 | 177 | ||
71 | @@ -168,6 +190,17 @@ | |||
72 | 168 | link_picture=picture_url, | 190 | link_picture=picture_url, |
73 | 169 | ) | 191 | ) |
74 | 170 | return permalink | 192 | return permalink |
75 | 193 | |||
76 | 194 | def _linkify_mention(self, name): | ||
77 | 195 | return self._linkify('https://twitter.com/' + name, '@' + name) | ||
78 | 196 | |||
79 | 197 | def _linkify(self, address, name): | ||
80 | 198 | return ''.join([ | ||
81 | 199 | '<a href="', | ||
82 | 200 | address, | ||
83 | 201 | '">', | ||
84 | 202 | name, | ||
85 | 203 | '</a>']) | ||
86 | 171 | 204 | ||
87 | 172 | def _append_since(self, url, stream='messages'): | 205 | def _append_since(self, url, stream='messages'): |
88 | 173 | since = self._tweet_ids.get(stream) | 206 | since = self._tweet_ids.get(stream) |
89 | 174 | 207 | ||
90 | === added file 'friends/tests/data/twitter-hashtags.dat' | |||
91 | --- friends/tests/data/twitter-hashtags.dat 1970-01-01 00:00:00 +0000 | |||
92 | +++ friends/tests/data/twitter-hashtags.dat 2014-01-24 10:56:16 +0000 | |||
93 | @@ -0,0 +1,94 @@ | |||
94 | 1 | { | ||
95 | 2 | "created_at": "Fri Jan 17 14:23:41 +0000 2014", | ||
96 | 3 | "id": 424185261375766500, | ||
97 | 4 | "id_str": "424185261375766530", | ||
98 | 5 | "text": "A service that filters food pictures from Instagram. #MillionDollarIdea", | ||
99 | 6 | "source": "web", | ||
100 | 7 | "truncated": false, | ||
101 | 8 | "in_reply_to_status_id": null, | ||
102 | 9 | "in_reply_to_status_id_str": null, | ||
103 | 10 | "in_reply_to_user_id": null, | ||
104 | 11 | "in_reply_to_user_id_str": null, | ||
105 | 12 | "in_reply_to_screen_name": null, | ||
106 | 13 | "user": { | ||
107 | 14 | "id": 17339829, | ||
108 | 15 | "id_str": "17339829", | ||
109 | 16 | "name": "Kai Mast", | ||
110 | 17 | "screen_name": "Kai_Mast", | ||
111 | 18 | "location": "Bamberg", | ||
112 | 19 | "description": "Computer Science Student, Grad School Applicant, C++ Fanboy", | ||
113 | 20 | "url": "http://t.co/1myW31Mlhl", | ||
114 | 21 | "entities": { | ||
115 | 22 | "url": { | ||
116 | 23 | "urls": [ | ||
117 | 24 | { | ||
118 | 25 | "url": "http://t.co/1myW31Mlhl", | ||
119 | 26 | "expanded_url": "http://kai-mast.de", | ||
120 | 27 | "display_url": "kai-mast.de", | ||
121 | 28 | "indices": [ | ||
122 | 29 | 0, | ||
123 | 30 | 22 | ||
124 | 31 | ] | ||
125 | 32 | } | ||
126 | 33 | ] | ||
127 | 34 | }, | ||
128 | 35 | "description": { | ||
129 | 36 | "urls": [] | ||
130 | 37 | } | ||
131 | 38 | }, | ||
132 | 39 | "protected": false, | ||
133 | 40 | "followers_count": 415, | ||
134 | 41 | "friends_count": 904, | ||
135 | 42 | "listed_count": 36, | ||
136 | 43 | "created_at": "Wed Nov 12 14:23:09 +0000 2008", | ||
137 | 44 | "favourites_count": 939, | ||
138 | 45 | "utc_offset": 3600, | ||
139 | 46 | "time_zone": "Berlin", | ||
140 | 47 | "geo_enabled": true, | ||
141 | 48 | "verified": false, | ||
142 | 49 | "statuses_count": 7886, | ||
143 | 50 | "lang": "en", | ||
144 | 51 | "contributors_enabled": false, | ||
145 | 52 | "is_translator": false, | ||
146 | 53 | "profile_background_color": "8B542B", | ||
147 | 54 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme8/bg.gif", | ||
148 | 55 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme8/bg.gif", | ||
149 | 56 | "profile_background_tile": false, | ||
150 | 57 | "profile_image_url": "http://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P_normal.png", | ||
151 | 58 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P_normal.png", | ||
152 | 59 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/17339829/1360006443", | ||
153 | 60 | "profile_link_color": "9D582E", | ||
154 | 61 | "profile_sidebar_border_color": "D9B17E", | ||
155 | 62 | "profile_sidebar_fill_color": "EADEAA", | ||
156 | 63 | "profile_text_color": "333333", | ||
157 | 64 | "profile_use_background_image": true, | ||
158 | 65 | "default_profile": false, | ||
159 | 66 | "default_profile_image": false, | ||
160 | 67 | "following": false, | ||
161 | 68 | "follow_request_sent": false, | ||
162 | 69 | "notifications": false | ||
163 | 70 | }, | ||
164 | 71 | "geo": null, | ||
165 | 72 | "coordinates": null, | ||
166 | 73 | "place": null, | ||
167 | 74 | "contributors": null, | ||
168 | 75 | "retweet_count": 0, | ||
169 | 76 | "favorite_count": 0, | ||
170 | 77 | "entities": { | ||
171 | 78 | "hashtags": [ | ||
172 | 79 | { | ||
173 | 80 | "text": "MillionDollarIdea", | ||
174 | 81 | "indices": [ | ||
175 | 82 | 53, | ||
176 | 83 | 71 | ||
177 | 84 | ] | ||
178 | 85 | } | ||
179 | 86 | ], | ||
180 | 87 | "symbols": [], | ||
181 | 88 | "urls": [], | ||
182 | 89 | "user_mentions": [] | ||
183 | 90 | }, | ||
184 | 91 | "favorited": false, | ||
185 | 92 | "retweeted": false, | ||
186 | 93 | "lang": "en" | ||
187 | 94 | } | ||
188 | 0 | 95 | ||
189 | === added file 'friends/tests/data/twitter-multiple-links.dat' | |||
190 | --- friends/tests/data/twitter-multiple-links.dat 1970-01-01 00:00:00 +0000 | |||
191 | +++ friends/tests/data/twitter-multiple-links.dat 2014-01-24 10:56:16 +0000 | |||
192 | @@ -0,0 +1,144 @@ | |||
193 | 1 | { | ||
194 | 2 | "created_at": "Thu Jan 23 11:40:35 +0000 2014", | ||
195 | 3 | "id": 426318539796930560, | ||
196 | 4 | "id_str": "426318539796930560", | ||
197 | 5 | "text": "An old people's home has recreated famous movie scenes for a wonderful calendar http://t.co/jjqteYzur0 http://t.co/JxQTPG7WLL", | ||
198 | 6 | "source": "<a href=\"https://about.twitter.com/products/tweetdeck\" rel=\"nofollow\">TweetDeck</a>", | ||
199 | 7 | "truncated": false, | ||
200 | 8 | "in_reply_to_status_id": null, | ||
201 | 9 | "in_reply_to_status_id_str": null, | ||
202 | 10 | "in_reply_to_user_id": null, | ||
203 | 11 | "in_reply_to_user_id_str": null, | ||
204 | 12 | "in_reply_to_screen_name": null, | ||
205 | 13 | "user": { | ||
206 | 14 | "id": 16973333, | ||
207 | 15 | "id_str": "16973333", | ||
208 | 16 | "name": "The Independent", | ||
209 | 17 | "screen_name": "Independent", | ||
210 | 18 | "location": "London, United Kingdom", | ||
211 | 19 | "description": "News, comment and features from The Independent. Also follow: @IndyVoices, \r\n@IndyPolitics, @IndyWorld and our journalists at http://t.co/YjS7NcXK4A", | ||
212 | 20 | "url": "http://t.co/wDS5ly0QoO", | ||
213 | 21 | "entities": { | ||
214 | 22 | "url": { | ||
215 | 23 | "urls": [ | ||
216 | 24 | { | ||
217 | 25 | "url": "http://t.co/wDS5ly0QoO", | ||
218 | 26 | "expanded_url": "http://www.independent.co.uk", | ||
219 | 27 | "display_url": "independent.co.uk", | ||
220 | 28 | "indices": [ | ||
221 | 29 | 0, | ||
222 | 30 | 22 | ||
223 | 31 | ] | ||
224 | 32 | } | ||
225 | 33 | ] | ||
226 | 34 | }, | ||
227 | 35 | "description": { | ||
228 | 36 | "urls": [ | ||
229 | 37 | { | ||
230 | 38 | "url": "http://t.co/YjS7NcXK4A", | ||
231 | 39 | "expanded_url": "http://ind.pn/Wdlm9a", | ||
232 | 40 | "display_url": "ind.pn/Wdlm9a", | ||
233 | 41 | "indices": [ | ||
234 | 42 | 126, | ||
235 | 43 | 148 | ||
236 | 44 | ] | ||
237 | 45 | } | ||
238 | 46 | ] | ||
239 | 47 | } | ||
240 | 48 | }, | ||
241 | 49 | "protected": false, | ||
242 | 50 | "followers_count": 505759, | ||
243 | 51 | "friends_count": 1747, | ||
244 | 52 | "listed_count": 7890, | ||
245 | 53 | "created_at": "Sun Oct 26 00:00:29 +0000 2008", | ||
246 | 54 | "favourites_count": 29, | ||
247 | 55 | "utc_offset": 0, | ||
248 | 56 | "time_zone": "London", | ||
249 | 57 | "geo_enabled": false, | ||
250 | 58 | "verified": true, | ||
251 | 59 | "statuses_count": 58003, | ||
252 | 60 | "lang": "en", | ||
253 | 61 | "contributors_enabled": false, | ||
254 | 62 | "is_translator": false, | ||
255 | 63 | "profile_background_color": "EBEBEB", | ||
256 | 64 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/378800000119288704/4ac964c83462c88837dc1e735aa1a45e.png", | ||
257 | 65 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/378800000119288704/4ac964c83462c88837dc1e735aa1a45e.png", | ||
258 | 66 | "profile_background_tile": true, | ||
259 | 67 | "profile_image_url": "http://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d_normal.jpeg", | ||
260 | 68 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d_normal.jpeg", | ||
261 | 69 | "profile_link_color": "FC051A", | ||
262 | 70 | "profile_sidebar_border_color": "FFFFFF", | ||
263 | 71 | "profile_sidebar_fill_color": "FFFFFF", | ||
264 | 72 | "profile_text_color": "333333", | ||
265 | 73 | "profile_use_background_image": true, | ||
266 | 74 | "default_profile": false, | ||
267 | 75 | "default_profile_image": false, | ||
268 | 76 | "following": false, | ||
269 | 77 | "follow_request_sent": false, | ||
270 | 78 | "notifications": false | ||
271 | 79 | }, | ||
272 | 80 | "geo": null, | ||
273 | 81 | "coordinates": null, | ||
274 | 82 | "place": null, | ||
275 | 83 | "contributors": null, | ||
276 | 84 | "retweet_count": 131, | ||
277 | 85 | "favorite_count": 83, | ||
278 | 86 | "entities": { | ||
279 | 87 | "hashtags": [], | ||
280 | 88 | "symbols": [], | ||
281 | 89 | "urls": [ | ||
282 | 90 | { | ||
283 | 91 | "url": "http://t.co/jjqteYzur0", | ||
284 | 92 | "expanded_url": "http://ind.pn/1g3wX9q", | ||
285 | 93 | "display_url": "ind.pn/1g3wX9q", | ||
286 | 94 | "indices": [ | ||
287 | 95 | 80, | ||
288 | 96 | 102 | ||
289 | 97 | ] | ||
290 | 98 | } | ||
291 | 99 | ], | ||
292 | 100 | "user_mentions": [], | ||
293 | 101 | "media": [ | ||
294 | 102 | { | ||
295 | 103 | "id": 426318539692056600, | ||
296 | 104 | "id_str": "426318539692056576", | ||
297 | 105 | "indices": [ | ||
298 | 106 | 103, | ||
299 | 107 | 125 | ||
300 | 108 | ], | ||
301 | 109 | "media_url": "http://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg", | ||
302 | 110 | "media_url_https": "https://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg", | ||
303 | 111 | "url": "http://t.co/JxQTPG7WLL", | ||
304 | 112 | "display_url": "pic.twitter.com/JxQTPG7WLL", | ||
305 | 113 | "expanded_url": "http://twitter.com/Independent/status/426318539796930560/photo/1", | ||
306 | 114 | "type": "photo", | ||
307 | 115 | "sizes": { | ||
308 | 116 | "thumb": { | ||
309 | 117 | "w": 150, | ||
310 | 118 | "h": 150, | ||
311 | 119 | "resize": "crop" | ||
312 | 120 | }, | ||
313 | 121 | "large": { | ||
314 | 122 | "w": 1024, | ||
315 | 123 | "h": 682, | ||
316 | 124 | "resize": "fit" | ||
317 | 125 | }, | ||
318 | 126 | "small": { | ||
319 | 127 | "w": 340, | ||
320 | 128 | "h": 226, | ||
321 | 129 | "resize": "fit" | ||
322 | 130 | }, | ||
323 | 131 | "medium": { | ||
324 | 132 | "w": 600, | ||
325 | 133 | "h": 400, | ||
326 | 134 | "resize": "fit" | ||
327 | 135 | } | ||
328 | 136 | } | ||
329 | 137 | } | ||
330 | 138 | ] | ||
331 | 139 | }, | ||
332 | 140 | "favorited": false, | ||
333 | 141 | "retweeted": false, | ||
334 | 142 | "possibly_sensitive": false, | ||
335 | 143 | "lang": "en" | ||
336 | 144 | } | ||
337 | 0 | 145 | ||
338 | === modified file 'friends/tests/test_twitter.py' | |||
339 | --- friends/tests/test_twitter.py 2013-12-05 20:45:19 +0000 | |||
340 | +++ friends/tests/test_twitter.py 2014-01-24 10:56:16 +0000 | |||
341 | @@ -145,7 +145,8 @@ | |||
342 | 145 | ['twitter', 88, '240556426106372096', | 145 | ['twitter', 88, '240556426106372096', |
343 | 146 | 'messages', 'Raffi Krikorian', '8285392', 'raffi', False, | 146 | 'messages', 'Raffi Krikorian', '8285392', 'raffi', False, |
344 | 147 | '2012-08-28T21:08:15Z', 'lecturing at the "analyzing big data ' | 147 | '2012-08-28T21:08:15Z', 'lecturing at the "analyzing big data ' |
346 | 148 | 'with twitter" class at @cal with @othman ' | 148 | 'with twitter" class at <a href="https://twitter.com/Cal">@Cal</a>' |
347 | 149 | ' with <a href="https://twitter.com/othman">@othman</a> ' | ||
348 | 149 | '<a href="http://twitter.com/yunorno/status/114080493036773378/photo/1">' | 150 | '<a href="http://twitter.com/yunorno/status/114080493036773378/photo/1">' |
349 | 150 | 'pic.twitter.com/rJC5Pxsu</a>', | 151 | 'pic.twitter.com/rJC5Pxsu</a>', |
350 | 151 | 'https://si0.twimg.com/profile_images/1270234259/' | 152 | 'https://si0.twimg.com/profile_images/1270234259/' |
351 | @@ -468,7 +469,7 @@ | |||
352 | 468 | expected_row = [ | 469 | expected_row = [ |
353 | 469 | 'twitter', 88, '324220250889543682', | 470 | 'twitter', 88, '324220250889543682', |
354 | 470 | 'messages', 'Robert Bruce', '836242932', 'therealrobru', True, | 471 | 'messages', 'Robert Bruce', '836242932', 'therealrobru', True, |
356 | 471 | '2013-04-16T17:58:26Z', 'RT @tarek_ziade: Just found a "Notification ' | 472 | '2013-04-16T17:58:26Z', 'RT <a href="https://twitter.com/tarek_ziade">@tarek_ziade</a>: Just found a "Notification ' |
357 | 472 | 'of Inspection" card in the bottom of my bag. looks like they were ' | 473 | 'of Inspection" card in the bottom of my bag. looks like they were ' |
358 | 473 | 'curious about those raspberry-pi :O', | 474 | 'curious about those raspberry-pi :O', |
359 | 474 | 'https://si0.twimg.com/profile_images/2631306428/' | 475 | 'https://si0.twimg.com/profile_images/2631306428/' |
360 | @@ -478,6 +479,78 @@ | |||
361 | 478 | ] | 479 | ] |
362 | 479 | self.assertEqual(list(TestModel.get_row(0)), expected_row) | 480 | self.assertEqual(list(TestModel.get_row(0)), expected_row) |
363 | 480 | 481 | ||
364 | 482 | |||
365 | 483 | @mock.patch('friends.utils.base.Model', TestModel) | ||
366 | 484 | @mock.patch('friends.utils.http.Soup.Message', | ||
367 | 485 | FakeSoupMessage('friends.tests.data', 'twitter-multiple-links.dat')) | ||
368 | 486 | @mock.patch('friends.protocols.twitter.Twitter._login', | ||
369 | 487 | return_value=True) | ||
370 | 488 | @mock.patch('friends.utils.base._seen_ids', {}) | ||
371 | 489 | def test_multiple_links(self, *mocks): | ||
372 | 490 | self.account.access_token = 'access' | ||
373 | 491 | self.account.secret_token = 'secret' | ||
374 | 492 | self.account.user_name = 'Independent' | ||
375 | 493 | self.account.auth.parameters = dict( | ||
376 | 494 | ConsumerKey='key', | ||
377 | 495 | ConsumerSecret='secret') | ||
378 | 496 | |||
379 | 497 | self.assertEqual(0, TestModel.get_n_rows()) | ||
380 | 498 | self.assertEqual( | ||
381 | 499 | self.protocol.send('some message'), | ||
382 | 500 | 'https://twitter.com/Independent/status/426318539796930560') | ||
383 | 501 | self.assertEqual(1, TestModel.get_n_rows()) | ||
384 | 502 | |||
385 | 503 | self.maxDiff = None | ||
386 | 504 | expected_row = [ | ||
387 | 505 | 'twitter', 88, '426318539796930560', | ||
388 | 506 | 'messages', 'The Independent', '16973333', 'Independent', True, | ||
389 | 507 | '2014-01-23T11:40:35Z', | ||
390 | 508 | 'An old people\'s home has recreated famous movie scenes for a wonderful calendar ' | ||
391 | 509 | '<a href="http://ind.pn/1g3wX9q">ind.pn/1g3wX9q</a> ' | ||
392 | 510 | '<a href="http://twitter.com/Independent/status/426318539796930560/photo/1">pic.twitter.com/JxQTPG7WLL</a>', | ||
393 | 511 | 'https://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d.jpeg', | ||
394 | 512 | 'https://twitter.com/Independent/status/426318539796930560', | ||
395 | 513 | 0, False, | ||
396 | 514 | 'http://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg', | ||
397 | 515 | '', '', '', '', '', '', 0.0, 0.0, | ||
398 | 516 | ] | ||
399 | 517 | self.assertEqual(list(TestModel.get_row(0)), expected_row) | ||
400 | 518 | |||
401 | 519 | @mock.patch('friends.utils.base.Model', TestModel) | ||
402 | 520 | @mock.patch('friends.utils.http.Soup.Message', | ||
403 | 521 | FakeSoupMessage('friends.tests.data', 'twitter-hashtags.dat')) | ||
404 | 522 | @mock.patch('friends.protocols.twitter.Twitter._login', | ||
405 | 523 | return_value=True) | ||
406 | 524 | @mock.patch('friends.utils.base._seen_ids', {}) | ||
407 | 525 | def test_multiple_links(self, *mocks): | ||
408 | 526 | self.account.access_token = 'access' | ||
409 | 527 | self.account.secret_token = 'secret' | ||
410 | 528 | self.account.user_name = 'Independent' | ||
411 | 529 | self.account.auth.parameters = dict( | ||
412 | 530 | ConsumerKey='key', | ||
413 | 531 | ConsumerSecret='secret') | ||
414 | 532 | |||
415 | 533 | self.assertEqual(0, TestModel.get_n_rows()) | ||
416 | 534 | self.assertEqual( | ||
417 | 535 | self.protocol.send('some message'), | ||
418 | 536 | 'https://twitter.com/Kai_Mast/status/424185261375766530') | ||
419 | 537 | self.assertEqual(1, TestModel.get_n_rows()) | ||
420 | 538 | |||
421 | 539 | self.maxDiff = None | ||
422 | 540 | expected_row = [ | ||
423 | 541 | 'twitter', 88, '424185261375766530', | ||
424 | 542 | 'messages', 'Kai Mast', '17339829', 'Kai_Mast', False, | ||
425 | 543 | '2014-01-17T14:23:41Z', | ||
426 | 544 | 'A service that filters food pictures from Instagram. ' | ||
427 | 545 | '<a href="https://twitter.com/search?q=%23MillionDollarIdea&src=hash">#MillionDollarIdea</a>', | ||
428 | 546 | 'https://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P.png', | ||
429 | 547 | 'https://twitter.com/Kai_Mast/status/424185261375766530', | ||
430 | 548 | 0, False, | ||
431 | 549 | '', | ||
432 | 550 | '', '', '', '', '', '', 0.0, 0.0, | ||
433 | 551 | ] | ||
434 | 552 | self.assertEqual(list(TestModel.get_row(0)), expected_row) | ||
435 | 553 | |||
436 | 481 | def test_unfollow(self): | 554 | def test_unfollow(self): |
437 | 482 | get_url = self.protocol._get_url = mock.Mock() | 555 | get_url = self.protocol._get_url = mock.Mock() |
438 | 483 | 556 |
Wow, congrats on the test coverage, that's really impressive ;-)
One thing that I'm a little bit confused about. You named a variable "urls_sorted" but I don't actually see any sorting going on there. Then you go on to use a concept of 'offset' in order to keep track of where each link should go, and I find it a bit sloppy.
What I'd like to see instead, is just iterate over urls_sorted in a sorted way, with descending values of 'begin' (eg, linkify the last URL first and progress backwards through the string). That way there's never an offset to have to worry about as the change in string length does not affect links that appear prior to the change being made.
Another (small) problem is that you used iter() in a useless way. Friends is python3-only, so dict.items() always returns an iter and your extra call to iter() doesn't do anything.
So instead it should probably look like this:
for key, url in sorted( urls_sorted. items() , reverse=True):
And then...
if content: [message[ :begin] , content, message[end:]])
message = ''.join(
Also, I'm slightly annoyed by the inconsistency between the _linkify_mention function which is nearly identical to other linkification techniques that are inlined in the larger function. I'm sure there can be a general purpose _linkify method that can be used to linkify all kinds of links, in a much more consistent and space-efficient manner (ie, fewer lines of code).