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