Merge lp:~super-friends/friends/raring into lp:friends
- raring
- Merge into trunk
Proposed by
Ken VanDine
Status: | Merged |
---|---|
Approved by: | Ken VanDine |
Approved revision: | 182 |
Merged at revision: | 164 |
Proposed branch: | lp:~super-friends/friends/raring |
Merge into: | lp:friends |
Diff against target: |
3182 lines (+1267/-632) 34 files modified
debian/changelog (+18/-0) debian/friends-dispatcher.install (+3/-0) debian/rules (+4/-1) friends/main.py (+9/-8) friends/protocols/facebook.py (+47/-20) friends/protocols/flickr.py (+5/-2) friends/protocols/foursquare.py (+6/-1) friends/protocols/twitter.py (+7/-3) friends/service/dispatcher.py (+58/-8) friends/tests/data/facebook-full.dat (+366/-1) friends/tests/data/flickr-full.dat (+13/-1) friends/tests/mocks.py (+50/-2) friends/tests/test_account.py (+3/-62) friends/tests/test_cache.py (+0/-4) friends/tests/test_dispatcher.py (+75/-10) friends/tests/test_facebook.py (+153/-72) friends/tests/test_flickr.py (+59/-76) friends/tests/test_foursquare.py (+7/-15) friends/tests/test_identica.py (+2/-10) friends/tests/test_mock_dispatcher.py (+0/-2) friends/tests/test_model.py (+11/-2) friends/tests/test_notify.py (+1/-14) friends/tests/test_protocols.py (+154/-79) friends/tests/test_shortener.py (+0/-2) friends/tests/test_twitter.py (+52/-32) friends/utils/account.py (+3/-23) friends/utils/authentication.py (+6/-1) friends/utils/base.py (+97/-143) friends/utils/model.py (+9/-20) service/configure.ac (+1/-0) service/src/Makefile.am (+3/-2) service/src/service.vala (+43/-15) setup.py (+1/-0) tools/debug_live.py (+1/-1) |
To merge this branch: | bzr merge lp:~super-friends/friends/raring |
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot (community) | continuous-integration | Approve | |
Ken VanDine | Approve | ||
Review via email: mp+154365@code.launchpad.net |
Commit message
* Stop deduplicating messages across protocols, simplifying model
schema (LP: #1156941)
* Add schema columns for latitude, longitude, and location name.
* Fix 'likes' column from gdouble to guint64.
* Add geotagging support from foursquare, facebook, flickr.
* Implement since= for Facebook, reducing bandwidth usage.
* Automatically prepend the required @mention to Twitter
replies (LP: #1156829)
* Automatically linkify URLs that get published to the model.
* Fix the publishing of Facebook Stories (LP: #1155785)
Description of the change
Merge queued up changes for raring to trunk now that the FFe bug 1156979 was approved
To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) : | # |
review:
Approve
(continuous-integration)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/changelog' |
2 | --- debian/changelog 2013-03-19 05:03:07 +0000 |
3 | +++ debian/changelog 2013-03-20 13:27:53 +0000 |
4 | @@ -1,3 +1,21 @@ |
5 | +friends (0.1.3-0ubuntu1) UNRELEASED; urgency=low |
6 | + |
7 | + [ Robert Bruce Park ] |
8 | + * Keep the Dispatcher alive for 30s beyond the return of the final |
9 | + method invocation. |
10 | + * Stop deduplicating messages across protocols, simplifying model |
11 | + schema (LP: #1156941) |
12 | + * Add schema columns for latitude, longitude, and location name. |
13 | + * Fix 'likes' column from gdouble to guint64. |
14 | + * Add geotagging support from foursquare, facebook, flickr. |
15 | + * Implement since= for Facebook, reducing bandwidth usage. |
16 | + * Automatically prepend the required @mention to Twitter |
17 | + replies (LP: #1156829) |
18 | + * Automatically linkify URLs that get published to the model. |
19 | + * Fix the publishing of Facebook Stories (LP: #1155785) |
20 | + |
21 | + -- Ken VanDine <ken.vandine@canonical.com> Wed, 20 Mar 2013 09:14:15 -0400 |
22 | + |
23 | friends (0.1.2daily13.03.19-0ubuntu1) raring; urgency=low |
24 | |
25 | [ Martin Pitt ] |
26 | |
27 | === modified file 'debian/friends-dispatcher.install' |
28 | --- debian/friends-dispatcher.install 2013-02-05 01:25:43 +0000 |
29 | +++ debian/friends-dispatcher.install 2013-03-20 13:27:53 +0000 |
30 | @@ -5,4 +5,7 @@ |
31 | usr/lib/python3/dist-packages/friends/service/* |
32 | usr/lib/python3/dist-packages/friends/shorteners/* |
33 | usr/lib/python3/dist-packages/friends/utils/* |
34 | +usr/lib/python3/dist-packages/friends/tests/mocks.py |
35 | +usr/lib/python3/dist-packages/friends/tests/__init__.py |
36 | +usr/lib/python3/dist-packages/friends/tests/data/* |
37 | usr/share/dbus-1/services/com.canonical.Friends.Dispatcher.service |
38 | |
39 | === modified file 'debian/rules' |
40 | --- debian/rules 2013-02-13 05:43:57 +0000 |
41 | +++ debian/rules 2013-03-20 13:27:53 +0000 |
42 | @@ -18,8 +18,11 @@ |
43 | override_dh_auto_install: |
44 | python3 setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb |
45 | python3 setup.py install_service_files -d $(CURDIR)/debian/tmp/usr |
46 | + rm -rf $(CURDIR)/debian/tmp/usr/lib/python3/dist-packages/friends/tests/test* |
47 | + rm -rf $(CURDIR)/debian/tmp/usr/lib/python3/dist-packages/friends/*/__pycache__ |
48 | + rm -rf $(CURDIR)/debian/tmp/usr/lib/python3/dist-packages/friends/__pycache__ |
49 | dh_auto_install -D service |
50 | - dh_install --list-missing |
51 | + dh_install --fail-missing |
52 | |
53 | override_dh_auto_test: |
54 | dbus-test-runner -t make -p check -m 300 |
55 | |
56 | === modified file 'friends/main.py' |
57 | --- friends/main.py 2013-03-01 20:28:27 +0000 |
58 | +++ friends/main.py 2013-03-20 13:27:53 +0000 |
59 | @@ -43,11 +43,16 @@ |
60 | |
61 | if args.test: |
62 | from friends.service.mock_service import Dispatcher |
63 | + from friends.tests.mocks import populate_fake_data |
64 | + |
65 | + populate_fake_data() |
66 | Dispatcher() |
67 | + |
68 | try: |
69 | loop.run() |
70 | except KeyboardInterrupt: |
71 | pass |
72 | + |
73 | sys.exit(0) |
74 | |
75 | |
76 | @@ -57,11 +62,9 @@ |
77 | GObject.threads_init(None) |
78 | |
79 | from friends.service.dispatcher import Dispatcher, DBUS_INTERFACE |
80 | -from friends.utils.base import _OperationThread, _publish_lock |
81 | -from friends.utils.base import Base, initialize_caches |
82 | +from friends.utils.base import Base, initialize_caches, _publish_lock |
83 | from friends.utils.model import Model, prune_model |
84 | from friends.utils.logging import initialize |
85 | -from friends.utils.avatar import Avatar |
86 | |
87 | |
88 | # Optional performance profiling module. |
89 | @@ -84,10 +87,6 @@ |
90 | print(class_name) |
91 | return |
92 | |
93 | - # Our threading implementation needs to know how to quit the |
94 | - # application once all threads have completed. |
95 | - _OperationThread.shutdown = loop.quit |
96 | - |
97 | # Disallow multiple instances of friends-dispatcher |
98 | bus = dbus.SessionBus() |
99 | obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') |
100 | @@ -140,7 +139,9 @@ |
101 | log.info('Starting friends-dispatcher main loop') |
102 | loop.run() |
103 | except KeyboardInterrupt: |
104 | - log.info('Stopped friends-dispatcher main loop') |
105 | + pass |
106 | + |
107 | + log.info('Stopped friends-dispatcher main loop') |
108 | |
109 | # This bit doesn't run until after the mainloop exits. |
110 | if args.performance and yappi is not None: |
111 | |
112 | === modified file 'friends/protocols/facebook.py' |
113 | --- friends/protocols/facebook.py 2013-02-27 22:22:38 +0000 |
114 | +++ friends/protocols/facebook.py 2013-03-20 13:27:53 +0000 |
115 | @@ -24,10 +24,9 @@ |
116 | import time |
117 | import logging |
118 | |
119 | -from datetime import datetime, timedelta |
120 | - |
121 | from friends.utils.avatar import Avatar |
122 | from friends.utils.base import Base, feature |
123 | +from friends.utils.cache import JsonCache |
124 | from friends.utils.http import Downloader, Uploader |
125 | from friends.utils.time import parsetime, iso8601utc |
126 | from friends.errors import FriendsError |
127 | @@ -42,10 +41,17 @@ |
128 | FACEBOOK_ADDRESS_BOOK = 'friends-facebook-contacts' |
129 | |
130 | |
131 | +TEN_DAYS = 864000 # seconds |
132 | + |
133 | + |
134 | log = logging.getLogger(__name__) |
135 | |
136 | |
137 | class Facebook(Base): |
138 | + def __init__(self, account): |
139 | + super().__init__(account) |
140 | + self._timestamps = PostIdCache(self._name + '_ids') |
141 | + |
142 | def _whoami(self, authdata): |
143 | """Identify the authenticating user.""" |
144 | me_data = Downloader( |
145 | @@ -59,15 +65,22 @@ |
146 | # We can't do much with this entry. |
147 | return |
148 | |
149 | + place = entry.get('place', {}) |
150 | + location = place.get('location', {}) |
151 | + |
152 | args = dict( |
153 | + message_id=message_id, |
154 | stream=stream, |
155 | - message=entry.get('message', ''), |
156 | + message=entry.get('message', '') or entry.get('story', ''), |
157 | icon_uri=entry.get('icon', ''), |
158 | link_picture=entry.get('picture', ''), |
159 | link_name=entry.get('name', ''), |
160 | link_url=entry.get('link', ''), |
161 | link_desc=entry.get('description', ''), |
162 | link_caption=entry.get('caption', ''), |
163 | + location=place.get('name', ''), |
164 | + latitude=location.get('latitude', 0.0), |
165 | + longitude=location.get('longitude', 0.0), |
166 | ) |
167 | |
168 | # Posts gives us a likes dict, while replies give us an int. |
169 | @@ -89,10 +102,16 @@ |
170 | # Normalize the timestamp. |
171 | timestamp = entry.get('updated_time', entry.get('created_time')) |
172 | if timestamp is not None: |
173 | - args['timestamp'] = iso8601utc(parsetime(timestamp)) |
174 | + timestamp = args['timestamp'] = iso8601utc(parsetime(timestamp)) |
175 | + # We need to record timestamps for use with since=. Note that |
176 | + # _timestamps is a special dict subclass that only accepts |
177 | + # timestamps that are larger than the existing value, so at any |
178 | + # given time it will map the stream to the most |
179 | + # recent timestamp we've seen for that stream. |
180 | + self._timestamps[stream] = timestamp |
181 | |
182 | # Publish this message into the SharedModel. |
183 | - self._publish(message_id, **args) |
184 | + self._publish(**args) |
185 | |
186 | # If there are any replies, publish them as well. |
187 | for comment in entry.get('comments', {}).get('data', []): |
188 | @@ -109,6 +128,7 @@ |
189 | |
190 | while True: |
191 | response = Downloader(url, params).get_json() |
192 | + |
193 | if self._is_error(response): |
194 | break |
195 | |
196 | @@ -137,24 +157,18 @@ |
197 | # We've gotten everything Facebook is going to give us. |
198 | return entries |
199 | |
200 | - def _get(self, url, stream, since=None): |
201 | + def _get(self, url, stream): |
202 | """Retrieve a list of Facebook objects. |
203 | |
204 | A maximum of 50 objects are requested. |
205 | - |
206 | - :param since: Only get objects posted since this date. If not given, |
207 | - then only objects younger than 10 days are retrieved. The value |
208 | - is a number seconds since the epoch. |
209 | - :type since: float |
210 | """ |
211 | access_token = self._get_access_token() |
212 | - if since is None: |
213 | - when = datetime.now() - timedelta(days=10) |
214 | - else: |
215 | - when = datetime.fromtimestamp(since) |
216 | + since = self._timestamps.get( |
217 | + stream, iso8601utc(int(time.time()) - TEN_DAYS)) |
218 | + |
219 | entries = [] |
220 | params = dict(access_token=access_token, |
221 | - since=when.isoformat(), |
222 | + since=since, |
223 | limit=self._DOWNLOAD_LIMIT) |
224 | |
225 | entries = self._follow_pagination(url, params) |
226 | @@ -163,15 +177,15 @@ |
227 | self._publish_entry(entry, stream=stream) |
228 | |
229 | @feature |
230 | - def home(self, since=None): |
231 | + def home(self): |
232 | """Gather and publish public timeline messages.""" |
233 | - self._get(ME_URL + '/home', 'messages', since) |
234 | + self._get(ME_URL + '/home', 'messages') |
235 | return self._get_n_rows() |
236 | |
237 | @feature |
238 | - def wall(self, since=None): |
239 | + def wall(self): |
240 | """Gather and publish messages written on user's wall.""" |
241 | - self._get(ME_URL + '/feed', 'mentions', since) |
242 | + self._get(ME_URL + '/feed', 'mentions') |
243 | return self._get_n_rows() |
244 | |
245 | @feature |
246 | @@ -369,3 +383,16 @@ |
247 | def delete_contacts(self): |
248 | source = self._get_eds_source(FACEBOOK_ADDRESS_BOOK) |
249 | return self._delete_service_contacts(source) |
250 | + |
251 | + |
252 | +class PostIdCache(JsonCache): |
253 | + """Persist most-recent timestamps as JSON.""" |
254 | + |
255 | + def __setitem__(self, key, value): |
256 | + if key.find('/') >= 0: |
257 | + # Don't flood the cache with irrelevant "reply_to/..." and |
258 | + # "search/..." streams, we only need the main streams. |
259 | + return |
260 | + # Thank SCIENCE for lexically-sortable timestamp strings! |
261 | + if value > self.get(key, ''): |
262 | + JsonCache.__setitem__(self, key, value) |
263 | |
264 | === modified file 'friends/protocols/flickr.py' |
265 | --- friends/protocols/flickr.py 2013-02-27 23:04:36 +0000 |
266 | +++ friends/protocols/flickr.py 2013-03-20 13:27:53 +0000 |
267 | @@ -113,7 +113,7 @@ |
268 | method='flickr.photos.getContactsPhotos', |
269 | format='json', |
270 | nojsoncallback='1', |
271 | - extras='date_upload,owner_name,icon_server', |
272 | + extras='date_upload,owner_name,icon_server,geo', |
273 | ) |
274 | |
275 | response = self._get_url(args) |
276 | @@ -166,7 +166,10 @@ |
277 | link_caption=data.get('title', ''), |
278 | link_url=img_url, |
279 | link_picture=img_src, |
280 | - link_icon=img_thumb) |
281 | + link_icon=img_thumb, |
282 | + latitude=data.get('latitude', 0.0), |
283 | + longitude=data.get('longitude', 0.0), |
284 | + ) |
285 | return self._get_n_rows() |
286 | |
287 | # http://www.flickr.com/services/api/upload.api.html |
288 | |
289 | === modified file 'friends/protocols/foursquare.py' |
290 | --- friends/protocols/foursquare.py 2013-02-26 19:05:10 +0000 |
291 | +++ friends/protocols/foursquare.py 2013-03-20 13:27:53 +0000 |
292 | @@ -85,6 +85,8 @@ |
293 | checkin_id = checkin.get('id', '') |
294 | tz_offset = checkin.get('timeZoneOffset', 0) |
295 | epoch = checkin.get('createdAt', 0) |
296 | + venue = checkin.get('venue', {}) |
297 | + location = venue.get('location', {}) |
298 | self._publish( |
299 | message_id=checkin_id, |
300 | stream='messages', |
301 | @@ -94,6 +96,9 @@ |
302 | message=checkin.get('shout', ''), |
303 | likes=checkin.get('likes', {}).get('count', 0), |
304 | icon_uri=Avatar.get_image(avatar_url), |
305 | - url=checkin.get('venue', {}).get('canonicalUrl', ''), |
306 | + url=venue.get('canonicalUrl', ''), |
307 | + location=venue.get('name', ''), |
308 | + latitude=location.get('lat', 0.0), |
309 | + longitude=location.get('lng', 0.0), |
310 | ) |
311 | return self._get_n_rows() |
312 | |
313 | === modified file 'friends/protocols/twitter.py' |
314 | --- friends/protocols/twitter.py 2013-03-08 02:31:15 +0000 |
315 | +++ friends/protocols/twitter.py 2013-03-20 13:27:53 +0000 |
316 | @@ -26,12 +26,10 @@ |
317 | import logging |
318 | |
319 | from urllib.parse import quote |
320 | -from gi.repository import GLib |
321 | |
322 | from friends.utils.avatar import Avatar |
323 | from friends.utils.base import Base, feature |
324 | from friends.utils.cache import JsonCache |
325 | -from friends.utils.model import Model |
326 | from friends.utils.http import BaseRateLimiter, Downloader |
327 | from friends.utils.time import parsetime, iso8601utc |
328 | from friends.errors import FriendsError |
329 | @@ -70,7 +68,7 @@ |
330 | super().__init__(account) |
331 | self._rate_limiter = RateLimiter() |
332 | # Can be 'twitter_ids' or 'identica_ids' |
333 | - self._tweet_ids = TweetIdCache(self.__class__.__name__.lower() + '_ids') |
334 | + self._tweet_ids = TweetIdCache(self._name + '_ids') |
335 | |
336 | def _whoami(self, authdata): |
337 | """Identify the authenticating user.""" |
338 | @@ -254,6 +252,12 @@ |
339 | order for Twitter to actually accept this as a reply. Otherwise it |
340 | will just be an ordinary tweet. |
341 | """ |
342 | + try: |
343 | + sender = '@{}'.format(self._fetch_cell(message_id, 'sender_nick')) |
344 | + if message.find(sender) < 0: |
345 | + message = sender + ' ' + message |
346 | + except FriendsError: |
347 | + pass |
348 | url = self._api_base.format(endpoint='statuses/update') |
349 | tweet = self._get_url(url, dict(in_reply_to_status_id=message_id, |
350 | status=message)) |
351 | |
352 | === modified file 'friends/service/dispatcher.py' |
353 | --- friends/service/dispatcher.py 2013-02-20 13:08:27 +0000 |
354 | +++ friends/service/dispatcher.py 2013-03-20 13:27:53 +0000 |
355 | @@ -28,12 +28,13 @@ |
356 | import dbus.service |
357 | |
358 | from gi.repository import GLib |
359 | +from contextlib import ContextDecorator |
360 | |
361 | from friends.utils.avatar import Avatar |
362 | from friends.utils.account import AccountManager |
363 | from friends.utils.manager import protocol_manager |
364 | from friends.utils.menus import MenuManager |
365 | -from friends.utils.model import Model |
366 | +from friends.utils.model import Model, persist_model |
367 | from friends.shorteners import lookup |
368 | |
369 | |
370 | @@ -43,6 +44,48 @@ |
371 | STUB = lambda *ignore, **kwignore: None |
372 | |
373 | |
374 | +# Avoid race condition during shut-down |
375 | +_exit_lock = threading.Lock() |
376 | + |
377 | + |
378 | +class ManageTimers(ContextDecorator): |
379 | + """Exit the dispatcher 30s after the most recent method call returns.""" |
380 | + timers = set() |
381 | + callback = STUB |
382 | + |
383 | + def __enter__(self): |
384 | + self.clear_all_timers() |
385 | + |
386 | + def __exit__(self, *ignore): |
387 | + self.set_new_timer() |
388 | + |
389 | + def clear_all_timers(self): |
390 | + log.debug('Clearing {} shutdown timer(s)...'.format(len(self.timers))) |
391 | + while self.timers: |
392 | + GLib.source_remove(self.timers.pop()) |
393 | + |
394 | + def set_new_timer(self): |
395 | + # Concurrency will cause two methods to exit near each other, |
396 | + # causing two timers to be set, so we have to clear them again. |
397 | + self.clear_all_timers() |
398 | + log.debug('Starting new shutdown timer...') |
399 | + self.timers.add(GLib.timeout_add_seconds(30, self.terminate)) |
400 | + |
401 | + def terminate(self, *ignore): |
402 | + """Exit the dispatcher, but only if there are no active subthreads.""" |
403 | + with _exit_lock: |
404 | + if threading.activeCount() < 2: |
405 | + log.debug('No threads found, shutting down.') |
406 | + persist_model() |
407 | + self.timers.add(GLib.idle_add(self.callback)) |
408 | + else: |
409 | + log.debug('Delaying shutdown because active threads found.') |
410 | + self.set_new_timer() |
411 | + |
412 | + |
413 | +exit_after_idle = ManageTimers() |
414 | + |
415 | + |
416 | class Dispatcher(dbus.service.Object): |
417 | """This is the primary handler of dbus method calls.""" |
418 | __dbus_object_path__ = '/com/canonical/friends/Dispatcher' |
419 | @@ -59,10 +102,13 @@ |
420 | self.menu_manager = MenuManager(self.Refresh, self.mainloop.quit) |
421 | Model.connect('row-added', self._increment_unread_count) |
422 | |
423 | + ManageTimers.callback = mainloop.quit |
424 | + |
425 | def _increment_unread_count(self, model, itr): |
426 | self._unread_count += 1 |
427 | self.menu_manager.update_unread_count(self._unread_count) |
428 | |
429 | + @exit_after_idle |
430 | @dbus.service.method(DBUS_INTERFACE) |
431 | def Refresh(self): |
432 | """Download new messages from each connected protocol.""" |
433 | @@ -80,6 +126,7 @@ |
434 | # If a protocol doesn't support receive then ignore it. |
435 | pass |
436 | |
437 | + @exit_after_idle |
438 | @dbus.service.method(DBUS_INTERFACE) |
439 | def ClearIndicators(self): |
440 | """Indicate that messages have been read. |
441 | @@ -92,8 +139,8 @@ |
442 | service.ClearIndicators() |
443 | """ |
444 | self.menu_manager.update_unread_count(0) |
445 | - GLib.idle_add(self.mainloop.quit) |
446 | |
447 | + @exit_after_idle |
448 | @dbus.service.method(DBUS_INTERFACE, |
449 | in_signature='sss', |
450 | out_signature='s', |
451 | @@ -117,7 +164,7 @@ |
452 | """ |
453 | if account_id: |
454 | accounts = [self.account_manager.get(account_id)] |
455 | - if accounts == [None]: |
456 | + if None in accounts: |
457 | message = 'Could not find account: {}'.format(account_id) |
458 | failure(message) |
459 | log.error(message) |
460 | @@ -138,6 +185,7 @@ |
461 | if not called: |
462 | failure('No accounts supporting {} found.'.format(action)) |
463 | |
464 | + @exit_after_idle |
465 | @dbus.service.method(DBUS_INTERFACE, |
466 | in_signature='s', |
467 | out_signature='s', |
468 | @@ -162,7 +210,7 @@ |
469 | sent = True |
470 | log.debug( |
471 | 'Sending message to {}'.format( |
472 | - account.protocol.__class__.__name__)) |
473 | + account.protocol._Name)) |
474 | account.protocol( |
475 | 'send', |
476 | message, |
477 | @@ -172,6 +220,7 @@ |
478 | if not sent: |
479 | failure('No send_enabled accounts found.') |
480 | |
481 | + @exit_after_idle |
482 | @dbus.service.method(DBUS_INTERFACE, |
483 | in_signature='sss', |
484 | out_signature='s', |
485 | @@ -205,6 +254,7 @@ |
486 | failure(message) |
487 | log.error(message) |
488 | |
489 | + @exit_after_idle |
490 | @dbus.service.method(DBUS_INTERFACE, |
491 | in_signature='sss', |
492 | out_signature='s', |
493 | @@ -262,6 +312,7 @@ |
494 | failure(message) |
495 | log.error(message) |
496 | |
497 | + @exit_after_idle |
498 | @dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s') |
499 | def GetFeatures(self, protocol_name): |
500 | """Returns a list of features supported by service as json string. |
501 | @@ -274,9 +325,9 @@ |
502 | features = json.loads(service.GetFeatures('facebook')) |
503 | """ |
504 | protocol = protocol_manager.protocols.get(protocol_name) |
505 | - GLib.idle_add(self.mainloop.quit) |
506 | - return json.dumps(protocol.get_features()) |
507 | + return json.dumps(protocol.get_features() if protocol else []) |
508 | |
509 | + @exit_after_idle |
510 | @dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s') |
511 | def URLShorten(self, url): |
512 | """Shorten a URL. |
513 | @@ -291,7 +342,6 @@ |
514 | service = dbus.Interface(obj, DBUS_INTERFACE) |
515 | short_url = service.URLShorten(url) |
516 | """ |
517 | - GLib.idle_add(self.mainloop.quit) |
518 | service_name = self.settings.get_string('urlshorter') |
519 | log.info('Shortening URL {} with {}'.format(url, service_name)) |
520 | if (lookup.is_shortened(url) or |
521 | @@ -305,7 +355,7 @@ |
522 | log.exception('URL shortening class: {}'.format(service)) |
523 | return url |
524 | |
525 | + @exit_after_idle |
526 | @dbus.service.method(DBUS_INTERFACE) |
527 | def ExpireAvatars(self): |
528 | Avatar.expire_old_avatars() |
529 | - GLib.idle_add(self.mainloop.quit) |
530 | |
531 | === modified file 'friends/tests/data/facebook-full.dat' |
532 | --- friends/tests/data/facebook-full.dat 2012-10-13 01:27:15 +0000 |
533 | +++ friends/tests/data/facebook-full.dat 2013-03-20 13:27:53 +0000 |
534 | @@ -1,1 +1,366 @@ |
535 | -{"paging": {"previous": "https://graph.facebook.com/101/home?access_token=ABC&limit=25&since=1348682101&__previous=1"}, "data": [{"picture": "https://fbexternal-a.akamaihd.net/rush.jpg", "from": {"category": "Entertainment", "name": "Rush is a Band", "id": "117402931676347"}, "name": "Rush is a Band Blog", "comments": {"count": 0}, "actions": [{"link": "https://www.facebook.com/117402931676347/posts/287578798009078", "name": "Comment"}, {"link": "https://www.facebook.com/117402931676347/posts/287578798009078", "name": "Like"}], "updated_time": "2012-09-26T17:34:00+0000", "caption": "www.rushisaband.com", "link": "http://www.rushisaband.com/blog/Rush-Clockwork-Angels-tour", "likes": {"count": 16, "data": [{"name": "Alex Lifeson", "id": "801"}, {"name": "Vlada Lee", "id": "801"}, {"name": "Richard Peart", "id": "803"}, {"name": "Eric Lifeson", "id": "804"}]}, "created_time": "2012-09-26T17:34:00+0000", "message": "Rush takes off to the Great White North", "icon": "https://s-static.ak.facebook.com/rsrc.php/v2/yD/r/a.gif", "type": "link", "id": "108", "status_type": "shared_story", "description": "Rush is a Band: Neil Peart, Geddy Lee, Alex Lifeson"}, {"picture": "https://images.gibson.com/Rush_Clockwork-Angels_t.jpg", "likes": {"count": 27, "data": [{"name": "Tracy Lee", "id": "805"}, {"name": "Wendy Peart", "id": "806"}, {"name": "Vlada Lifeson", "id": "807"}, {"name": "Chevy Lee", "id": "808"}]}, "from": {"category": "Entertainment", "name": "Rush is a Band", "id": "117402931676347"}, "name": "Top 10 Alex Lifeson Guitar Moments", "comments": {"count": 5, "data": [{"created_time": "2012-09-26T17:16:00+0000", "message": "OK Don...10) Headlong Flight", "from": {"name": "Bruce Peart", "id": "809"}, "id": "117402931676347_386054134801436_3235476"}, {"created_time": "2012-09-26T17:49:06+0000", "message": "No Cygnus X-1 Bruce? I call shenanigans!", "from": {"name": "Don Lee", "id": "810"}, "id": "117402931676347_386054134801436_3235539"}]}, "actions": [{"link": "https://www.facebook.com/117402931676347/posts/386054134801436", "name": "Comment"}, {"link": "https://www.facebook.com/117402931676347/posts/386054134801436", "name": "Like"}], "updated_time": "2012-09-26T17:49:06+0000", "caption": "www2.gibson.com", "link": "http://www2.gibson.com/Alex-Lifeson.aspx", "shares": {"count": 11}, "created_time": "2012-09-26T16:42:15+0000", "message": "http://www2.gibson.com/Alex-Lifeson-0225-2011.aspx", "icon": "https://s-static.ak.facebook.com/rsrc.php/v2/yD/r/a.gif", "type": "link", "id": "109", "status_type": "shared_story", "description": "For millions of Rush fans old and new, it\u2019s a pleasure"}]} |
536 | \ No newline at end of file |
537 | +{ |
538 | + "data": [ |
539 | + { |
540 | + "id": "fake_id", |
541 | + "from": { |
542 | + "name": "Yours Truly", |
543 | + "id": "56789" |
544 | + }, |
545 | + "message": "Writing code that supports geotagging data from facebook. If y'all could make some geotagged facebook posts for me to test with, that'd be super.", |
546 | + "actions": [ |
547 | + { |
548 | + "name": "Comment", |
549 | + "link": "https://www.facebook.com/fake/posts/id" |
550 | + }, |
551 | + { |
552 | + "name": "Like", |
553 | + "link": "https://www.facebook.com/fake/posts/id" |
554 | + } |
555 | + ], |
556 | + "privacy": { |
557 | + "value": "" |
558 | + }, |
559 | + "place": { |
560 | + "id": "103135879727382", |
561 | + "name": "Victoria, British Columbia", |
562 | + "location": { |
563 | + "street": "", |
564 | + "zip": "", |
565 | + "latitude": 48.4333, |
566 | + "longitude": -123.35 |
567 | + } |
568 | + }, |
569 | + "type": "status", |
570 | + "status_type": "mobile_status_update", |
571 | + "created_time": "2013-03-12T21:27:56+0000", |
572 | + "updated_time": "2013-03-13T23:29:07+0000", |
573 | + "likes": { |
574 | + "data": [ |
575 | + { |
576 | + "name": "Anna", |
577 | + "id": "12345" |
578 | + } |
579 | + ], |
580 | + "count": 1 |
581 | + }, |
582 | + "comments": { |
583 | + "data": [ |
584 | + { |
585 | + "id": "fake as a snake", |
586 | + "from": { |
587 | + "name": "Grandma", |
588 | + "id": "9876" |
589 | + }, |
590 | + "message": "If I knew what a geotagged facebook post was I might be able to comply!", |
591 | + "created_time": "2013-03-12T22:56:17+0000" |
592 | + }, |
593 | + { |
594 | + "id": "faker than cake!", |
595 | + "from": { |
596 | + "name": "Father", |
597 | + "id": "234" |
598 | + }, |
599 | + "message": "don't know how", |
600 | + "created_time": "2013-03-12T23:29:45+0000" |
601 | + }, |
602 | + { |
603 | + "id": "still fake", |
604 | + "from": { |
605 | + "name": "Mother", |
606 | + "id": "456" |
607 | + }, |
608 | + "message": "HUH!!!!", |
609 | + "created_time": "2013-03-13T02:20:27+0000" |
610 | + }, |
611 | + { |
612 | + "id": "this one is real", |
613 | + "from": { |
614 | + "name": "Yours Truly", |
615 | + "id": "56789" |
616 | + }, |
617 | + "message": "Coming up with tons of fake data is hard!", |
618 | + "created_time": "2013-03-13T23:29:07+0000" |
619 | + } |
620 | + ], |
621 | + "count": 4 |
622 | + } |
623 | + }, |
624 | + { |
625 | + "id": "270843027745_10151370303782746", |
626 | + "from": { |
627 | + "category": "Shopping/retail", |
628 | + "category_list": [ |
629 | + { |
630 | + "id": "128003127270269", |
631 | + "name": "Bike Shop" |
632 | + } |
633 | + ], |
634 | + "name": "Western Cycle Source for Sports", |
635 | + "id": "270843027745" |
636 | + }, |
637 | + "story": "Western Cycle Source for Sports updated their cover photo.", |
638 | + "story_tags": { |
639 | + "0": [ |
640 | + { |
641 | + "id": "270843027745", |
642 | + "name": "Western Cycle Source for Sports", |
643 | + "offset": 0, |
644 | + "length": 31, |
645 | + "type": "page" |
646 | + } |
647 | + ] |
648 | + }, |
649 | + "picture": "https://fbcdn-photos-a.akamaihd.net/hphotos-ak-snc7/482418_10151370303672746_1924798223_s.jpg", |
650 | + "link": "https://www.facebook.com/photo.php?fbid=10151370303672746&set=a.10150598301902746.381693.270843027745&type=1&relevant_count=1", |
651 | + "icon": "https://fbstatic-a.akamaihd.net/rsrc.php/v2/yz/r/StEh3RhPvjk.gif", |
652 | + "actions": [ |
653 | + { |
654 | + "name": "Comment", |
655 | + "link": "https://www.facebook.com/270843027745/posts/10151370303782746" |
656 | + }, |
657 | + { |
658 | + "name": "Like", |
659 | + "link": "https://www.facebook.com/270843027745/posts/10151370303782746" |
660 | + } |
661 | + ], |
662 | + "privacy": { |
663 | + "value": "" |
664 | + }, |
665 | + "place": { |
666 | + "id": "270843027745", |
667 | + "name": "Western Cycle Source for Sports", |
668 | + "location": { |
669 | + "street": "1550 8th Ave", |
670 | + "city": "Regina", |
671 | + "state": "SK", |
672 | + "country": "Canada", |
673 | + "zip": "S4R 1E4", |
674 | + "latitude": 50.45679, |
675 | + "longitude": -104.60276 |
676 | + } |
677 | + }, |
678 | + "type": "photo", |
679 | + "object_id": "10151370303672746", |
680 | + "created_time": "2013-03-11T23:46:06+0000", |
681 | + "updated_time": "2013-03-11T23:46:06+0000", |
682 | + "likes": { |
683 | + "data": [ |
684 | + { |
685 | + "name": "Lou Schwindt", |
686 | + "id": "57" |
687 | + }, |
688 | + { |
689 | + "name": "Maureen Daniel", |
690 | + "id": "72" |
691 | + }, |
692 | + { |
693 | + "name": "Lee Watson", |
694 | + "id": "696" |
695 | + }, |
696 | + { |
697 | + "name": "Rob Nelson", |
698 | + "id": "40" |
699 | + } |
700 | + ], |
701 | + "count": 10 |
702 | + }, |
703 | + "comments": { |
704 | + "count": 0 |
705 | + } |
706 | + }, |
707 | + { |
708 | + "id": "161247843901324_629147610444676", |
709 | + "from": { |
710 | + "category": "Hotel", |
711 | + "category_list": [ |
712 | + { |
713 | + "id": "164243073639257", |
714 | + "name": "Hotel" |
715 | + } |
716 | + ], |
717 | + "name": "Best Western Denver Southwest", |
718 | + "id": "161247843901324" |
719 | + }, |
720 | + "message": "Today only -- Come meet Caroline and Meredith and Stanley the Stegosaurus (& Greg & Joe, too!) at the TechZulu Trend Lounge, Hilton Garden Inn 18th floor, 500 N Interstate 35, Austin, Texas. Monday, March 11th, 4:00pm to 7:00 pm. Also here Hannah Hart (My Drunk Kitchen) and Angry Video Game Nerd producer, Sean Keegan. Stanley is in the lobby.", |
721 | + "picture": "https://fbcdn-photos-a.akamaihd.net/hphotos-ak-snc7/601266_629147587111345_968504279_s.jpg", |
722 | + "link": "https://www.facebook.com/photo.php?fbid=629147587111345&set=a.173256162700492.47377.161247843901324&type=1&relevant_count=1", |
723 | + "icon": "https://fbstatic-a.akamaihd.net/rsrc.php/v2/yz/r/StEh3RhPvjk.gif", |
724 | + "actions": [ |
725 | + { |
726 | + "name": "Comment", |
727 | + "link": "https://www.facebook.com/161247843901324/posts/629147610444676" |
728 | + }, |
729 | + { |
730 | + "name": "Like", |
731 | + "link": "https://www.facebook.com/161247843901324/posts/629147610444676" |
732 | + } |
733 | + ], |
734 | + "privacy": { |
735 | + "value": "" |
736 | + }, |
737 | + "place": { |
738 | + "id": "132709090079327", |
739 | + "name": "Hilton Garden Inn Austin Downtown/Convention Center", |
740 | + "location": { |
741 | + "street": "500 North Interstate 35", |
742 | + "city": "Austin", |
743 | + "state": "TX", |
744 | + "country": "United States", |
745 | + "zip": "78701", |
746 | + "latitude": 30.265384957204, |
747 | + "longitude": -97.735604602521 |
748 | + } |
749 | + }, |
750 | + "type": "photo", |
751 | + "status_type": "added_photos", |
752 | + "object_id": "629147587111345", |
753 | + "created_time": "2013-03-11T20:49:10+0000", |
754 | + "updated_time": "2013-03-11T23:51:25+0000", |
755 | + "likes": { |
756 | + "data": [ |
757 | + { |
758 | + "name": "Andrew Henninger", |
759 | + "id": "11" |
760 | + }, |
761 | + { |
762 | + "name": "Sarah Brents", |
763 | + "id": "22" |
764 | + }, |
765 | + { |
766 | + "name": "Thomas Bush", |
767 | + "id": "33" |
768 | + }, |
769 | + { |
770 | + "name": "Jennifer Tornetta", |
771 | + "id": "44" |
772 | + } |
773 | + ], |
774 | + "count": 84 |
775 | + }, |
776 | + "comments": { |
777 | + "data": [ |
778 | + { |
779 | + "id": "1612484301324_6294760444676_1126376", |
780 | + "from": { |
781 | + "name": "Amy Gibbs", |
782 | + "id": "55" |
783 | + }, |
784 | + "message": "You have to love a family that travels with their stegasaurus.", |
785 | + "created_time": "2013-03-11T23:51:05+0000", |
786 | + "likes": 2 |
787 | + }, |
788 | + { |
789 | + "id": "1612843901324_6294761044676_1124378", |
790 | + "from": { |
791 | + "name": "Amy Gibbs", |
792 | + "id": "55" |
793 | + }, |
794 | + "message": "*stegosaurus...sorry!", |
795 | + "created_time": "2013-03-11T23:51:25+0000", |
796 | + "likes": 1 |
797 | + } |
798 | + ], |
799 | + "count": 11 |
800 | + } |
801 | + }, |
802 | + { |
803 | + "id": "104443_100085049977", |
804 | + "from": { |
805 | + "name": "Guy Frenchie", |
806 | + "id": "1244414" |
807 | + }, |
808 | + "story": "Guy Frenchie did some things with some stuff.", |
809 | + "story_tags": { |
810 | + "0": [ |
811 | + { |
812 | + "id": "1244414", |
813 | + "name": "Guy Frenchie", |
814 | + "offset": 0, |
815 | + "length": 16, |
816 | + "type": "user" |
817 | + } |
818 | + ], |
819 | + "26": [ |
820 | + { |
821 | + "id": "37067557", |
822 | + "name": "somebody", |
823 | + "offset": 26, |
824 | + "length": 10, |
825 | + "type": "page" |
826 | + } |
827 | + ], |
828 | + "48": [ |
829 | + { |
830 | + "id": "50681138", |
831 | + "name": "What do you think about things and stuff?", |
832 | + "offset": 48, |
833 | + "length": 52 |
834 | + } |
835 | + ] |
836 | + }, |
837 | + "icon": "https://fbstatic-a.akamaihd.net/rsrc.php/v2/yg/r/5PpICR5KcPe.png", |
838 | + "actions": [ |
839 | + { |
840 | + "name": "Comment", |
841 | + "link": "https://www.facebook.com/1244414/posts/100085049977" |
842 | + }, |
843 | + { |
844 | + "name": "Like", |
845 | + "link": "https://www.facebook.com/1244414/posts/100085049977" |
846 | + } |
847 | + ], |
848 | + "privacy": { |
849 | + "value": "" |
850 | + }, |
851 | + "type": "question", |
852 | + "object_id": "584616119", |
853 | + "application": { |
854 | + "name": "Questions", |
855 | + "id": "101502535258" |
856 | + }, |
857 | + "created_time": "2013-03-15T19:57:14+0000", |
858 | + "updated_time": "2013-03-15T19:57:14+0000", |
859 | + "likes": { |
860 | + "data": [ |
861 | + { |
862 | + "name": "Kevin Diner", |
863 | + "id": "55520" |
864 | + }, |
865 | + { |
866 | + "name": "Bozo the Clown", |
867 | + "id": "13960" |
868 | + } |
869 | + ], |
870 | + "count": 3 |
871 | + }, |
872 | + "comments": { |
873 | + "data": [ |
874 | + { |
875 | + "id": "14446143_102008355988977_100927", |
876 | + "from": { |
877 | + "name": "Seymour Butts", |
878 | + "id": "505677" |
879 | + }, |
880 | + "message": "seems legit", |
881 | + "created_time": "2013-03-13T12:20:19+0000", |
882 | + "likes": 2 |
883 | + }, |
884 | + { |
885 | + "id": "120143_1020035588977_1019440", |
886 | + "from": { |
887 | + "name": "Andre the Giant", |
888 | + "id": "100390199" |
889 | + }, |
890 | + "message": "Anybody want a peanut?", |
891 | + "created_time": "2013-03-13T12:23:25+0000" |
892 | + } |
893 | + ], |
894 | + "count": 22 |
895 | + } |
896 | + } |
897 | + ], |
898 | + "paging": { |
899 | + "previous": "https://graph.facebook.com/me/home&limit=25&since=1234", |
900 | + "next": "https://graph.facebook.com/me/home&limit=25&until=4321" |
901 | + } |
902 | +} |
903 | |
904 | === modified file 'friends/tests/data/flickr-full.dat' |
905 | --- friends/tests/data/flickr-full.dat 2012-10-20 15:35:30 +0000 |
906 | +++ friends/tests/data/flickr-full.dat 2013-03-20 13:27:53 +0000 |
907 | @@ -1,1 +1,13 @@ |
908 | -{"photos": {"photo": [{"username": "Geddy Lee", "secret": "abc", "title": "ant", "owner": "123", "id": "801", "dateupload": "2012-05-10T13:36:45", "server": "1"}, {"username": "Alex Lifeson", "secret": "def", "ownername": "Alex Lifeson", "title": "bee", "owner": "456", "id": "802", "server": "1"}, {"username": "Neil Peart", "title": "cat", "farm": "animalz", "server": "1", "iconserver": "9", "secret": "ghi", "ownername": "Bob Dobbs", "owner": "789", "id": "803", "iconfarm": "iconz"}]}} |
909 | +{ "photos": { |
910 | + "photo": [ |
911 | + { "id": "8552892154", "secret": "a", "server": "8378", "farm": 9, "owner": "47303164@N00", "username": "raise my voice", "title": "Chocolate chai #yegcoffee", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363117902", "ownername": "raise my voice", "iconserver": 93, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
912 | + { "id": "8552845358", "secret": "b", "server": "8085", "farm": 9, "owner": "47303164@N00", "username": "raise my voice", "title": "Torah ark #yegjew", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363116818", "ownername": "raise my voice", "iconserver": 93, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
913 | + { "id": "8552661200", "secret": "c", "server": "8522", "farm": 9, "owner": "60551783@N00", "username": "Reinhard.Pantke", "title": "Henningsvaer", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363112533", "ownername": "Reinhard.Pantke", "iconserver": 8, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
914 | + { "id": "8550946245", "secret": "d", "server": "8107", "farm": 9, "owner": "60551783@N00", "username": "Reinhard.Pantke", "title": "Summerfeeling on Lofoten", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363098878", "ownername": "Reinhard.Pantke", "iconserver": 8, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
915 | + { "id": "8550829193", "secret": "e", "server": "8246", "farm": 9, "owner": "27204141@N05", "username": "Nelson Webb", "title": "St. Michael - The Archangel", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363096450", "ownername": "Nelson Webb", "iconserver": "2047", "iconfarm": 3, "latitude": 53.833156, "longitude": -112.330784, "accuracy": 15, "context": 0, "place_id": "4Y55lnhZVrNO", "woeid": "8496", "geo_is_family": 0, "geo_is_friend": 0, "geo_is_contact": 0, "geo_is_public": 1 }, |
916 | + { "id": "8551930826", "secret": "f", "server": "8247", "farm": 9, "owner": "27204141@N05", "username": "Nelson Webb", "title": "Pine scented air freshener", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363096449", "ownername": "Nelson Webb", "iconserver": "2047", "iconfarm": 3, "latitude": 53.878136, "longitude": -112.335162, "accuracy": 15, "context": 0, "place_id": "4Y55lnhZVrNO", "woeid": "8496", "geo_is_family": 0, "geo_is_friend": 0, "geo_is_contact": 0, "geo_is_public": 1 }, |
917 | + { "id": "8549658873", "secret": "g", "server": "8239", "farm": 9, "owner": "30584843@N00", "username": "Mark Iocchelli", "title": "Sleepy Hollow", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363055714", "ownername": "Mark Iocchelli", "iconserver": 22, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
918 | + { "id": "8548811967", "secret": "h", "server": "8229", "farm": 9, "owner": "47303164@N00", "username": "raise my voice", "title": "Trying out The Wokkery #yegfood", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363028798", "ownername": "raise my voice", "iconserver": 93, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
919 | + { "id": "8548753789", "secret": "i", "server": "8512", "farm": 9, "owner": "30584843@N00", "username": "Mark Iocchelli", "title": "Alberta Rail Pipeline", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363027071", "ownername": "Mark Iocchelli", "iconserver": 22, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }, |
920 | + { "id": "8549607582", "secret": "j", "server": "8087", "farm": 9, "owner": "60404254@N00", "username": "tavis_mcnally", "title": "26 weeks", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363022277", "ownername": "tavis_mcnally", "iconserver": "2182", "iconfarm": 3, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 } |
921 | + ], "total": "290", "page": 1, "per_page": 10, "pages": 29 }, "stat": "ok" } |
922 | |
923 | === modified file 'friends/tests/mocks.py' |
924 | --- friends/tests/mocks.py 2013-02-19 17:00:41 +0000 |
925 | +++ friends/tests/mocks.py 2013-03-20 13:27:53 +0000 |
926 | @@ -30,15 +30,19 @@ |
927 | import hashlib |
928 | import logging |
929 | import threading |
930 | +import tempfile |
931 | +import shutil |
932 | |
933 | from io import StringIO |
934 | from logging.handlers import QueueHandler |
935 | from pkg_resources import resource_listdir, resource_string |
936 | from queue import Empty, Queue |
937 | from urllib.parse import urlsplit |
938 | +from gi.repository import Dee |
939 | |
940 | from friends.utils.base import Base |
941 | from friends.utils.logging import LOG_FORMAT |
942 | +from friends.utils.model import COLUMN_TYPES |
943 | |
944 | |
945 | try: |
946 | @@ -51,6 +55,50 @@ |
947 | NEWLINE = '\n' |
948 | |
949 | |
950 | +# Create a test model that will not interfere with the user's environment. |
951 | +# We'll use this object as a mock of the real model. |
952 | +TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
953 | +TestModel.set_schema_full(COLUMN_TYPES) |
954 | + |
955 | + |
956 | +@mock.patch('friends.utils.http._soup', mock.Mock()) |
957 | +@mock.patch('friends.utils.base.Model', TestModel) |
958 | +@mock.patch('friends.utils.base.Base._get_access_token', |
959 | + mock.Mock(return_value='Access Tolkien')) |
960 | +def populate_fake_data(): |
961 | + """Dump a mixture of random data from our testsuite into TestModel. |
962 | + |
963 | + This is invoked by running 'friends-dispatcher --test' so that you |
964 | + can have some phony data in the model to test against. |
965 | + |
966 | + Just remember that the data appears in a separate model so as not |
967 | + to interfere with the user's official DeeModel stream. |
968 | + """ |
969 | + from friends.utils.cache import JsonCache |
970 | + from friends.protocols.facebook import Facebook |
971 | + from friends.protocols.flickr import Flickr |
972 | + from friends.protocols.twitter import Twitter |
973 | + from gi.repository import Dee |
974 | + |
975 | + temp_cache = tempfile.mkdtemp() |
976 | + root = JsonCache._root = os.path.join(temp_cache, '{}.json') |
977 | + |
978 | + protocols = { |
979 | + 'facebook-full.dat': Facebook(FakeAccount(account_id=1)), |
980 | + 'flickr-full.dat': Flickr(FakeAccount(account_id=2)), |
981 | + 'twitter-home.dat': Twitter(FakeAccount(account_id=3)), |
982 | + } |
983 | + |
984 | + for fake_name, protocol in protocols.items(): |
985 | + protocol.source_registry = EDSRegistry() |
986 | + with mock.patch('friends.utils.http.Soup.Message', |
987 | + FakeSoupMessage('friends.tests.data', |
988 | + fake_name)) as fake: |
989 | + protocol.receive() |
990 | + |
991 | + shutil.rmtree(temp_cache) |
992 | + |
993 | + |
994 | class FakeAuth: |
995 | id = 'fakeauth id' |
996 | method = 'fakeauth method' |
997 | @@ -61,7 +109,7 @@ |
998 | class FakeAccount: |
999 | """A fake account object for testing purposes.""" |
1000 | |
1001 | - def __init__(self, service=None): |
1002 | + def __init__(self, service=None, account_id=88): |
1003 | self.access_token = None |
1004 | self.secret_token = None |
1005 | self.user_full_name = None |
1006 | @@ -69,7 +117,7 @@ |
1007 | self.user_id = None |
1008 | self.auth = FakeAuth() |
1009 | self.login_lock = threading.Lock() |
1010 | - self.id = '1234' |
1011 | + self.id = account_id |
1012 | self.protocol = Base(self) |
1013 | |
1014 | |
1015 | |
1016 | === modified file 'friends/tests/test_account.py' |
1017 | --- friends/tests/test_account.py 2013-02-05 01:11:35 +0000 |
1018 | +++ friends/tests/test_account.py 2013-03-20 13:27:53 +0000 |
1019 | @@ -23,19 +23,11 @@ |
1020 | |
1021 | import unittest |
1022 | |
1023 | -from gi.repository import Dee |
1024 | - |
1025 | from friends.errors import UnsupportedProtocolError |
1026 | from friends.protocols.flickr import Flickr |
1027 | -from friends.tests.mocks import FakeAccount, LogMock, SettingsIterMock, mock |
1028 | +from friends.tests.mocks import FakeAccount, LogMock, SettingsIterMock |
1029 | +from friends.tests.mocks import TestModel, mock |
1030 | from friends.utils.account import Account, AccountManager |
1031 | -from friends.utils.model import COLUMN_TYPES |
1032 | - |
1033 | - |
1034 | -# Create a test model that will not interfere with the user's environment. |
1035 | -# We'll use this object as a mock of the real model. |
1036 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1037 | -TestModel.set_schema_full(COLUMN_TYPES) |
1038 | |
1039 | |
1040 | class TestAccount(unittest.TestCase): |
1041 | @@ -159,7 +151,6 @@ |
1042 | assert self.account != None |
1043 | |
1044 | |
1045 | - |
1046 | accounts_manager = mock.Mock() |
1047 | accounts_manager.new_for_service_type( |
1048 | 'microblogging').get_enabled_account_services.return_value = [] |
1049 | @@ -199,7 +190,7 @@ |
1050 | # the account manager's mapping. |
1051 | manager = AccountManager() |
1052 | manager._add_new_account(self.account_service) |
1053 | - self.assertIn('1234', manager._accounts) |
1054 | + self.assertIn(88, manager._accounts) |
1055 | |
1056 | def test_account_manager_enabled_event(self): |
1057 | manager = AccountManager() |
1058 | @@ -210,56 +201,6 @@ |
1059 | manager._on_enabled_event(accounts_manager, 2) |
1060 | account.protocol.assert_called_once_with('receive') |
1061 | |
1062 | - def test_account_manager_delete_account_no_account(self): |
1063 | - # Deleting an account removes the global_id from the mapping. But if |
1064 | - # that global id is missing, then it does not cause an exception. |
1065 | - manager = AccountManager() |
1066 | - manager._get_service = mock.Mock() |
1067 | - manager._get_service.return_value = self.account_service |
1068 | - self.assertNotIn('1234', manager._accounts) |
1069 | - manager._on_account_deleted(accounts_manager, '1234') |
1070 | - self.assertNotIn('1234', manager._accounts) |
1071 | - |
1072 | - @mock.patch('friends.utils.base.Model', TestModel) |
1073 | - @mock.patch('friends.utils.base._seen_ids', {}) |
1074 | - def test_account_manager_delete_account(self): |
1075 | - # Deleting an account removes the id from the mapping. But if |
1076 | - # that id is missing, then it does not cause an exception. |
1077 | - manager = AccountManager() |
1078 | - manager._get_service = mock.Mock() |
1079 | - manager._get_service.return_value = self.account_service |
1080 | - manager._add_new_account(self.account_service) |
1081 | - self.assertIn('1234', manager._accounts) |
1082 | - manager._on_account_deleted(accounts_manager, '1234') |
1083 | - self.assertNotIn('1234', manager._accounts) |
1084 | - |
1085 | - @mock.patch('friends.utils.base.Model', TestModel) |
1086 | - @mock.patch('friends.utils.base._seen_ids', {}) |
1087 | - def test_account_manager_delete_account_preserve_messages(self): |
1088 | - # Deleting an Account should not delete messages from the row |
1089 | - # that exist on other protocols too. |
1090 | - manager = AccountManager() |
1091 | - manager._get_service = mock.Mock() |
1092 | - manager._get_service.return_value = self.account_service |
1093 | - manager._add_new_account(self.account_service) |
1094 | - example_row = [[['twitter', '6', '1234'], |
1095 | - ['base', '1234', '5678']], |
1096 | - 'messages', 'Fred Flintstone', '', 'fred', True, |
1097 | - '2012-08-28T19:59:34', 'Yabba dabba dooooo!', '', '', |
1098 | - 0.0, False, '', '', '', '', '', ''] |
1099 | - result_row = [[['twitter', '6', '1234']], |
1100 | - 'messages', 'Fred Flintstone', '', 'fred', True, |
1101 | - '2012-08-28T19:59:34', 'Yabba dabba dooooo!', '', '', |
1102 | - 0.0, False, '', '', '', '', '', ''] |
1103 | - row_iter = TestModel.append(*example_row) |
1104 | - from friends.utils.base import _seen_ids |
1105 | - _seen_ids[ |
1106 | - ('base', '1234', '5678') |
1107 | - ] = TestModel.get_position(row_iter) |
1108 | - self.assertEqual(list(TestModel.get_row(0)), example_row) |
1109 | - manager._on_account_deleted(accounts_manager, '1234') |
1110 | - self.assertEqual(list(TestModel.get_row(0)), result_row) |
1111 | - |
1112 | |
1113 | @mock.patch('gi.repository.Accounts.Manager', accounts_manager) |
1114 | class TestAccountManagerRealAccount(unittest.TestCase): |
1115 | |
1116 | === modified file 'friends/tests/test_cache.py' |
1117 | --- friends/tests/test_cache.py 2013-03-08 02:31:15 +0000 |
1118 | +++ friends/tests/test_cache.py 2013-03-20 13:27:53 +0000 |
1119 | @@ -21,14 +21,10 @@ |
1120 | |
1121 | |
1122 | import os |
1123 | -import time |
1124 | import shutil |
1125 | import tempfile |
1126 | import unittest |
1127 | |
1128 | -from datetime import date, timedelta |
1129 | -from pkg_resources import resource_filename |
1130 | - |
1131 | from friends.utils.cache import JsonCache |
1132 | |
1133 | |
1134 | |
1135 | === modified file 'friends/tests/test_dispatcher.py' |
1136 | --- friends/tests/test_dispatcher.py 2013-02-19 17:00:41 +0000 |
1137 | +++ friends/tests/test_dispatcher.py 2013-03-20 13:27:53 +0000 |
1138 | @@ -26,7 +26,7 @@ |
1139 | |
1140 | from dbus.mainloop.glib import DBusGMainLoop |
1141 | |
1142 | -from friends.service.dispatcher import Dispatcher, STUB |
1143 | +from friends.service.dispatcher import Dispatcher, ManageTimers, STUB |
1144 | from friends.tests.mocks import LogMock, mock |
1145 | |
1146 | |
1147 | @@ -61,7 +61,11 @@ |
1148 | self.dispatcher.account_manager.get_all.assert_called_once_with() |
1149 | account.protocol.assert_called_once_with('receive') |
1150 | |
1151 | - self.assertEqual(self.log_mock.empty(), 'Refresh requested\n') |
1152 | + self.assertEqual(self.log_mock.empty(), |
1153 | + 'Clearing 1 shutdown timer(s)...\n' |
1154 | + 'Refresh requested\n' |
1155 | + 'Clearing 0 shutdown timer(s)...\n' |
1156 | + 'Starting new shutdown timer...\n') |
1157 | |
1158 | def test_clear_indicators(self): |
1159 | self.dispatcher.menu_manager = mock.Mock() |
1160 | @@ -81,7 +85,10 @@ |
1161 | 'like', '23346356767354626', success=STUB, failure=STUB) |
1162 | |
1163 | self.assertEqual(self.log_mock.empty(), |
1164 | - '345: like 23346356767354626\n') |
1165 | + 'Clearing 1 shutdown timer(s)...\n' |
1166 | + '345: like 23346356767354626\n' |
1167 | + 'Clearing 0 shutdown timer(s)...\n' |
1168 | + 'Starting new shutdown timer...\n') |
1169 | |
1170 | def test_failing_do(self): |
1171 | account = mock.Mock() |
1172 | @@ -93,7 +100,10 @@ |
1173 | self.assertEqual(account.protocol.call_count, 0) |
1174 | |
1175 | self.assertEqual(self.log_mock.empty(), |
1176 | - 'Could not find account: 6\n') |
1177 | + 'Clearing 1 shutdown timer(s)...\n' |
1178 | + 'Could not find account: 6\n' |
1179 | + 'Clearing 0 shutdown timer(s)...\n' |
1180 | + 'Starting new shutdown timer...\n') |
1181 | |
1182 | def test_send_message(self): |
1183 | account1 = mock.Mock() |
1184 | @@ -128,7 +138,10 @@ |
1185 | success=STUB, failure=STUB) |
1186 | |
1187 | self.assertEqual(self.log_mock.empty(), |
1188 | - 'Replying to 2, objid\n') |
1189 | + 'Clearing 1 shutdown timer(s)...\n' |
1190 | + 'Replying to 2, objid\n' |
1191 | + 'Clearing 0 shutdown timer(s)...\n' |
1192 | + 'Starting new shutdown timer...\n') |
1193 | |
1194 | def test_send_reply_failed(self): |
1195 | account = mock.Mock() |
1196 | @@ -140,8 +153,11 @@ |
1197 | self.assertEqual(account.protocol.call_count, 0) |
1198 | |
1199 | self.assertEqual(self.log_mock.empty(), |
1200 | - 'Replying to 2, objid\n' + |
1201 | - 'Could not find account: 2\n') |
1202 | + 'Clearing 1 shutdown timer(s)...\n' |
1203 | + 'Replying to 2, objid\n' |
1204 | + 'Could not find account: 2\n' |
1205 | + 'Clearing 0 shutdown timer(s)...\n' |
1206 | + 'Starting new shutdown timer...\n') |
1207 | |
1208 | def test_upload_async(self): |
1209 | account = mock.Mock() |
1210 | @@ -166,7 +182,10 @@ |
1211 | ) |
1212 | |
1213 | self.assertEqual(self.log_mock.empty(), |
1214 | - 'Uploading file://path/to/image.png to 2\n') |
1215 | + 'Clearing 1 shutdown timer(s)...\n' |
1216 | + 'Uploading file://path/to/image.png to 2\n' |
1217 | + 'Clearing 0 shutdown timer(s)...\n' |
1218 | + 'Starting new shutdown timer...\n') |
1219 | |
1220 | def test_get_features(self): |
1221 | self.assertEqual(json.loads(self.dispatcher.GetFeatures('facebook')), |
1222 | @@ -205,6 +224,52 @@ |
1223 | self.dispatcher.URLShorten(long_url), |
1224 | 'short url') |
1225 | lookup_mock.is_shortened.assert_called_once_with(long_url) |
1226 | - self.dispatcher.settings.get_boolean.assert_called_once_with('shorten-urls') |
1227 | + self.dispatcher.settings.get_boolean.assert_called_once_with( |
1228 | + 'shorten-urls') |
1229 | lookup_mock.lookup.assert_called_once_with('is.gd') |
1230 | - lookup_mock.lookup.return_value.shorten.assert_called_once_with(long_url) |
1231 | + lookup_mock.lookup.return_value.shorten.assert_called_once_with( |
1232 | + long_url) |
1233 | + |
1234 | + @mock.patch('friends.service.dispatcher.GLib') |
1235 | + def test_manage_timers_clear(self, glib): |
1236 | + manager = ManageTimers() |
1237 | + manager.timers = {1} |
1238 | + manager.__enter__() |
1239 | + glib.source_remove.assert_called_once_with(1) |
1240 | + manager.timers = {1, 2, 3} |
1241 | + manager.clear_all_timers() |
1242 | + self.assertEqual(glib.source_remove.call_count, 4) |
1243 | + |
1244 | + @mock.patch('friends.service.dispatcher.GLib') |
1245 | + def test_manage_timers_set(self, glib): |
1246 | + manager = ManageTimers() |
1247 | + manager.timers = set() |
1248 | + manager.clear_all_timers = mock.Mock() |
1249 | + manager.__exit__() |
1250 | + glib.timeout_add_seconds.assert_called_once_with(30, manager.terminate) |
1251 | + manager.clear_all_timers.assert_called_once_with() |
1252 | + self.assertEqual(len(manager.timers), 1) |
1253 | + |
1254 | + @mock.patch('friends.service.dispatcher.persist_model') |
1255 | + @mock.patch('friends.service.dispatcher.threading') |
1256 | + @mock.patch('friends.service.dispatcher.GLib') |
1257 | + def test_manage_timers_terminate(self, glib, thread, persist): |
1258 | + manager = ManageTimers() |
1259 | + manager.timers = set() |
1260 | + thread.activeCount.return_value = 1 |
1261 | + manager.terminate() |
1262 | + thread.activeCount.assert_called_once_with() |
1263 | + persist.assert_called_once_with() |
1264 | + glib.idle_add.assert_called_once_with(manager.callback) |
1265 | + |
1266 | + @mock.patch('friends.service.dispatcher.persist_model') |
1267 | + @mock.patch('friends.service.dispatcher.threading') |
1268 | + @mock.patch('friends.service.dispatcher.GLib') |
1269 | + def test_manage_timers_dont_kill_threads(self, glib, thread, persist): |
1270 | + manager = ManageTimers() |
1271 | + manager.timers = set() |
1272 | + manager.set_new_timer = mock.Mock() |
1273 | + thread.activeCount.return_value = 10 |
1274 | + manager.terminate() |
1275 | + thread.activeCount.assert_called_once_with() |
1276 | + manager.set_new_timer.assert_called_once_with() |
1277 | |
1278 | === modified file 'friends/tests/test_facebook.py' |
1279 | --- friends/tests/test_facebook.py 2013-02-27 22:22:38 +0000 |
1280 | +++ friends/tests/test_facebook.py 2013-03-20 13:27:53 +0000 |
1281 | @@ -15,27 +15,26 @@ |
1282 | |
1283 | """Test the Facebook plugin.""" |
1284 | |
1285 | + |
1286 | __all__ = [ |
1287 | 'TestFacebook', |
1288 | ] |
1289 | |
1290 | |
1291 | +import os |
1292 | +import tempfile |
1293 | import unittest |
1294 | +import shutil |
1295 | |
1296 | -from gi.repository import Dee, GLib |
1297 | +from gi.repository import GLib |
1298 | from pkg_resources import resource_filename |
1299 | |
1300 | from friends.protocols.facebook import Facebook |
1301 | -from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock, mock |
1302 | +from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
1303 | +from friends.tests.mocks import TestModel, mock |
1304 | from friends.tests.mocks import EDSBookClientMock, EDSSource, EDSRegistry |
1305 | from friends.errors import ContactsError, FriendsError, AuthorizationError |
1306 | -from friends.utils.model import COLUMN_TYPES |
1307 | - |
1308 | - |
1309 | -# Create a test model that will not interfere with the user's environment. |
1310 | -# We'll use this object as a mock of the real model. |
1311 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1312 | -TestModel.set_schema_full(COLUMN_TYPES) |
1313 | +from friends.utils.cache import JsonCache |
1314 | |
1315 | |
1316 | @mock.patch('friends.utils.http._soup', mock.Mock()) |
1317 | @@ -44,12 +43,16 @@ |
1318 | """Test the Facebook API.""" |
1319 | |
1320 | def setUp(self): |
1321 | + self._temp_cache = tempfile.mkdtemp() |
1322 | + self._root = JsonCache._root = os.path.join( |
1323 | + self._temp_cache, '{}.json') |
1324 | self.account = FakeAccount() |
1325 | self.protocol = Facebook(self.account) |
1326 | self.protocol.source_registry = EDSRegistry() |
1327 | |
1328 | def tearDown(self): |
1329 | TestModel.clear() |
1330 | + shutil.rmtree(self._temp_cache) |
1331 | |
1332 | def test_features(self): |
1333 | # The set of public features. |
1334 | @@ -106,75 +109,153 @@ |
1335 | # Receive the wall feed for a user. |
1336 | self.maxDiff = None |
1337 | self.account.access_token = 'abc' |
1338 | - self.assertEqual(self.protocol.receive(), 4) |
1339 | - self.assertEqual(TestModel.get_n_rows(), 4) |
1340 | + self.assertEqual(self.protocol.receive(), 12) |
1341 | + self.assertEqual(TestModel.get_n_rows(), 12) |
1342 | + self.assertEqual(list(TestModel.get_row(0)), [ |
1343 | + 'facebook', |
1344 | + 88, |
1345 | + 'fake_id', |
1346 | + 'mentions', |
1347 | + 'Yours Truly', |
1348 | + '56789', |
1349 | + 'Yours Truly', |
1350 | + False, |
1351 | + '2013-03-13T23:29:07Z', |
1352 | + 'Writing code that supports geotagging data from facebook. ' + |
1353 | + 'If y\'all could make some geotagged facebook posts for me ' + |
1354 | + 'to test with, that\'d be super.', |
1355 | + GLib.get_user_cache_dir() + |
1356 | + '/friends/avatars/5c4e74c64b1a09343558afc1046c2b1d176a2ba2', |
1357 | + 'https://www.facebook.com/56789', |
1358 | + 1, |
1359 | + False, |
1360 | + '', |
1361 | + '', |
1362 | + '', |
1363 | + '', |
1364 | + '', |
1365 | + '', |
1366 | + 'Victoria, British Columbia', |
1367 | + 48.4333, |
1368 | + -123.35, |
1369 | + ]) |
1370 | self.assertEqual(list(TestModel.get_row(2)), [ |
1371 | - [['facebook', |
1372 | - '1234', |
1373 | - '117402931676347_386054134801436_3235476']], |
1374 | - 'reply_to/109', |
1375 | - 'Bruce Peart', |
1376 | - '809', |
1377 | - 'Bruce Peart', |
1378 | - False, |
1379 | - '2012-09-26T17:16:00Z', |
1380 | - 'OK Don...10) Headlong Flight', |
1381 | - GLib.get_user_cache_dir() + |
1382 | - '/friends/avatars/b688c8def0455d4a3853d5fcdfaf0708645cfd3e', |
1383 | - 'https://www.facebook.com/809', |
1384 | - 0.0, |
1385 | - False, |
1386 | - '', |
1387 | - '', |
1388 | - '', |
1389 | - '', |
1390 | - '', |
1391 | - '']) |
1392 | - self.assertEqual(list(TestModel.get_row(0)), [ |
1393 | - [['facebook', '1234', '108']], |
1394 | - 'mentions', |
1395 | - 'Rush is a Band', |
1396 | - '117402931676347', |
1397 | - 'Rush is a Band', |
1398 | - False, |
1399 | - '2012-09-26T17:34:00Z', |
1400 | - 'Rush takes off to the Great White North', |
1401 | - GLib.get_user_cache_dir() + |
1402 | - '/friends/avatars/7d1a70e6998f4a38954e93ca03d689463f71d63b', |
1403 | - 'https://www.facebook.com/117402931676347', |
1404 | - 16.0, |
1405 | - False, |
1406 | - 'https://fbexternal-a.akamaihd.net/rush.jpg', |
1407 | - 'Rush is a Band Blog', |
1408 | - 'http://www.rushisaband.com/blog/Rush-Clockwork-Angels-tour', |
1409 | - 'Rush is a Band: Neil Peart, Geddy Lee, Alex Lifeson', |
1410 | - 'www.rushisaband.com', |
1411 | - '']) |
1412 | - self.assertEqual(list(TestModel.get_row(1)), [ |
1413 | - [['facebook', '1234', '109']], |
1414 | - 'mentions', |
1415 | - 'Rush is a Band', |
1416 | - '117402931676347', |
1417 | - 'Rush is a Band', |
1418 | - False, |
1419 | - '2012-09-26T17:49:06Z', |
1420 | - 'http://www2.gibson.com/Alex-Lifeson-0225-2011.aspx', |
1421 | - GLib.get_user_cache_dir() + |
1422 | - '/friends/avatars/7d1a70e6998f4a38954e93ca03d689463f71d63b', |
1423 | - 'https://www.facebook.com/117402931676347', |
1424 | - 27.0, |
1425 | - False, |
1426 | - 'https://images.gibson.com/Rush_Clockwork-Angels_t.jpg', |
1427 | - 'Top 10 Alex Lifeson Guitar Moments', |
1428 | - 'http://www2.gibson.com/Alex-Lifeson.aspx', |
1429 | - 'For millions of Rush fans old and new, it’s a pleasure', |
1430 | - 'www2.gibson.com', |
1431 | - '']) |
1432 | + 'facebook', |
1433 | + 88, |
1434 | + 'faker than cake!', |
1435 | + 'reply_to/fake_id', |
1436 | + 'Father', |
1437 | + '234', |
1438 | + 'Father', |
1439 | + False, |
1440 | + '2013-03-12T23:29:45Z', |
1441 | + 'don\'t know how', |
1442 | + GLib.get_user_cache_dir() + |
1443 | + '/friends/avatars/9b9379ccc7948e4804dff7914bfa4c6de3974df5', |
1444 | + 'https://www.facebook.com/234', |
1445 | + 0, |
1446 | + False, |
1447 | + '', |
1448 | + '', |
1449 | + '', |
1450 | + '', |
1451 | + '', |
1452 | + '', |
1453 | + '', |
1454 | + 0.0, |
1455 | + 0.0, |
1456 | + ]) |
1457 | + self.assertEqual(list(TestModel.get_row(6)), [ |
1458 | + 'facebook', |
1459 | + 88, |
1460 | + '161247843901324_629147610444676', |
1461 | + 'mentions', |
1462 | + 'Best Western Denver Southwest', |
1463 | + '161247843901324', |
1464 | + 'Best Western Denver Southwest', |
1465 | + False, |
1466 | + '2013-03-11T23:51:25Z', |
1467 | + 'Today only -- Come meet Caroline and Meredith and Stanley the ' + |
1468 | + 'Stegosaurus (& Greg & Joe, too!) at the TechZulu Trend Lounge, ' + |
1469 | + 'Hilton Garden Inn 18th floor, 500 N Interstate 35, Austin, ' + |
1470 | + 'Texas. Monday, March 11th, 4:00pm to 7:00 pm. Also here ' + |
1471 | + 'Hannah Hart (My Drunk Kitchen) and Angry Video Game Nerd ' + |
1472 | + 'producer, Sean Keegan. Stanley is in the lobby.', |
1473 | + GLib.get_user_cache_dir() + |
1474 | + '/friends/avatars/5b2d70e788df790b9c8db4c6a138fc4a1f433ec9', |
1475 | + 'https://www.facebook.com/161247843901324', |
1476 | + 84, |
1477 | + False, |
1478 | + 'https://fbcdn-photos-a.akamaihd.net/hphotos-ak-snc7/' + |
1479 | + '601266_629147587111345_968504279_s.jpg', |
1480 | + '', |
1481 | + 'https://www.facebook.com/photo.php?fbid=629147587111345&set=a.173256162700492.47377.161247843901324&type=1&relevant_count=1', |
1482 | + '', |
1483 | + '', |
1484 | + '', |
1485 | + 'Hilton Garden Inn Austin Downtown/Convention Center', |
1486 | + 30.265384957204, |
1487 | + -97.735604602521, |
1488 | + ]) |
1489 | + self.assertEqual(list(TestModel.get_row(9)), [ |
1490 | + 'facebook', |
1491 | + 88, |
1492 | + '104443_100085049977', |
1493 | + 'mentions', |
1494 | + 'Guy Frenchie', |
1495 | + '1244414', |
1496 | + 'Guy Frenchie', |
1497 | + False, |
1498 | + '2013-03-15T19:57:14Z', |
1499 | + 'Guy Frenchie did some things with some stuff.', |
1500 | + GLib.get_user_cache_dir() + |
1501 | + '/friends/avatars/3f5e276af0c43f6411d931b829123825ede1968e', |
1502 | + 'https://www.facebook.com/1244414', |
1503 | + 3, |
1504 | + False, |
1505 | + '', |
1506 | + '', |
1507 | + '', |
1508 | + '', |
1509 | + '', |
1510 | + '', |
1511 | + '', |
1512 | + 0.0, |
1513 | + 0.0, |
1514 | + ]) |
1515 | |
1516 | # XXX We really need full coverage of the receive() method, including |
1517 | # cases where some data is missing, or can't be converted |
1518 | # (e.g. timestamps), and paginations. |
1519 | |
1520 | + @mock.patch('friends.utils.base.Model', TestModel) |
1521 | + @mock.patch('friends.utils.http.Soup.Message', |
1522 | + FakeSoupMessage('friends.tests.data', 'facebook-full.dat')) |
1523 | + @mock.patch('friends.protocols.facebook.Facebook._login', |
1524 | + return_value=True) |
1525 | + @mock.patch('friends.utils.base._seen_ids', {}) |
1526 | + def test_home_since_id(self, *mocks): |
1527 | + self.account.access_token = 'access' |
1528 | + self.account.secret_token = 'secret' |
1529 | + self.account.auth.parameters = dict( |
1530 | + ConsumerKey='key', |
1531 | + ConsumerSecret='secret') |
1532 | + self.assertEqual(self.protocol.home(), 12) |
1533 | + |
1534 | + with open(self._root.format('facebook_ids'), 'r') as fd: |
1535 | + self.assertEqual(fd.read(), '{"messages": "2013-03-15T19:57:14Z"}') |
1536 | + |
1537 | + follow = self.protocol._follow_pagination = mock.Mock() |
1538 | + follow.return_value = [] |
1539 | + self.assertEqual(self.protocol.home(), 12) |
1540 | + follow.assert_called_once_with( |
1541 | + 'https://graph.facebook.com/me/home', |
1542 | + dict(limit=50, |
1543 | + since='2013-03-15T19:57:14Z', |
1544 | + access_token='access', |
1545 | + ) |
1546 | + ) |
1547 | + |
1548 | @mock.patch('friends.protocols.facebook.Downloader') |
1549 | def test_send_to_my_wall(self, dload): |
1550 | dload().get_json.return_value = dict(id='post_id') |
1551 | |
1552 | === modified file 'friends/tests/test_flickr.py' |
1553 | --- friends/tests/test_flickr.py 2013-02-27 23:04:36 +0000 |
1554 | +++ friends/tests/test_flickr.py 2013-03-20 13:27:53 +0000 |
1555 | @@ -22,18 +22,12 @@ |
1556 | |
1557 | import unittest |
1558 | |
1559 | -from gi.repository import GLib, Dee |
1560 | +from gi.repository import GLib |
1561 | |
1562 | from friends.errors import AuthorizationError, FriendsError |
1563 | from friends.protocols.flickr import Flickr |
1564 | -from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock, mock |
1565 | -from friends.utils.model import COLUMN_INDICES, COLUMN_TYPES |
1566 | - |
1567 | - |
1568 | -# Create a test model that will not interfere with the user's environment. |
1569 | -# We'll use this object as a mock of the real model. |
1570 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1571 | -TestModel.set_schema_full(COLUMN_TYPES) |
1572 | +from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
1573 | +from friends.tests.mocks import TestModel, mock |
1574 | |
1575 | |
1576 | @mock.patch('friends.utils.http._soup', mock.Mock()) |
1577 | @@ -133,7 +127,7 @@ |
1578 | 'http://api.flickr.com/services/rest', |
1579 | method='GET', |
1580 | params=dict( |
1581 | - extras='date_upload,owner_name,icon_server', |
1582 | + extras='date_upload,owner_name,icon_server,geo', |
1583 | format='json', |
1584 | nojsoncallback='1', |
1585 | api_key='fake', |
1586 | @@ -157,77 +151,66 @@ |
1587 | @mock.patch('friends.utils.base.Model', TestModel) |
1588 | def test_flickr_data(self): |
1589 | # Start by setting up a fake account id. |
1590 | - self.account.id = 'lerxst' |
1591 | + self.account.id = 69 |
1592 | with mock.patch.object(self.protocol, '_get_access_token', |
1593 | return_value='token'): |
1594 | - self.assertEqual(self.protocol.receive(), 3) |
1595 | - self.assertEqual(TestModel.get_n_rows(), 3) |
1596 | + self.assertEqual(self.protocol.receive(), 10) |
1597 | + self.assertEqual(TestModel.get_n_rows(), 10) |
1598 | |
1599 | self.assertEqual( |
1600 | list(TestModel.get_row(0)), |
1601 | - [[['flickr', 'lerxst', '801']], |
1602 | - 'images', |
1603 | - '', |
1604 | - '123', |
1605 | - '', |
1606 | - False, |
1607 | - '2012-05-10T13:36:45Z', |
1608 | - '', |
1609 | - '', |
1610 | - '', |
1611 | - 0.0, |
1612 | - False, |
1613 | - '', |
1614 | - '', |
1615 | - '', |
1616 | - '', |
1617 | - 'ant', |
1618 | - '', |
1619 | - ]) |
1620 | - |
1621 | - self.assertEqual( |
1622 | - list(TestModel.get_row(1)), |
1623 | - [[['flickr', 'lerxst', '802']], |
1624 | - 'images', |
1625 | - 'Alex Lifeson', |
1626 | - '456', |
1627 | - 'Alex Lifeson', |
1628 | - True, |
1629 | - '', |
1630 | - '', |
1631 | - '', |
1632 | - '', |
1633 | - 0.0, |
1634 | - False, |
1635 | - '', |
1636 | - '', |
1637 | - '', |
1638 | - '', |
1639 | - 'bee', |
1640 | - '', |
1641 | - ]) |
1642 | - |
1643 | - self.assertEqual( |
1644 | - list(TestModel.get_row(2)), |
1645 | - [[['flickr', 'lerxst', '803']], |
1646 | - 'images', |
1647 | - 'Bob Dobbs', |
1648 | - '789', |
1649 | - 'Bob Dobbs', |
1650 | - False, |
1651 | - '', |
1652 | - '', |
1653 | - GLib.get_user_cache_dir() + |
1654 | - '/friends/avatars/b913501d6face9d13f3006b731a711b596d23099', |
1655 | - 'http://www.flickr.com/people/789', |
1656 | - 0.0, |
1657 | - False, |
1658 | - 'http://farmanimalz.static.flickr.com/1/789_ghi_m.jpg', |
1659 | - '', |
1660 | - 'http://farmanimalz.static.flickr.com/1/789_ghi_b.jpg', |
1661 | - '', |
1662 | - 'cat', |
1663 | - 'http://farmanimalz.static.flickr.com/1/789_ghi_t.jpg', |
1664 | + ['flickr', |
1665 | + 69, |
1666 | + '8552892154', |
1667 | + 'images', |
1668 | + 'raise my voice', |
1669 | + '47303164@N00', |
1670 | + 'raise my voice', |
1671 | + True, |
1672 | + '2013-03-12T19:51:42Z', |
1673 | + '', |
1674 | + GLib.get_user_cache_dir() + |
1675 | + '/friends/avatars/7b30ff0140dd9b80f2b1782a2802c3ce785fa0ce', |
1676 | + 'http://www.flickr.com/people/47303164@N00', |
1677 | + 0, |
1678 | + False, |
1679 | + 'http://farm9.static.flickr.com/8378/47303164@N00_a_m.jpg', |
1680 | + '', |
1681 | + 'http://farm9.static.flickr.com/8378/47303164@N00_a_b.jpg', |
1682 | + '', |
1683 | + 'Chocolate chai #yegcoffee', |
1684 | + 'http://farm9.static.flickr.com/8378/47303164@N00_a_t.jpg', |
1685 | + '', |
1686 | + 0.0, |
1687 | + 0.0, |
1688 | + ]) |
1689 | + |
1690 | + self.assertEqual( |
1691 | + list(TestModel.get_row(4)), |
1692 | + ['flickr', |
1693 | + 69, |
1694 | + '8550829193', |
1695 | + 'images', |
1696 | + 'Nelson Webb', |
1697 | + '27204141@N05', |
1698 | + 'Nelson Webb', |
1699 | + True, |
1700 | + '2013-03-12T13:54:10Z', |
1701 | + '', |
1702 | + GLib.get_user_cache_dir() + |
1703 | + '/friends/avatars/cae2939354a33fea5f008df91bb8e25920be5dc3', |
1704 | + 'http://www.flickr.com/people/27204141@N05', |
1705 | + 0, |
1706 | + False, |
1707 | + 'http://farm9.static.flickr.com/8246/27204141@N05_e_m.jpg', |
1708 | + '', |
1709 | + 'http://farm9.static.flickr.com/8246/27204141@N05_e_b.jpg', |
1710 | + '', |
1711 | + 'St. Michael - The Archangel', |
1712 | + 'http://farm9.static.flickr.com/8246/27204141@N05_e_t.jpg', |
1713 | + '', |
1714 | + 53.833156, |
1715 | + -112.330784, |
1716 | ]) |
1717 | |
1718 | @mock.patch('friends.utils.http.Soup.form_request_new_from_multipart', |
1719 | |
1720 | === modified file 'friends/tests/test_foursquare.py' |
1721 | --- friends/tests/test_foursquare.py 2013-02-26 19:13:31 +0000 |
1722 | +++ friends/tests/test_foursquare.py 2013-03-20 13:27:53 +0000 |
1723 | @@ -22,20 +22,12 @@ |
1724 | |
1725 | import unittest |
1726 | |
1727 | -from gi.repository import Dee |
1728 | - |
1729 | from friends.protocols.foursquare import FourSquare |
1730 | -from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock, mock |
1731 | -from friends.utils.model import COLUMN_TYPES |
1732 | +from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
1733 | +from friends.tests.mocks import TestModel, mock |
1734 | from friends.errors import AuthorizationError |
1735 | |
1736 | |
1737 | -# Create a test model that will not interfere with the user's environment. |
1738 | -# We'll use this object as a mock of the real model. |
1739 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1740 | -TestModel.set_schema_full(COLUMN_TYPES) |
1741 | - |
1742 | - |
1743 | @mock.patch('friends.utils.http._soup', mock.Mock()) |
1744 | @mock.patch('friends.utils.base.notify', mock.Mock()) |
1745 | class TestFourSquare(unittest.TestCase): |
1746 | @@ -93,11 +85,11 @@ |
1747 | self.assertEqual(self.protocol.receive(), 1) |
1748 | self.assertEqual(1, TestModel.get_n_rows()) |
1749 | expected = [ |
1750 | - [['foursquare', '1234', '50574c9ce4b0a9a6e84433a0']], |
1751 | + 'foursquare', 88, '50574c9ce4b0a9a6e84433a0', |
1752 | 'messages', 'Jimbob Smith', '', '', True, '2012-09-17T19:15:24Z', |
1753 | "Working on friends's foursquare plugin.", |
1754 | - '~/.cache/friends/avatar/hash', '', 0.0, False, '', '', '', |
1755 | - '', '', '', |
1756 | + '~/.cache/friends/avatar/hash', '', 0, False, '', '', '', |
1757 | + '', '', '', 'Pop Soda\'s Coffee House & Gallery', |
1758 | + 49.88873164336725, -97.158043384552, |
1759 | ] |
1760 | - for got, want in zip(TestModel.get_row(0), expected): |
1761 | - self.assertEqual(got, want) |
1762 | + self.assertEqual(list(TestModel.get_row(0)), expected) |
1763 | |
1764 | === modified file 'friends/tests/test_identica.py' |
1765 | --- friends/tests/test_identica.py 2013-03-08 02:31:15 +0000 |
1766 | +++ friends/tests/test_identica.py 2013-03-20 13:27:53 +0000 |
1767 | @@ -26,23 +26,15 @@ |
1768 | import unittest |
1769 | import shutil |
1770 | |
1771 | -from gi.repository import Dee |
1772 | - |
1773 | from friends.protocols.identica import Identica |
1774 | -from friends.tests.mocks import FakeAccount, LogMock, mock |
1775 | +from friends.tests.mocks import FakeAccount, LogMock, TestModel, mock |
1776 | from friends.utils.cache import JsonCache |
1777 | -from friends.utils.model import COLUMN_TYPES |
1778 | from friends.errors import AuthorizationError |
1779 | |
1780 | |
1781 | -# Create a test model that will not interfere with the user's environment. |
1782 | -# We'll use this object as a mock of the real model. |
1783 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1784 | -TestModel.set_schema_full(COLUMN_TYPES) |
1785 | - |
1786 | - |
1787 | @mock.patch('friends.utils.http._soup', mock.Mock()) |
1788 | @mock.patch('friends.utils.base.notify', mock.Mock()) |
1789 | +@mock.patch('friends.utils.base.Model', TestModel) |
1790 | class TestIdentica(unittest.TestCase): |
1791 | """Test the Identica API.""" |
1792 | |
1793 | |
1794 | === modified file 'friends/tests/test_mock_dispatcher.py' |
1795 | --- friends/tests/test_mock_dispatcher.py 2013-02-05 01:11:35 +0000 |
1796 | +++ friends/tests/test_mock_dispatcher.py 2013-03-20 13:27:53 +0000 |
1797 | @@ -22,13 +22,11 @@ |
1798 | |
1799 | import dbus.service |
1800 | import unittest |
1801 | -import json |
1802 | |
1803 | from dbus.mainloop.glib import DBusGMainLoop |
1804 | |
1805 | from friends.service.mock_service import Dispatcher as MockDispatcher |
1806 | from friends.service.dispatcher import Dispatcher |
1807 | -from friends.tests.mocks import LogMock, mock |
1808 | |
1809 | |
1810 | # Set up the DBus main loop. |
1811 | |
1812 | === modified file 'friends/tests/test_model.py' |
1813 | --- friends/tests/test_model.py 2013-02-05 01:11:35 +0000 |
1814 | +++ friends/tests/test_model.py 2013-03-20 13:27:53 +0000 |
1815 | @@ -26,10 +26,8 @@ |
1816 | |
1817 | import unittest |
1818 | |
1819 | -from friends.utils.model import Model |
1820 | from friends.utils.model import prune_model, persist_model |
1821 | from friends.tests.mocks import LogMock, mock |
1822 | -from gi.repository import Dee |
1823 | |
1824 | |
1825 | class TestModel(unittest.TestCase): |
1826 | @@ -42,6 +40,17 @@ |
1827 | self.log_mock.stop() |
1828 | |
1829 | @mock.patch('friends.utils.model.Model') |
1830 | + def test_persist_model(self, model): |
1831 | + model.__len__.return_value = 500 |
1832 | + model.is_synchronized.return_value = True |
1833 | + persist_model() |
1834 | + model.is_synchronized.assert_called_once_with() |
1835 | + model.flush_revision_queue.assert_called_once_with() |
1836 | + self.assertEqual(self.log_mock.empty(), |
1837 | + 'Trying to save Dee.SharedModel with 500 rows.\n' + |
1838 | + 'Saving Dee.SharedModel with 500 rows.\n') |
1839 | + |
1840 | + @mock.patch('friends.utils.model.Model') |
1841 | @mock.patch('friends.utils.model.persist_model') |
1842 | def test_prune_one(self, persist, model): |
1843 | model.get_n_rows.return_value = 8001 |
1844 | |
1845 | === modified file 'friends/tests/test_notify.py' |
1846 | --- friends/tests/test_notify.py 2013-02-05 01:11:35 +0000 |
1847 | +++ friends/tests/test_notify.py 2013-03-20 13:27:53 +0000 |
1848 | @@ -22,20 +22,11 @@ |
1849 | |
1850 | import unittest |
1851 | |
1852 | -from gi.repository import Dee |
1853 | - |
1854 | -from friends.tests.mocks import FakeAccount, mock |
1855 | +from friends.tests.mocks import FakeAccount, TestModel, mock |
1856 | from friends.utils.base import Base |
1857 | -from friends.utils.model import COLUMN_TYPES |
1858 | from friends.utils.notify import notify |
1859 | |
1860 | |
1861 | -# Create a test model that will not interfere with the user's environment. |
1862 | -# We'll use this object as a mock of the real model. |
1863 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1864 | -TestModel.set_schema_full(COLUMN_TYPES) |
1865 | - |
1866 | - |
1867 | class TestNotifications(unittest.TestCase): |
1868 | """Test notification details.""" |
1869 | |
1870 | @@ -43,7 +34,6 @@ |
1871 | TestModel.clear() |
1872 | |
1873 | @mock.patch('friends.utils.base.Model', TestModel) |
1874 | - @mock.patch('friends.utils.base._seen_messages', {}) |
1875 | @mock.patch('friends.utils.base._seen_ids', {}) |
1876 | @mock.patch('friends.utils.base.notify') |
1877 | def test_publish_all(self, notify): |
1878 | @@ -57,7 +47,6 @@ |
1879 | notify.assert_called_once_with('Benjamin', 'notify!', '') |
1880 | |
1881 | @mock.patch('friends.utils.base.Model', TestModel) |
1882 | - @mock.patch('friends.utils.base._seen_messages', {}) |
1883 | @mock.patch('friends.utils.base._seen_ids', {}) |
1884 | @mock.patch('friends.utils.base.notify') |
1885 | def test_publish_mentions_private(self, notify): |
1886 | @@ -73,7 +62,6 @@ |
1887 | notify.assert_called_once_with('Benjamin', 'This message is private!', '') |
1888 | |
1889 | @mock.patch('friends.utils.base.Model', TestModel) |
1890 | - @mock.patch('friends.utils.base._seen_messages', {}) |
1891 | @mock.patch('friends.utils.base._seen_ids', {}) |
1892 | @mock.patch('friends.utils.base.notify') |
1893 | def test_publish_mention_fail(self, notify): |
1894 | @@ -89,7 +77,6 @@ |
1895 | self.assertEqual(notify.call_count, 0) |
1896 | |
1897 | @mock.patch('friends.utils.base.Model', TestModel) |
1898 | - @mock.patch('friends.utils.base._seen_messages', {}) |
1899 | @mock.patch('friends.utils.base._seen_ids', {}) |
1900 | @mock.patch('friends.utils.base.notify') |
1901 | def test_publish_mention_none(self, notify): |
1902 | |
1903 | === modified file 'friends/tests/test_protocols.py' |
1904 | --- friends/tests/test_protocols.py 2013-02-05 01:11:35 +0000 |
1905 | +++ friends/tests/test_protocols.py 2013-03-20 13:27:53 +0000 |
1906 | @@ -24,21 +24,12 @@ |
1907 | import unittest |
1908 | import threading |
1909 | |
1910 | -from gi.repository import Dee |
1911 | - |
1912 | from friends.protocols.flickr import Flickr |
1913 | from friends.protocols.twitter import Twitter |
1914 | -from friends.tests.mocks import FakeAccount, LogMock, mock |
1915 | -from friends.utils.base import Base, feature |
1916 | +from friends.tests.mocks import FakeAccount, LogMock, TestModel, mock |
1917 | +from friends.utils.base import Base, feature, linkify_string |
1918 | from friends.utils.manager import ProtocolManager |
1919 | -from friends.utils.model import ( |
1920 | - COLUMN_INDICES, COLUMN_NAMES, COLUMN_TYPES, Model) |
1921 | - |
1922 | - |
1923 | -# Create a test model that will not interfere with the user's environment. |
1924 | -# We'll use this object as a mock of the real model. |
1925 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
1926 | -TestModel.set_schema_full(COLUMN_TYPES) |
1927 | +from friends.utils.model import COLUMN_INDICES, Model |
1928 | |
1929 | |
1930 | class TestProtocolManager(unittest.TestCase): |
1931 | @@ -147,42 +138,39 @@ |
1932 | count = Model.get_n_rows() |
1933 | self.assertEqual(TestModel.get_n_rows(), 0) |
1934 | base = Base(FakeAccount()) |
1935 | - base._publish('alpha', message='a') |
1936 | - base._publish('beta', message='b') |
1937 | - base._publish('omega', message='c') |
1938 | + base._publish(message_id='alpha', message='a') |
1939 | + base._publish(message_id='beta', message='b') |
1940 | + base._publish(message_id='omega', message='c') |
1941 | self.assertEqual(Model.get_n_rows(), count) |
1942 | self.assertEqual(TestModel.get_n_rows(), 3) |
1943 | |
1944 | @mock.patch('friends.utils.base.Model', TestModel) |
1945 | @mock.patch('friends.utils.base._seen_ids', {}) |
1946 | - @mock.patch('friends.utils.base._seen_messages', {}) |
1947 | def test_seen_dicts_successfully_instantiated(self): |
1948 | - from friends.utils.base import _seen_ids, _seen_messages |
1949 | + from friends.utils.base import _seen_ids |
1950 | from friends.utils.base import initialize_caches |
1951 | self.assertEqual(TestModel.get_n_rows(), 0) |
1952 | base = Base(FakeAccount()) |
1953 | - base._publish('alpha', sender='a', message='a') |
1954 | - base._publish('beta', sender='a', message='a') |
1955 | - base._publish('omega', sender='a', message='b') |
1956 | - self.assertEqual(TestModel.get_n_rows(), 2) |
1957 | + base._publish(message_id='alpha', sender='a', message='a') |
1958 | + base._publish(message_id='beta', sender='a', message='a') |
1959 | + base._publish(message_id='omega', sender='a', message='b') |
1960 | + self.assertEqual(TestModel.get_n_rows(), 3) |
1961 | _seen_ids.clear() |
1962 | - _seen_messages.clear() |
1963 | initialize_caches() |
1964 | - self.assertEqual(sorted(list(_seen_messages.keys())), ['aa', 'ab']) |
1965 | - self.assertEqual(sorted(list(_seen_ids.keys())), |
1966 | - [('base', '1234', 'alpha'), |
1967 | - ('base', '1234', 'beta'), |
1968 | - ('base', '1234', 'omega')]) |
1969 | - # These two point at the same row because sender+message are identical |
1970 | - self.assertEqual(_seen_ids[('base', '1234', 'alpha')], |
1971 | - _seen_ids[('base', '1234', 'beta')]) |
1972 | + self.assertEqual( |
1973 | + _seen_ids, |
1974 | + dict(alpha=0, |
1975 | + beta=1, |
1976 | + omega=2, |
1977 | + ) |
1978 | + ) |
1979 | |
1980 | @mock.patch('friends.utils.base.Model', TestModel) |
1981 | def test_invalid_argument(self): |
1982 | base = Base(FakeAccount()) |
1983 | self.assertEqual(0, TestModel.get_n_rows()) |
1984 | with self.assertRaises(TypeError) as cm: |
1985 | - base._publish('message_id', invalid_argument='not good') |
1986 | + base._publish(message_id='message_id', invalid_argument='not good') |
1987 | self.assertEqual(str(cm.exception), |
1988 | 'Unexpected keyword arguments: invalid_argument') |
1989 | |
1990 | @@ -192,12 +180,11 @@ |
1991 | base = Base(FakeAccount()) |
1992 | self.assertEqual(0, TestModel.get_n_rows()) |
1993 | with self.assertRaises(TypeError) as cm: |
1994 | - base._publish('p.middy', bad='no', wrong='yes') |
1995 | + base._publish(message_id='p.middy', bad='no', wrong='yes') |
1996 | self.assertEqual(str(cm.exception), |
1997 | 'Unexpected keyword arguments: bad, wrong') |
1998 | |
1999 | @mock.patch('friends.utils.base.Model', TestModel) |
2000 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2001 | @mock.patch('friends.utils.base._seen_ids', {}) |
2002 | def test_one_message(self): |
2003 | # Test that publishing a message inserts a row into the model. |
2004 | @@ -215,28 +202,34 @@ |
2005 | liked=True)) |
2006 | self.assertEqual(1, TestModel.get_n_rows()) |
2007 | row = TestModel.get_row(0) |
2008 | - # For convenience. |
2009 | - def V(column_name): |
2010 | - return row[COLUMN_INDICES[column_name]] |
2011 | - self.assertEqual(V('message_ids'), |
2012 | - [['base', '1234', '1234']]) |
2013 | - self.assertEqual(V('stream'), 'messages') |
2014 | - self.assertEqual(V('sender'), 'fred') |
2015 | - self.assertEqual(V('sender_nick'), 'freddy') |
2016 | - self.assertTrue(V('from_me')) |
2017 | - self.assertEqual(V('timestamp'), 'today') |
2018 | - self.assertEqual(V('message'), 'hello, @jimmy') |
2019 | - self.assertEqual(V('likes'), 10) |
2020 | - self.assertTrue(V('liked')) |
2021 | - # All the other columns have empty string values. |
2022 | - empty_columns = set(COLUMN_NAMES) - set( |
2023 | - ['message_ids', 'stream', 'sender', 'sender_nick', 'from_me', |
2024 | - 'timestamp', 'comments', 'message', 'likes', 'liked']) |
2025 | - for column_name in empty_columns: |
2026 | - self.assertEqual(row[COLUMN_INDICES[column_name]], '') |
2027 | + self.assertEqual( |
2028 | + list(row), |
2029 | + ['base', |
2030 | + 88, |
2031 | + '1234', |
2032 | + 'messages', |
2033 | + 'fred', |
2034 | + '', |
2035 | + 'freddy', |
2036 | + True, |
2037 | + 'today', |
2038 | + 'hello, @jimmy', |
2039 | + '', |
2040 | + '', |
2041 | + 10, |
2042 | + True, |
2043 | + '', |
2044 | + '', |
2045 | + '', |
2046 | + '', |
2047 | + '', |
2048 | + '', |
2049 | + '', |
2050 | + 0.0, |
2051 | + 0.0, |
2052 | + ]) |
2053 | |
2054 | @mock.patch('friends.utils.base.Model', TestModel) |
2055 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2056 | @mock.patch('friends.utils.base._seen_ids', {}) |
2057 | def test_unpublish(self): |
2058 | base = Base(FakeAccount()) |
2059 | @@ -246,32 +239,24 @@ |
2060 | sender='fred', |
2061 | message='hello, @jimmy')) |
2062 | self.assertTrue(base._publish( |
2063 | + message_id='1234', |
2064 | + sender='fred', |
2065 | + message='hello, @jimmy')) |
2066 | + self.assertTrue(base._publish( |
2067 | message_id='5678', |
2068 | sender='fred', |
2069 | message='hello, +jimmy')) |
2070 | - self.assertEqual(1, TestModel.get_n_rows()) |
2071 | - self.assertEqual(TestModel[0][0], |
2072 | - [['base', '1234', '1234'], |
2073 | - ['base', '1234', '5678']]) |
2074 | + self.assertEqual(2, TestModel.get_n_rows()) |
2075 | base._unpublish('1234') |
2076 | self.assertEqual(1, TestModel.get_n_rows()) |
2077 | - self.assertEqual(TestModel[0][0], |
2078 | - [['base', '1234', '5678']]) |
2079 | base._unpublish('5678') |
2080 | self.assertEqual(0, TestModel.get_n_rows()) |
2081 | |
2082 | @mock.patch('friends.utils.base.Model', TestModel) |
2083 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2084 | @mock.patch('friends.utils.base._seen_ids', {}) |
2085 | def test_duplicate_messages_identified(self): |
2086 | - # When two messages which are deemed identical, by way of the |
2087 | - # _make_key() test in base.py, are published, only one ends up in the |
2088 | - # model. However, the message_ids list-of-lists gets both sets of |
2089 | - # identifiers. |
2090 | base = Base(FakeAccount()) |
2091 | self.assertEqual(0, TestModel.get_n_rows()) |
2092 | - # Insert the first message into the table. The key will be the string |
2093 | - # 'fredhellojimmy' |
2094 | self.assertTrue(base._publish( |
2095 | message_id='1234', |
2096 | stream='messages', |
2097 | @@ -282,11 +267,9 @@ |
2098 | message='hello, @jimmy', |
2099 | likes=10, |
2100 | liked=True)) |
2101 | - # Insert the second message into the table. Note that because |
2102 | - # punctuation was stripped from the above message, this one will also |
2103 | - # have the key 'fredhellojimmy', thus it will be deemed a duplicate. |
2104 | + # Duplicate |
2105 | self.assertTrue(base._publish( |
2106 | - message_id='5678', |
2107 | + message_id='1234', |
2108 | stream='messages', |
2109 | sender='fred', |
2110 | sender_nick='freddy', |
2111 | @@ -300,13 +283,8 @@ |
2112 | # The first published message wins. |
2113 | row = TestModel.get_row(0) |
2114 | self.assertEqual(row[COLUMN_INDICES['message']], 'hello, @jimmy') |
2115 | - # Both message ids will be present, in the order they were published. |
2116 | - self.assertEqual(row[COLUMN_INDICES['message_ids']], |
2117 | - [['base', '1234', '1234'], |
2118 | - ['base', '1234', '5678']]) |
2119 | |
2120 | @mock.patch('friends.utils.base.Model', TestModel) |
2121 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2122 | @mock.patch('friends.utils.base._seen_ids', {}) |
2123 | def test_duplicate_ids_not_duplicated(self): |
2124 | # When two messages are actually identical (same ids and all), |
2125 | @@ -325,12 +303,34 @@ |
2126 | message='hello, @jimmy')) |
2127 | self.assertEqual(1, TestModel.get_n_rows()) |
2128 | row = TestModel.get_row(0) |
2129 | - # The same message_id should not appear twice. |
2130 | - self.assertEqual(row[COLUMN_INDICES['message_ids']], |
2131 | - [['base', '1234', '1234']]) |
2132 | + self.assertEqual( |
2133 | + list(row), |
2134 | + ['base', |
2135 | + 88, |
2136 | + '1234', |
2137 | + 'messages', |
2138 | + 'fred', |
2139 | + '', |
2140 | + '', |
2141 | + False, |
2142 | + '', |
2143 | + 'hello, @jimmy', |
2144 | + '', |
2145 | + '', |
2146 | + 0, |
2147 | + False, |
2148 | + '', |
2149 | + '', |
2150 | + '', |
2151 | + '', |
2152 | + '', |
2153 | + '', |
2154 | + '', |
2155 | + 0.0, |
2156 | + 0.0, |
2157 | + ]) |
2158 | |
2159 | @mock.patch('friends.utils.base.Model', TestModel) |
2160 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2161 | @mock.patch('friends.utils.base._seen_ids', {}) |
2162 | def test_similar_messages_allowed(self): |
2163 | # Because both the sender and message contribute to the unique key we |
2164 | @@ -392,3 +392,78 @@ |
2165 | |
2166 | def test_features(self): |
2167 | self.assertEqual(MyProtocol.get_features(), ['feature_1', 'feature_2']) |
2168 | + |
2169 | + def test_linkify_string(self): |
2170 | + # String with no URL is unchanged. |
2171 | + self.assertEqual('Hello!', linkify_string('Hello!')) |
2172 | + # http:// works. |
2173 | + self.assertEqual( |
2174 | + '<a href="http://www.example.com">http://www.example.com</a>', |
2175 | + linkify_string('http://www.example.com')) |
2176 | + # https:// works, too. |
2177 | + self.assertEqual( |
2178 | + '<a href="https://www.example.com">https://www.example.com</a>', |
2179 | + linkify_string('https://www.example.com')) |
2180 | + # http:// is optional if you include www. |
2181 | + self.assertEqual( |
2182 | + '<a href="www.example.com">www.example.com</a>', |
2183 | + linkify_string('www.example.com')) |
2184 | + # Haha, nobody uses ftp anymore! |
2185 | + self.assertEqual( |
2186 | + '<a href="ftp://example.com/">ftp://example.com/</a>', |
2187 | + linkify_string('ftp://example.com/')) |
2188 | + # Trailing periods are not linkified. |
2189 | + self.assertEqual( |
2190 | + '<a href="http://example.com">http://example.com</a>.', |
2191 | + linkify_string('http://example.com.')) |
2192 | + # URL can contain periods without getting cut off. |
2193 | + self.assertEqual( |
2194 | + '<a href="http://example.com/products/buy.html">' |
2195 | + 'http://example.com/products/buy.html</a>.', |
2196 | + linkify_string('http://example.com/products/buy.html.')) |
2197 | + # Don't linkify trailing brackets. |
2198 | + self.assertEqual( |
2199 | + 'Example Co (<a href="http://example.com">http://example.com</a>).', |
2200 | + linkify_string('Example Co (http://example.com).')) |
2201 | + # Don't linkify trailing exclamation marks. |
2202 | + self.assertEqual( |
2203 | + 'Go to <a href="https://example.com">https://example.com</a>!', |
2204 | + linkify_string('Go to https://example.com!')) |
2205 | + # Don't linkify trailing commas, also ensure all links are found. |
2206 | + self.assertEqual( |
2207 | + '<a href="www.example.com">www.example.com</a>, <a ' |
2208 | + 'href="http://example.com/stuff">http://example.com/stuff</a>, and ' |
2209 | + '<a href="http://example.com/things">http://example.com/things</a> ' |
2210 | + 'are my favorite sites.', |
2211 | + linkify_string('www.example.com, http://example.com/stuff, and ' |
2212 | + 'http://example.com/things are my favorite sites.')) |
2213 | + # Don't linkify trailing question marks. |
2214 | + self.assertEqual( |
2215 | + 'Ever been to <a href="www.example.com">www.example.com</a>?', |
2216 | + linkify_string('Ever been to www.example.com?')) |
2217 | + # URLs can contain question marks ok. |
2218 | + self.assertEqual( |
2219 | + 'Like <a href="http://example.com?foo=bar&grill=true">' |
2220 | + 'http://example.com?foo=bar&grill=true</a>?', |
2221 | + linkify_string('Like http://example.com?foo=bar&grill=true?')) |
2222 | + # Multi-line strings are also supported. |
2223 | + self.assertEqual( |
2224 | + 'Hey, visit us online!\n\n' |
2225 | + '<a href="http://example.com">http://example.com</a>', |
2226 | + linkify_string('Hey, visit us online!\n\nhttp://example.com')) |
2227 | + # Don't accidentally duplicate linkification. |
2228 | + self.assertEqual( |
2229 | + '<a href="www.example.com">click here!</a>', |
2230 | + linkify_string('<a href="www.example.com">click here!</a>')) |
2231 | + self.assertEqual( |
2232 | + '<a href="www.example.com">www.example.com</a>', |
2233 | + linkify_string('<a href="www.example.com">www.example.com</a>')) |
2234 | + self.assertEqual( |
2235 | + '<a href="www.example.com">www.example.com</a> is our website', |
2236 | + linkify_string( |
2237 | + '<a href="www.example.com">www.example.com</a> is our website')) |
2238 | + # This, apparently, is valid HTML. |
2239 | + self.assertEqual( |
2240 | + '<a href = "www.example.com">www.example.com</a>', |
2241 | + linkify_string( |
2242 | + '<a href = "www.example.com">www.example.com</a>')) |
2243 | |
2244 | === modified file 'friends/tests/test_shortener.py' |
2245 | --- friends/tests/test_shortener.py 2013-02-13 02:05:37 +0000 |
2246 | +++ friends/tests/test_shortener.py 2013-03-20 13:27:53 +0000 |
2247 | @@ -22,8 +22,6 @@ |
2248 | |
2249 | import unittest |
2250 | |
2251 | -from operator import getitem |
2252 | - |
2253 | from friends.shorteners import isgd, ougd, linkeecom, lookup, tinyurlcom |
2254 | from friends.tests.mocks import FakeSoupMessage, mock |
2255 | |
2256 | |
2257 | === modified file 'friends/tests/test_twitter.py' |
2258 | --- friends/tests/test_twitter.py 2013-03-08 02:31:15 +0000 |
2259 | +++ friends/tests/test_twitter.py 2013-03-20 13:27:53 +0000 |
2260 | @@ -26,22 +26,16 @@ |
2261 | import unittest |
2262 | import shutil |
2263 | |
2264 | -from gi.repository import GLib, Dee |
2265 | +from gi.repository import GLib |
2266 | from urllib.error import HTTPError |
2267 | |
2268 | from friends.protocols.twitter import RateLimiter, Twitter |
2269 | -from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock, mock |
2270 | +from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
2271 | +from friends.tests.mocks import TestModel, mock |
2272 | from friends.utils.cache import JsonCache |
2273 | -from friends.utils.model import COLUMN_TYPES |
2274 | from friends.errors import AuthorizationError |
2275 | |
2276 | |
2277 | -# Create a test model that will not interfere with the user's environment. |
2278 | -# We'll use this object as a mock of the real model. |
2279 | -TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel') |
2280 | -TestModel.set_schema_full(COLUMN_TYPES) |
2281 | - |
2282 | - |
2283 | @mock.patch('friends.utils.http._soup', mock.Mock()) |
2284 | @mock.patch('friends.utils.base.notify', mock.Mock()) |
2285 | class TestTwitter(unittest.TestCase): |
2286 | @@ -124,7 +118,6 @@ |
2287 | FakeSoupMessage('friends.tests.data', 'twitter-home.dat')) |
2288 | @mock.patch('friends.protocols.twitter.Twitter._login', |
2289 | return_value=True) |
2290 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2291 | @mock.patch('friends.utils.base._seen_ids', {}) |
2292 | def test_home(self, *mocks): |
2293 | self.account.access_token = 'access' |
2294 | @@ -138,31 +131,32 @@ |
2295 | |
2296 | # This test data was ripped directly from Twitter's API docs. |
2297 | expected = [ |
2298 | - [[['twitter', '1234', '240558470661799936']], |
2299 | + ['twitter', 88, '240558470661799936', |
2300 | 'messages', 'OAuth Dancer', '119476949', 'oauth_dancer', False, |
2301 | '2012-08-28T21:16:23Z', 'just another test', |
2302 | GLib.get_user_cache_dir() + |
2303 | '/friends/avatars/ded4ba3c00583ee511f399d0b2537731ca14c39d', |
2304 | 'https://twitter.com/oauth_dancer/status/240558470661799936', |
2305 | - 0.0, False, '', '', '', '', '', '', |
2306 | + 0, False, '', '', '', '', '', '', '', 0.0, 0.0, |
2307 | ], |
2308 | - [[['twitter', '1234', '240556426106372096']], |
2309 | + ['twitter', 88, '240556426106372096', |
2310 | 'messages', 'Raffi Krikorian', '8285392', 'raffi', False, |
2311 | - '2012-08-28T21:08:15Z', 'lecturing at the "analyzing big data ' + |
2312 | - 'with twitter" class at @cal with @othman http://t.co/bfj7zkDJ', |
2313 | + '2012-08-28T21:08:15Z', 'lecturing at the "analyzing big data ' |
2314 | + 'with twitter" class at @cal with @othman ' |
2315 | + '<a href="http://t.co/bfj7zkDJ">http://t.co/bfj7zkDJ</a>', |
2316 | GLib.get_user_cache_dir() + |
2317 | '/friends/avatars/0219effc03a3049a622476e6e001a4014f33dc31', |
2318 | 'https://twitter.com/raffi/status/240556426106372096', |
2319 | - 0.0, False, '', '', '', '', '', '', |
2320 | + 0, False, '', '', '', '', '', '', '', 0.0, 0.0, |
2321 | ], |
2322 | - [[['twitter', '1234', '240539141056638977']], |
2323 | + ['twitter', 88, '240539141056638977', |
2324 | 'messages', 'Taylor Singletary', '819797', 'episod', False, |
2325 | '2012-08-28T19:59:34Z', |
2326 | 'You\'d be right more often if you thought you were wrong.', |
2327 | GLib.get_user_cache_dir() + |
2328 | '/friends/avatars/0c829cb2934ad76489be21ee5e103735d9b7b034', |
2329 | 'https://twitter.com/episod/status/240539141056638977', |
2330 | - 0.0, False, '', '', '', '', '', '', |
2331 | + 0, False, '', '', '', '', '', '', '', 0.0, 0.0, |
2332 | ], |
2333 | ] |
2334 | for i, expected_row in enumerate(expected): |
2335 | @@ -173,7 +167,6 @@ |
2336 | FakeSoupMessage('friends.tests.data', 'twitter-home.dat')) |
2337 | @mock.patch('friends.protocols.twitter.Twitter._login', |
2338 | return_value=True) |
2339 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2340 | @mock.patch('friends.utils.base._seen_ids', {}) |
2341 | def test_home_since_id(self, *mocks): |
2342 | self.account.access_token = 'access' |
2343 | @@ -193,13 +186,11 @@ |
2344 | 'https://api.twitter.com/1.1/statuses/' + |
2345 | 'home_timeline.json?count=50&since_id=240558470661799936') |
2346 | |
2347 | - |
2348 | @mock.patch('friends.utils.base.Model', TestModel) |
2349 | @mock.patch('friends.utils.http.Soup.Message', |
2350 | FakeSoupMessage('friends.tests.data', 'twitter-send.dat')) |
2351 | @mock.patch('friends.protocols.twitter.Twitter._login', |
2352 | return_value=True) |
2353 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2354 | @mock.patch('friends.utils.base._seen_ids', {}) |
2355 | def test_from_me(self, *mocks): |
2356 | self.account.access_token = 'access' |
2357 | @@ -216,18 +207,17 @@ |
2358 | |
2359 | # This test data was ripped directly from Twitter's API docs. |
2360 | expected_row = [ |
2361 | - [['twitter', '1234', '240558470661799936']], |
2362 | + 'twitter', 88, '240558470661799936', |
2363 | 'messages', 'OAuth Dancer', '119476949', 'oauth_dancer', True, |
2364 | '2012-08-28T21:16:23Z', 'just another test', |
2365 | GLib.get_user_cache_dir() + |
2366 | '/friends/avatars/ded4ba3c00583ee511f399d0b2537731ca14c39d', |
2367 | 'https://twitter.com/oauth_dancer/status/240558470661799936', |
2368 | - 0.0, False, '', '', '', '', '', '', |
2369 | + 0, False, '', '', '', '', '', '', '', 0.0, 0.0, |
2370 | ] |
2371 | self.assertEqual(list(TestModel.get_row(0)), expected_row) |
2372 | |
2373 | @mock.patch('friends.utils.base.Model', TestModel) |
2374 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2375 | @mock.patch('friends.utils.base._seen_ids', {}) |
2376 | def test_home_url(self): |
2377 | get_url = self.protocol._get_url = mock.Mock(return_value=['tweet']) |
2378 | @@ -240,7 +230,6 @@ |
2379 | 'https://api.twitter.com/1.1/statuses/home_timeline.json?count=50') |
2380 | |
2381 | @mock.patch('friends.utils.base.Model', TestModel) |
2382 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2383 | @mock.patch('friends.utils.base._seen_ids', {}) |
2384 | def test_mentions(self): |
2385 | get_url = self.protocol._get_url = mock.Mock(return_value=['tweet']) |
2386 | @@ -254,7 +243,6 @@ |
2387 | 'mentions_timeline.json?count=50') |
2388 | |
2389 | @mock.patch('friends.utils.base.Model', TestModel) |
2390 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2391 | @mock.patch('friends.utils.base._seen_ids', {}) |
2392 | def test_user(self): |
2393 | get_url = self.protocol._get_url = mock.Mock(return_value=['tweet']) |
2394 | @@ -267,7 +255,6 @@ |
2395 | 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=') |
2396 | |
2397 | @mock.patch('friends.utils.base.Model', TestModel) |
2398 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2399 | @mock.patch('friends.utils.base._seen_ids', {}) |
2400 | def test_list(self): |
2401 | get_url = self.protocol._get_url = mock.Mock(return_value=['tweet']) |
2402 | @@ -280,7 +267,6 @@ |
2403 | 'https://api.twitter.com/1.1/lists/statuses.json?list_id=some_list_id') |
2404 | |
2405 | @mock.patch('friends.utils.base.Model', TestModel) |
2406 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2407 | @mock.patch('friends.utils.base._seen_ids', {}) |
2408 | def test_lists(self): |
2409 | get_url = self.protocol._get_url = mock.Mock( |
2410 | @@ -294,7 +280,6 @@ |
2411 | 'https://api.twitter.com/1.1/lists/list.json') |
2412 | |
2413 | @mock.patch('friends.utils.base.Model', TestModel) |
2414 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2415 | @mock.patch('friends.utils.base._seen_ids', {}) |
2416 | def test_private(self): |
2417 | get_url = self.protocol._get_url = mock.Mock(return_value=['tweet']) |
2418 | @@ -346,7 +331,6 @@ |
2419 | ]) |
2420 | |
2421 | @mock.patch('friends.utils.base.Model', TestModel) |
2422 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2423 | @mock.patch('friends.utils.base._seen_ids', {}) |
2424 | def test_send_private(self): |
2425 | get_url = self.protocol._get_url = mock.Mock(return_value='tweet') |
2426 | @@ -406,6 +390,44 @@ |
2427 | 'tweet @pumpichank!', |
2428 | in_reply_to_status_id='1234')) |
2429 | |
2430 | + @mock.patch('friends.utils.base.Model', TestModel) |
2431 | + @mock.patch('friends.utils.http.Soup.Message', |
2432 | + FakeSoupMessage('friends.tests.data', 'twitter-home.dat')) |
2433 | + @mock.patch('friends.protocols.twitter.Twitter._login', |
2434 | + return_value=True) |
2435 | + @mock.patch('friends.utils.base._seen_ids', {}) |
2436 | + def test_send_thread_prepend_nick(self, *mocks): |
2437 | + self.account.access_token = 'access' |
2438 | + self.account.secret_token = 'secret' |
2439 | + self.account.auth.parameters = dict( |
2440 | + ConsumerKey='key', |
2441 | + ConsumerSecret='secret') |
2442 | + self.assertEqual(0, TestModel.get_n_rows()) |
2443 | + self.assertEqual(self.protocol.home(), 3) |
2444 | + self.assertEqual(3, TestModel.get_n_rows()) |
2445 | + |
2446 | + # If you forgot to @mention in your reply, we add it for you. |
2447 | + get = self.protocol._get_url = mock.Mock() |
2448 | + self.protocol._publish_tweet = mock.Mock() |
2449 | + self.protocol.send_thread( |
2450 | + '240556426106372096', |
2451 | + 'Exciting and original response!') |
2452 | + get.assert_called_once_with( |
2453 | + 'https://api.twitter.com/1.1/statuses/update.json', |
2454 | + dict(status='@raffi Exciting and original response!', |
2455 | + in_reply_to_status_id='240556426106372096')) |
2456 | + |
2457 | + # If you remembered the @mention, we won't duplicate it. |
2458 | + get.reset_mock() |
2459 | + self.protocol.send_thread( |
2460 | + '240556426106372096', |
2461 | + 'You are the greatest, @raffi!') |
2462 | + get.assert_called_once_with( |
2463 | + 'https://api.twitter.com/1.1/statuses/update.json', |
2464 | + dict(status='You are the greatest, @raffi!', |
2465 | + in_reply_to_status_id='240556426106372096')) |
2466 | + |
2467 | + |
2468 | def test_delete(self): |
2469 | get_url = self.protocol._get_url = mock.Mock(return_value='tweet') |
2470 | publish = self.protocol._unpublish = mock.Mock() |
2471 | @@ -466,7 +488,6 @@ |
2472 | dict(id='1234')) |
2473 | |
2474 | @mock.patch('friends.utils.base.Model', TestModel) |
2475 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2476 | @mock.patch('friends.utils.base._seen_ids', {}) |
2477 | def test_tag(self): |
2478 | get_url = self.protocol._get_url = mock.Mock( |
2479 | @@ -486,7 +507,6 @@ |
2480 | 'https://api.twitter.com/1.1/search/tweets.json?q=%23yegbike') |
2481 | |
2482 | @mock.patch('friends.utils.base.Model', TestModel) |
2483 | - @mock.patch('friends.utils.base._seen_messages', {}) |
2484 | @mock.patch('friends.utils.base._seen_ids', {}) |
2485 | def test_search(self): |
2486 | get_url = self.protocol._get_url = mock.Mock( |
2487 | |
2488 | === modified file 'friends/utils/account.py' |
2489 | --- friends/utils/account.py 2013-02-05 01:11:35 +0000 |
2490 | +++ friends/utils/account.py 2013-03-20 13:27:53 +0000 |
2491 | @@ -43,7 +43,6 @@ |
2492 | # are added or deleted. |
2493 | manager = Accounts.Manager.new_for_service_type('microblogging') |
2494 | manager.connect('enabled-event', self._on_enabled_event) |
2495 | - manager.connect('account-deleted', self._on_account_deleted) |
2496 | # Add all the currently known accounts. |
2497 | for account_service in manager.get_enabled_account_services(): |
2498 | self._add_new_account(account_service) |
2499 | @@ -63,25 +62,6 @@ |
2500 | account = self._add_new_account(account_service) |
2501 | if account is not None: |
2502 | account.protocol('receive') |
2503 | - else: |
2504 | - # If an account has been disabled in UOA, we should remove |
2505 | - # it's messages from the SharedModel. |
2506 | - self._unpublish_entire_account(account_id) |
2507 | - |
2508 | - def _on_account_deleted(self, manager, account_id): |
2509 | - account_service = self._get_service(manager, account_id) |
2510 | - if account_service is not None: |
2511 | - log.debug('Deleting account {}'.format(account_id)) |
2512 | - self._unpublish_entire_account(account_id) |
2513 | - else: |
2514 | - log.error('Tried to delete invalid account: {}'.format(account_id)) |
2515 | - |
2516 | - def _unpublish_entire_account(self, account_id): |
2517 | - """Delete all the account's messages from the SharedModel.""" |
2518 | - log.debug('Deleting all messages from {}.'.format(account_id)) |
2519 | - account = self._accounts.pop(str(account_id), None) |
2520 | - if account is not None: |
2521 | - account.protocol._unpublish_all() |
2522 | |
2523 | def _add_new_account(self, account_service): |
2524 | try: |
2525 | @@ -89,14 +69,14 @@ |
2526 | except UnsupportedProtocolError as error: |
2527 | log.info(error) |
2528 | else: |
2529 | - self._accounts[str(new_account.id)] = new_account |
2530 | + self._accounts[new_account.id] = new_account |
2531 | return new_account |
2532 | |
2533 | def get_all(self): |
2534 | return self._accounts.values() |
2535 | |
2536 | def get(self, account_id, default=None): |
2537 | - return self._accounts.get(str(account_id), default) |
2538 | + return self._accounts.get(int(account_id), default) |
2539 | |
2540 | |
2541 | class AuthData: |
2542 | @@ -132,12 +112,12 @@ |
2543 | self.auth = AuthData(account_service.get_auth_data()) |
2544 | # The provider in libaccounts should match the name of our protocol. |
2545 | account = account_service.get_account() |
2546 | + self.id = account.id |
2547 | self.protocol_name = account.get_provider_name() |
2548 | protocol_class = protocol_manager.protocols.get(self.protocol_name) |
2549 | if protocol_class is None: |
2550 | raise UnsupportedProtocolError(self.protocol_name) |
2551 | self.protocol = protocol_class(self) |
2552 | - self.id = str(account.id) |
2553 | # Connect responders to changes in the account information. |
2554 | account_service.connect('changed', self._on_account_changed, account) |
2555 | self._on_account_changed(account_service, account) |
2556 | |
2557 | === modified file 'friends/utils/authentication.py' |
2558 | --- friends/utils/authentication.py 2013-02-05 01:11:35 +0000 |
2559 | +++ friends/utils/authentication.py 2013-03-20 13:27:53 +0000 |
2560 | @@ -66,5 +66,10 @@ |
2561 | def _login_cb(self, session, reply, error, user_data): |
2562 | self._reply = reply |
2563 | if error: |
2564 | - raise AuthorizationError(self.account.id, error.message) |
2565 | + exception = AuthorizationError(self.account.id, error.message) |
2566 | + # Mardy says this error can happen during normal operation. |
2567 | + if error.message.endswith('userActionFinished error: 10'): |
2568 | + log.error(str(exception)) |
2569 | + else: |
2570 | + raise exception |
2571 | log.debug('Login completed') |
2572 | |
2573 | === modified file 'friends/utils/base.py' |
2574 | --- friends/utils/base.py 2013-03-08 02:31:15 +0000 |
2575 | +++ friends/utils/base.py 2013-03-20 13:27:53 +0000 |
2576 | @@ -25,7 +25,6 @@ |
2577 | |
2578 | import re |
2579 | import time |
2580 | -import string |
2581 | import logging |
2582 | import threading |
2583 | |
2584 | @@ -43,23 +42,49 @@ |
2585 | |
2586 | |
2587 | STUB = lambda *ignore, **kwignore: None |
2588 | -IGNORED = string.punctuation + string.whitespace |
2589 | -SCHEME_RE = re.compile('http[s]?://|friends:/', re.IGNORECASE) |
2590 | -EMPTY_STRING = '' |
2591 | COMMA_SPACE = ', ' |
2592 | AVATAR_IDX = COLUMN_INDICES['icon_uri'] |
2593 | FROM_ME_IDX = COLUMN_INDICES['from_me'] |
2594 | STREAM_IDX = COLUMN_INDICES['stream'] |
2595 | SENDER_IDX = COLUMN_INDICES['sender'] |
2596 | MESSAGE_IDX = COLUMN_INDICES['message'] |
2597 | -IDS_IDX = COLUMN_INDICES['message_ids'] |
2598 | +ID_IDX = COLUMN_INDICES['message_id'] |
2599 | +ACCT_IDX = COLUMN_INDICES['account_id'] |
2600 | TIME_IDX = COLUMN_INDICES['timestamp'] |
2601 | |
2602 | - |
2603 | -# This is a mapping from Dee.SharedModel row keys to the DeeModelIters |
2604 | -# representing the rows matching those keys. It is used for quickly finding |
2605 | -# duplicates when we want to insert new rows into the model. |
2606 | -_seen_messages = {} |
2607 | +# See friends/tests/test_protocols.py for further documentation |
2608 | +LINKIFY_REGEX = re.compile( |
2609 | + r""" |
2610 | + # Do not match if URL is preceded by '"' or '>' |
2611 | + # This is used to prevent duplication of linkification. |
2612 | + (?<![\"\>]) |
2613 | + # Record everything that we're about to match. |
2614 | + ( |
2615 | + # URLs can start with 'http://', 'https://', 'ftp://', or 'www.' |
2616 | + (?:(?:https?|ftp)://|www\.) |
2617 | + # Match many non-whitespace characters, but not greedily. |
2618 | + (?:\S+?) |
2619 | + # Stop recording the match. |
2620 | + ) |
2621 | + # This section will peek ahead (without matching) in order to |
2622 | + # determine precisely where the URL actually *ends*. |
2623 | + (?= |
2624 | + # Do not include any trailing period, comma, exclamation mark, |
2625 | + # question mark, or closing parentheses, if any are present. |
2626 | + [.,!?\)]* |
2627 | + # With "trailing" defined as immediately preceding the first |
2628 | + # space, or end-of-string. |
2629 | + (?:\s|$) |
2630 | + # But abort the whole thing if the URL ends with '</a>', |
2631 | + # again to prevent duplication of linkification. |
2632 | + (?!</a>) |
2633 | + )""", |
2634 | + flags=re.VERBOSE).sub |
2635 | + |
2636 | + |
2637 | +# This is a mapping from message_ids to DeeModel row index ints. It is |
2638 | +# used for quickly and easily preventing the same message from being |
2639 | +# published multiple times by mistake. |
2640 | _seen_ids = {} |
2641 | |
2642 | |
2643 | @@ -67,9 +92,6 @@ |
2644 | # publishing new data into the SharedModel. |
2645 | _publish_lock = threading.Lock() |
2646 | |
2647 | -# Avoid race condition during shut-down |
2648 | -_exit_lock = threading.Lock() |
2649 | - |
2650 | |
2651 | log = logging.getLogger(__name__) |
2652 | |
2653 | @@ -92,56 +114,27 @@ |
2654 | return method |
2655 | |
2656 | |
2657 | -def _make_key(row): |
2658 | - """Return a unique key for a row in the model. |
2659 | - |
2660 | - This is used for fuzzy comparisons with messages that are already in the |
2661 | - model. We don't want duplicate messages to show up in the stream of |
2662 | - messages that are visible to the user. But different social media sites |
2663 | - attach different semantic meanings to different punctuation marks, so we |
2664 | - want to ignore those for the sake of determining whether one message is |
2665 | - actually identical to another or not. Thus, we need to strip out this |
2666 | - punctuation for the sake of comparing the strings. For example: |
2667 | - |
2668 | - Fred uses Friends to post identical messages on Twitter and Google+ |
2669 | - (pretend that we support G+ for a moment). Fred writes 'Hey jimbob, been |
2670 | - to http://example.com lately?', and this message might show up on Twitter |
2671 | - like 'Hey @jimbob, been to example.com lately?', but it might show up on |
2672 | - G+ like 'Hey +jimbob, been to http://example.com lately?'. So we need to |
2673 | - strip out all the possibly different bits in order to identify that these |
2674 | - messages are the same for our purposes. In both of these cases, the |
2675 | - string is converted into 'Heyjimbobbeentoexamplecomlately' and then they |
2676 | - compare equally, so we've identified a duplicate message. |
2677 | - """ |
2678 | - # Given a 'row' of data, the sender and message fields are concatenated |
2679 | - # together to form the raw key. Then we strip out details such as url |
2680 | - # schemes, punctuation, and whitespace, that allow for the fuzzy matching. |
2681 | - key = SCHEME_RE.sub('', row[SENDER_IDX] + row[MESSAGE_IDX]) |
2682 | - # Now remove all punctuation and whitespace. |
2683 | - return EMPTY_STRING.join([char for char in key if char not in IGNORED]) |
2684 | - |
2685 | - |
2686 | def initialize_caches(): |
2687 | - """Populate _seen_ids and _seen_messages with Model data. |
2688 | + """Populate _seen_ids with Model data. |
2689 | |
2690 | Our Dee.SharedModel persists across instances, so we need to |
2691 | - populate these caches at launch. |
2692 | + populate this cache at launch. |
2693 | """ |
2694 | - for i in range(Model.get_n_rows()): |
2695 | - row_iter = Model.get_iter_at_row(i) |
2696 | - row = Model.get_row(row_iter) |
2697 | - _seen_messages[_make_key(row)] = i |
2698 | - for triple in row[IDS_IDX]: |
2699 | - _seen_ids[tuple(triple)] = i |
2700 | - log.debug( |
2701 | - '_seen_ids: {}, _seen_messages: {}'.format( |
2702 | - len(_seen_ids), len(_seen_messages))) |
2703 | + # Don't create a new dict; we need to keep the same dict object in |
2704 | + # memory since it gets imported into a few different places that |
2705 | + # would not get the updated reference to the new dict. |
2706 | + _seen_ids.clear() |
2707 | + _seen_ids.update({row[ID_IDX]: i for i, row in enumerate(Model)}) |
2708 | + log.debug('_seen_ids: {}'.format(len(_seen_ids))) |
2709 | + |
2710 | + |
2711 | +def linkify_string(string): |
2712 | + """Finds all URLs in a string and turns them into HTML links.""" |
2713 | + return LINKIFY_REGEX(r'<a href="\1">\1</a>', string) |
2714 | |
2715 | |
2716 | class _OperationThread(threading.Thread): |
2717 | """Manage async callbacks, and log subthread exceptions.""" |
2718 | - # main.py will replace this with a reference to the mainloop.quit method |
2719 | - shutdown = lambda: log.error('Failed to exit friends-dispatcher main loop') |
2720 | |
2721 | def __init__(self, *args, id=None, success=STUB, failure=STUB, **kws): |
2722 | self._id = id |
2723 | @@ -173,13 +166,6 @@ |
2724 | log.debug('{} has completed in {:.2f}s, thread exiting.'.format( |
2725 | self._id, elapsed)) |
2726 | |
2727 | - # If this is the last thread to exit, then the refresh is |
2728 | - # completed and we should save the model, and then exit. |
2729 | - with _exit_lock: |
2730 | - if threading.activeCount() < 3: |
2731 | - persist_model() |
2732 | - GLib.idle_add(self.shutdown) |
2733 | - |
2734 | |
2735 | class Base: |
2736 | """Parent class for any protocol plugin such as Facebook or Twitter. |
2737 | @@ -213,6 +199,8 @@ |
2738 | |
2739 | def __init__(self, account): |
2740 | self._account = account |
2741 | + self._Name = self.__class__.__name__ |
2742 | + self._name = self._Name.lower() |
2743 | |
2744 | def _whoami(self, result): |
2745 | """Use OAuth login results to identify the authenticating user. |
2746 | @@ -240,7 +228,7 @@ |
2747 | """ |
2748 | raise NotImplementedError( |
2749 | '{} protocol has no _whoami() method.'.format( |
2750 | - self.__class__.__name__)) |
2751 | + self._Name)) |
2752 | |
2753 | def receive(self): |
2754 | """Poll the social network for new messages. |
2755 | @@ -279,7 +267,7 @@ |
2756 | """ |
2757 | raise NotImplementedError( |
2758 | '{} protocol has no receive() method.'.format( |
2759 | - self.__class__.__name__)) |
2760 | + self._Name)) |
2761 | |
2762 | def __call__(self, operation, *args, success=STUB, failure=STUB, **kwargs): |
2763 | """Call an operation, i.e. a method, with arguments in a sub-thread. |
2764 | @@ -311,7 +299,7 @@ |
2765 | raise NotImplementedError(operation) |
2766 | method = getattr(self, operation) |
2767 | _OperationThread( |
2768 | - id='{}.{}'.format(self.__class__.__name__, operation), |
2769 | + id='{}.{}'.format(self._Name, operation), |
2770 | target=method, |
2771 | success=success, |
2772 | failure=failure, |
2773 | @@ -323,7 +311,7 @@ |
2774 | """Return the number of rows in the Dee.SharedModel.""" |
2775 | return len(Model) |
2776 | |
2777 | - def _publish(self, message_id, **kwargs): |
2778 | + def _publish(self, **kwargs): |
2779 | """Publish fresh data into the model, ignoring duplicates. |
2780 | |
2781 | This method inserts a new full row into the Dee.SharedModel |
2782 | @@ -352,35 +340,31 @@ |
2783 | present. Otherwise, False is returned if the message could not be |
2784 | appended. |
2785 | """ |
2786 | - # Initialize the row of arguments to contain the message_ids value. |
2787 | - # The column value is a list of lists (see friends/utils/model.py for |
2788 | - # details), and because the arguments are themselves a list, this gets |
2789 | - # initialized as a triply-nested list. |
2790 | - triple = [self.__class__.__name__.lower(), |
2791 | - self._account.id, |
2792 | - message_id] |
2793 | - args = [[triple]] |
2794 | - # Now iterate through all the column names listed in the SCHEMA, |
2795 | - # except for the first, since we just composed its value in the |
2796 | - # preceding line. Pop matching column values from the kwargs, in the |
2797 | - # order which they appear in the SCHEMA. If any are left over at the |
2798 | - # end of this, raise a TypeError indicating the unexpected column |
2799 | - # names. |
2800 | - # |
2801 | - # Missing column values default to the empty string. |
2802 | - for column_name, column_type in SCHEMA[1:]: |
2803 | + # These bits don't need to be set by the caller; we can infer them. |
2804 | + kwargs.update( |
2805 | + dict( |
2806 | + protocol=self._name, |
2807 | + account_id=self._account.id |
2808 | + ) |
2809 | + ) |
2810 | + # linkify the message |
2811 | + kwargs['message'] = linkify_string(kwargs.get('message', '')) |
2812 | + args = [] |
2813 | + # Now iterate through all the column names listed in the |
2814 | + # SCHEMA, and pop matching column values from the kwargs, in |
2815 | + # the order which they appear in the SCHEMA. If any are left |
2816 | + # over at the end of this, raise a TypeError indicating the |
2817 | + # unexpected column names. |
2818 | + for column_name, column_type in SCHEMA: |
2819 | args.append(kwargs.pop(column_name, DEFAULTS[column_type])) |
2820 | if len(kwargs) > 0: |
2821 | raise TypeError('Unexpected keyword arguments: {}'.format( |
2822 | COMMA_SPACE.join(sorted(kwargs)))) |
2823 | with _publish_lock: |
2824 | - # Don't let duplicate messages into the model, but do record the |
2825 | - # unique message ids of each duplicate message. |
2826 | - key = _make_key(args) |
2827 | - row_idx = _seen_messages.get(key) |
2828 | - if row_idx is None: |
2829 | - # We haven't seen this message before. |
2830 | - _seen_messages[key] = Model.get_position(Model.append(*args)) |
2831 | + message_id = args[ID_IDX] |
2832 | + # Don't let duplicate messages into the model |
2833 | + if message_id not in _seen_ids: |
2834 | + _seen_ids[message_id] = Model.get_position(Model.append(*args)) |
2835 | # I think it's safe not to notify the user about |
2836 | # messages that they sent themselves... |
2837 | if not args[FROM_ME_IDX] and self._do_notify(args[STREAM_IDX]): |
2838 | @@ -389,25 +373,7 @@ |
2839 | args[MESSAGE_IDX], |
2840 | args[AVATAR_IDX], |
2841 | ) |
2842 | - else: |
2843 | - # We have seen this before, so append to the matching column's |
2844 | - # message_ids list, this message's id. |
2845 | - row = Model.get_row(Model.get_iter_at_row(row_idx)) |
2846 | - # Remember that row[IDS] is the nested list-of-lists of |
2847 | - # message_ids. args[IDS] is the nested list-of-lists for the |
2848 | - # message that we're publishing. The outer list of the latter |
2849 | - # will always be of size 1. We want to take the inner list |
2850 | - # from args and append it to the list-of-lists (i.e. |
2851 | - # message_ids) of the row already in the model. To make sure |
2852 | - # the model gets updated, we need to insert into the row, thus |
2853 | - # it's best to concatenate the two lists together and store it |
2854 | - # back into the column. |
2855 | - if triple not in row[IDS_IDX]: |
2856 | - row[IDS_IDX] = row[IDS_IDX] + args[IDS_IDX] |
2857 | - # Tuple-ize triple because lists, being mutable, cannot be used as |
2858 | - # dictionary keys. |
2859 | - _seen_ids[tuple(triple)] = _seen_messages.get(key) |
2860 | - return key in _seen_messages |
2861 | + return message_id in _seen_ids |
2862 | |
2863 | def _unpublish(self, message_id): |
2864 | """Remove message_id from the Dee.SharedModel. |
2865 | @@ -416,45 +382,24 @@ |
2866 | published. |
2867 | :type message_id: string |
2868 | """ |
2869 | - triple = (self.__class__.__name__.lower(), |
2870 | - self._account.id, |
2871 | - message_id) |
2872 | - log.debug('Unpublishing {}!'.format(triple)) |
2873 | + log.debug('Unpublishing {}!'.format(message_id)) |
2874 | |
2875 | - row_idx = _seen_ids.pop(triple, None) |
2876 | + row_idx = _seen_ids.pop(message_id, None) |
2877 | if row_idx is None: |
2878 | raise FriendsError('Tried to delete an invalid message id.') |
2879 | |
2880 | - row_iter = Model.get_iter_at_row(row_idx) |
2881 | - row = Model.get_row(row_iter) |
2882 | - |
2883 | - if len(row[IDS_IDX]) == 1: |
2884 | - # Message only exists on one protocol, delete it |
2885 | - del _seen_messages[_make_key(row)] |
2886 | - Model.remove(row_iter) |
2887 | - # Shift our cached indexes up one, when one gets deleted. |
2888 | - for key, value in _seen_ids.items(): |
2889 | - if value > row_idx: |
2890 | - _seen_ids[key] = value - 1 |
2891 | - else: |
2892 | - # Message exists on other protocols too, only drop id |
2893 | - row[IDS_IDX] = [ids for ids |
2894 | - in row[IDS_IDX] |
2895 | - if ids[-1] != message_id] |
2896 | - |
2897 | - def _unpublish_all(self): |
2898 | - """Remove all of this account's messages from the Model. |
2899 | - |
2900 | - Saves the Model to disk after it is done purging rows.""" |
2901 | - for triple in _seen_ids.copy(): |
2902 | - if self._account.id in triple: |
2903 | - self._unpublish(triple[-1]) |
2904 | - persist_model() |
2905 | + Model.remove(Model.get_iter_at_row(row_idx)) |
2906 | + |
2907 | + # Shift our cached indexes up one, when one gets deleted. |
2908 | + for key, value in _seen_ids.items(): |
2909 | + if value > row_idx: |
2910 | + _seen_ids[key] = value - 1 |
2911 | |
2912 | def _get_access_token(self): |
2913 | """Return an access token, logging in if necessary. |
2914 | |
2915 | - :return: The access_token, if we are successfully logged in.""" |
2916 | + :return: The access_token, if we are successfully logged in. |
2917 | + """ |
2918 | if self._account.access_token is None: |
2919 | self._login() |
2920 | |
2921 | @@ -512,15 +457,14 @@ |
2922 | subthread needs to log in. You do not have to worry about |
2923 | subthread race conditions inside this method. |
2924 | """ |
2925 | - protocol = self.__class__.__name__ |
2926 | log.debug('{} to {}'.format( |
2927 | - 'Re-authenticating' if old_token else 'Logging in', protocol)) |
2928 | + 'Re-authenticating' if old_token else 'Logging in', self._Name)) |
2929 | |
2930 | result = Authentication(self._account).login() |
2931 | |
2932 | self._account.access_token = result.get('AccessToken') |
2933 | self._whoami(result) |
2934 | - log.debug('{} UID: {}'.format(protocol, self._account.user_id)) |
2935 | + log.debug('{} UID: {}'.format(self._Name, self._account.user_id)) |
2936 | |
2937 | def _get_oauth_headers(self, method, url, data=None, headers=None): |
2938 | """Basic wrapper around oauthlib that we use for Twitter and Flickr.""" |
2939 | @@ -559,6 +503,16 @@ |
2940 | message = None |
2941 | raise FriendsError(message or str(error)) |
2942 | |
2943 | + def _fetch_cell(self, message_id, column_name): |
2944 | + """Find a column value associated with a specific message_id.""" |
2945 | + row_id = _seen_ids.get(message_id) |
2946 | + col_idx = COLUMN_INDICES.get(column_name) |
2947 | + if None not in (row_id, col_idx): |
2948 | + row = Model.get_row(row_id) |
2949 | + return row[col_idx] |
2950 | + else: |
2951 | + raise FriendsError('Value could not be found.') |
2952 | + |
2953 | def _new_book_client(self, source): |
2954 | client = EBook.BookClient.new(source) |
2955 | client.open_sync(False, None) |
2956 | |
2957 | === modified file 'friends/utils/model.py' |
2958 | --- friends/utils/model.py 2013-02-05 01:11:35 +0000 |
2959 | +++ friends/utils/model.py 2013-03-20 13:27:53 +0000 |
2960 | @@ -41,26 +41,11 @@ |
2961 | log = logging.getLogger(__name__) |
2962 | |
2963 | |
2964 | -# Most of this schema is very straightforward, but the 'message_ids' column |
2965 | -# needs a bit of explanation: |
2966 | -# |
2967 | -# It is a two-dimensional array (ie, an array of arrays). Each inner |
2968 | -# array contains three elements: the name of the protocol |
2969 | -# (introspected from the name of the class that implements the |
2970 | -# protocol), the account_id as a string (like '6' or '3'), followed by |
2971 | -# the message_id for that particular service. |
2972 | -# |
2973 | -# Then, there will be one of these triples present for every service on which |
2974 | -# the message exists. So for example, if the user posts the same message to |
2975 | -# both facebook and twitter, that message will appear as a single row in this |
2976 | -# schema, and the 'message_ids' column will look something like this: |
2977 | -# |
2978 | -# [ |
2979 | -# ['facebook', '2', '12345'], |
2980 | -# ['twitter', '3', '987654'], |
2981 | -# ] |
2982 | +# DO NOT EDIT THIS WITHOUT ADJUSTING service.vala IN LOCKSTEP |
2983 | SCHEMA = ( |
2984 | - ('message_ids', 'aas'), |
2985 | + ('protocol', 's'), # Same as UOA 'provider_name' |
2986 | + ('account_id', 't'), # Same as UOA account id |
2987 | + ('message_id', 's'), |
2988 | ('stream', 's'), |
2989 | ('sender', 's'), |
2990 | ('sender_id', 's'), |
2991 | @@ -70,7 +55,7 @@ |
2992 | ('message', 's'), |
2993 | ('icon_uri', 's'), |
2994 | ('url', 's'), |
2995 | - ('likes', 'd'), |
2996 | + ('likes', 't'), |
2997 | ('liked', 'b'), |
2998 | ('link_picture', 's'), |
2999 | ('link_name', 's'), |
3000 | @@ -78,6 +63,9 @@ |
3001 | ('link_desc', 's'), |
3002 | ('link_caption', 's'), |
3003 | ('link_icon', 's'), |
3004 | + ('location', 's'), |
3005 | + ('latitude', 'd'), |
3006 | + ('longitude', 'd'), |
3007 | ) |
3008 | |
3009 | |
3010 | @@ -91,6 +79,7 @@ |
3011 | 'b': False, |
3012 | 's': '', |
3013 | 'd': 0, |
3014 | + 't': 0, |
3015 | } |
3016 | |
3017 | |
3018 | |
3019 | === modified file 'service/configure.ac' |
3020 | --- service/configure.ac 2013-02-07 23:36:14 +0000 |
3021 | +++ service/configure.ac 2013-03-20 13:27:53 +0000 |
3022 | @@ -18,6 +18,7 @@ |
3023 | |
3024 | DEE_REQUIRED=1.0.0 |
3025 | PKG_CHECK_MODULES(BASE, |
3026 | + libaccounts-glib |
3027 | gio-2.0 |
3028 | dee-1.0 >= $DEE_REQUIRED) |
3029 | |
3030 | |
3031 | === modified file 'service/src/Makefile.am' |
3032 | --- service/src/Makefile.am 2013-02-04 19:42:37 +0000 |
3033 | +++ service/src/Makefile.am 2013-03-20 13:27:53 +0000 |
3034 | @@ -2,14 +2,15 @@ |
3035 | friends-service |
3036 | |
3037 | INCLUDES = \ |
3038 | - $(BASE_CFLAGS) |
3039 | + $(BASE_CFLAGS) |
3040 | |
3041 | VALAFLAGS = \ |
3042 | + --pkg accounts \ |
3043 | --pkg dee-1.0 \ |
3044 | --pkg gio-2.0 |
3045 | |
3046 | friends_service_LDADD = \ |
3047 | - $(BASE_LIBS) |
3048 | + $(BASE_LIBS) |
3049 | |
3050 | friends_service_SOURCES = \ |
3051 | service.vala |
3052 | |
3053 | === modified file 'service/src/service.vala' |
3054 | --- service/src/service.vala 2013-02-20 13:24:44 +0000 |
3055 | +++ service/src/service.vala 2013-03-20 13:27:53 +0000 |
3056 | @@ -16,6 +16,7 @@ |
3057 | * Authored by Ken VanDine <ken.vandine@canonical.com> |
3058 | */ |
3059 | |
3060 | +using Ag; |
3061 | |
3062 | [DBus (name = "com.canonical.Friends.Dispatcher")] |
3063 | private interface Dispatcher : GLib.Object { |
3064 | @@ -35,11 +36,34 @@ |
3065 | private Dee.Model model; |
3066 | private Dee.SharedModel shared_model; |
3067 | private unowned Dee.ResourceManager resources; |
3068 | + private Ag.Manager acct_manager; |
3069 | private Dispatcher dispatcher; |
3070 | public int interval { get; set; } |
3071 | |
3072 | public Master () |
3073 | { |
3074 | + acct_manager = new Ag.Manager.for_service_type ("microblogging"); |
3075 | + acct_manager.account_deleted.connect ((manager, account_id) => { |
3076 | + debug ("Account %u deleted from UOA, purging...", account_id); |
3077 | + uint purged = 0; |
3078 | + uint rows = model.get_n_rows (); |
3079 | + // Destructively iterate over the Model from back to |
3080 | + // front; I know "i < rows" looks kinda goofy here, |
3081 | + // but what's happening is that i is unsigned, so once |
3082 | + // it hits 0, i-- will overflow to a very large |
3083 | + // number, and then "i < rows" will fail, stopping the |
3084 | + // iteration at index 0. |
3085 | + for (uint i = rows - 1; i < rows; i--) { |
3086 | + var itr = model.get_iter_at_row (i); |
3087 | + if (model.get_uint64 (itr, 1) == account_id) { |
3088 | + model.remove (itr); |
3089 | + purged++; |
3090 | + } |
3091 | + } |
3092 | + debug ("Purged %u rows.", purged); |
3093 | + } |
3094 | + ); |
3095 | + |
3096 | resources = Dee.ResourceManager.get_default (); |
3097 | model = new Dee.SequenceModel (); |
3098 | Dee.SequenceModel? _m = null; |
3099 | @@ -50,24 +74,29 @@ |
3100 | debug ("Failed to load model from resource manager: %s", e.message); |
3101 | } |
3102 | |
3103 | - string[] SCHEMA = {"aas", |
3104 | - "s", |
3105 | - "s", |
3106 | - "s", |
3107 | - "s", |
3108 | - "b", |
3109 | + string[] SCHEMA = {"s", |
3110 | + "t", |
3111 | + "s", |
3112 | + "s", |
3113 | + "s", |
3114 | + "s", |
3115 | + "s", |
3116 | + "b", |
3117 | + "s", |
3118 | + "s", |
3119 | + "s", |
3120 | + "s", |
3121 | + "t", |
3122 | + "b", |
3123 | + "s", |
3124 | + "s", |
3125 | + "s", |
3126 | "s", |
3127 | "s", |
3128 | "s", |
3129 | "s", |
3130 | "d", |
3131 | - "b", |
3132 | - "s", |
3133 | - "s", |
3134 | - "s", |
3135 | - "s", |
3136 | - "s", |
3137 | - "s"}; |
3138 | + "d"}; |
3139 | |
3140 | bool schemaReset = false; |
3141 | |
3142 | @@ -77,7 +106,7 @@ |
3143 | // Compare columns from cached model's schema |
3144 | string[] _SCHEMA = _m.get_schema (); |
3145 | if (_SCHEMA.length != SCHEMA.length) |
3146 | - schemaReset = true; |
3147 | + schemaReset = true; |
3148 | else |
3149 | { |
3150 | for (int i=0; i < _SCHEMA.length;i++ ) |
3151 | @@ -87,7 +116,6 @@ |
3152 | debug ("SCHEMA MISMATCH"); |
3153 | schemaReset = true; |
3154 | } |
3155 | - |
3156 | } |
3157 | } |
3158 | if (!schemaReset) |
3159 | |
3160 | === modified file 'setup.py' |
3161 | --- setup.py 2013-02-05 01:11:35 +0000 |
3162 | +++ setup.py 2013-03-20 13:27:53 +0000 |
3163 | @@ -29,6 +29,7 @@ |
3164 | include_package_data=True, |
3165 | package_data = { |
3166 | 'friends.service.templates': ['*.service.in'], |
3167 | + 'friends.tests.data': ['*.dat'], |
3168 | }, |
3169 | data_files = [ |
3170 | ('/usr/share/glib-2.0/schemas', |
3171 | |
3172 | === modified file 'tools/debug_live.py' |
3173 | --- tools/debug_live.py 2013-02-19 09:27:53 +0000 |
3174 | +++ tools/debug_live.py 2013-03-20 13:27:53 +0000 |
3175 | @@ -59,7 +59,7 @@ |
3176 | Model.connect('row-added', row_added) |
3177 | |
3178 | for account in a._accounts.values(): |
3179 | - if account.protocol.__class__.__name__.lower() == protocol.lower(): |
3180 | + if account.protocol._name == protocol.lower(): |
3181 | found = True |
3182 | account.protocol(*args) |
3183 |
All the changes have been reviewed before merging into the raring branch.