Merge lp:~the-dod/sahana-eden/twitter-oauth into lp:sahana-eden
- twitter-oauth
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1348 |
Proposed branch: | lp:~the-dod/sahana-eden/twitter-oauth |
Merge into: | lp:sahana-eden |
Diff against target: |
305 lines (+151/-26) 6 files modified
controllers/msg.py (+79/-16) models/000_config.py (+16/-8) models/00_utils.py (+1/-1) models/01_menu.py (+1/-0) models/msg.py (+47/-1) modules/s3cfg.py (+7/-0) |
To merge this branch: | bzr merge lp:~the-dod/sahana-eden/twitter-oauth |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Fran Boon | Pending | ||
Review via email: mp+38155@code.launchpad.net |
This proposal supersedes a proposal from 2010-10-11.
Commit message
Description of the change
Twitter-OAuth integration, phase 1.
See comment on the commit.
Hope I'm doing everything right being new to s3 and all :)
Fran Boon (flavour) wrote : Posted in a previous version of this proposal | # |
The Dod (the-dod) wrote : Posted in a previous version of this proposal | # |
First - I'd like to add one more resubmission - I've used jr.method to see whether we're in ["create","delete"] or we should make pin.readable False. Looks neater, and saves API calls to twitter too ;)
OAuth:
We can't register a global app for all sahana instances in the world (although it would work), because if you distribute these credentials, they can be used for hanky panky in your name, but it's easy (and requires no human verification) to register a twitter app:
you just login to twitter as @YourOrg or @YourOrgTech (better use an account of a real person or org that tweets "normal" tweets, so that twitter doesn't falsely-detect you as malware), and register an app at http://
http://
[ The "via" link is a vanity thing. Cool for publicizing the org or a specific event/campaign/etc. See "2 turntables..." link at http://
Once app is registered and credentials are in 000_config, site's operator (not necessarily the tech who did the 000_config) logs in to twitter as @YourOrgSahanaBot (this can be a virgin account that represents the sahana instance).
From twitter settings operator then click the "from twitter" link in the update form and gets something like
http://
After clicking allow, twitter returns a PIN for the form, and the rest of the OAuth happens at onvalidate.
The Dod (the-dod) wrote : Posted in a previous version of this proposal | # |
oops. s/["create"
Preview Diff
1 | === modified file 'controllers/msg.py' |
2 | --- controllers/msg.py 2010-10-10 05:06:31 +0000 |
3 | +++ controllers/msg.py 2010-10-11 20:32:47 +0000 |
4 | @@ -12,13 +12,13 @@ |
5 | |
6 | # Options Menu (available in all Functions' Views) |
7 | response.menu_options = [ |
8 | - [T("Compose"), False, URL(r=request, c="msg", f="compose")], |
9 | - [T("Distribution groups"), False, URL(r=request, f="group"), [ |
10 | - [T("List/Add"), False, URL(r=request, f="group")], |
11 | - [T("Group Memberships"), False, URL(r=request, f="group_membership")], |
12 | - ]], |
13 | - [T("Log"), False, URL(r=request, f="log")], |
14 | - [T("Outbox"), False, URL(r=request, f="outbox")], |
15 | + [T("Compose"), False, URL(r=request, c="msg", f="compose")], |
16 | + [T("Distribution groups"), False, URL(r=request, f="group"), [ |
17 | + [T("List/Add"), False, URL(r=request, f="group")], |
18 | + [T("Group Memberships"), False, URL(r=request, f="group_membership")], |
19 | + ]], |
20 | + [T("Log"), False, URL(r=request, f="log")], |
21 | + [T("Outbox"), False, URL(r=request, f="outbox")], |
22 | #["CAP", False, URL(r=request, f="tbc")] |
23 | ] |
24 | |
25 | @@ -228,8 +228,8 @@ |
26 | _title=T("Delete") + "|" + T("If this is set to True then mails will be deleted from the server after downloading."))) |
27 | |
28 | if not auth.has_membership(auth.id_group("Administrator")): |
29 | - session.error = UNAUTHORISED |
30 | - redirect(URL(r=request, f="index")) |
31 | + session.error = UNAUTHORISED |
32 | + redirect(URL(r=request, f="index")) |
33 | # CRUD Strings |
34 | ADD_SETTING = T("Add Setting") |
35 | VIEW_SETTINGS = T("View Settings") |
36 | @@ -251,6 +251,70 @@ |
37 | response.menu_options = admin_menu_options |
38 | return shn_rest_controller(module, "email_settings", listadd=False, deletable=False) |
39 | |
40 | +@auth.shn_requires_membership(1) |
41 | +def twitter_settings(): |
42 | + """ RESTful CRUD controller for twitter settings - appears in the administration menu """ |
43 | + |
44 | + # CRUD Strings |
45 | + ADD_SETTING = T("Add Setting") |
46 | + VIEW_SETTINGS = T("View Settings") |
47 | + s3.crud_strings[tablename] = Storage( |
48 | + title_create = ADD_SETTING, |
49 | + title_display = T("Setting Details"), |
50 | + title_list = VIEW_SETTINGS, |
51 | + title_update = T("Authenticate system's Twitter account"), |
52 | + title_search = T("Search Settings"), |
53 | + subtitle_list = T("Settings"), |
54 | + label_list_button = VIEW_SETTINGS, |
55 | + label_create_button = ADD_SETTING, |
56 | + msg_record_created = T("Setting added"), |
57 | + msg_record_modified = T("System's Twitter account updated"), |
58 | + msg_record_deleted = T("Setting deleted"), |
59 | + msg_list_empty = T("No Settings currently defined") |
60 | + ) |
61 | + |
62 | + def prep(jr): |
63 | + if not (deployment_settings.oauth.consumer_key and deployment_settings.oauth.consumer_secret): |
64 | + session.error=T("You should edit oauth_settings at models/000_config.py") |
65 | + return True |
66 | + try: |
67 | + import tweepy |
68 | + except: |
69 | + session.error=T("Couldn't import tweepy library") |
70 | + return True |
71 | + oauth = tweepy.OAuthHandler(deployment_settings.oauth.consumer_key, |
72 | + deployment_settings.oauth.consumer_secret) |
73 | + |
74 | + resource = request.function |
75 | + tablename = module + "_" + resource |
76 | + table = db[tablename] |
77 | + |
78 | + if jr.http == "GET" and jr.method in ["create","update"]: # We're showing the form |
79 | + try: |
80 | + session.s3.twitter_oauth_url = oauth.get_authorization_url() |
81 | + session.s3.twitter_request_key = oauth.request_token.key |
82 | + session.s3.twitter_request_secret = oauth.request_token.secret |
83 | + except tweepy.TweepError: |
84 | + session.error=T("problem connecting to twitter.com - please refresh") |
85 | + return True |
86 | + table.pin.readable = True |
87 | + table.pin.label = SPAN(T("PIN number "), |
88 | + A(T("from Twitter"), _href=T(session.s3.twitter_oauth_url), _target="_blank"), |
89 | + T(" (leave empty to detach account)")) |
90 | + table.pin.value = "" |
91 | + table.twitter_account.label = T("Current twitter account") |
92 | + return True |
93 | + else: # Not showing form, no need for pin |
94 | + table.pin.readable = False |
95 | + table.pin.label = T("PIN") # won't be seen |
96 | + table.pin.value = "" # but let's be on the safe side |
97 | + return True |
98 | + response.s3.prep = prep |
99 | + |
100 | + response.menu_options = admin_menu_options |
101 | + return shn_rest_controller(module, "twitter_settings", listadd=False, deletable=False) |
102 | + |
103 | + |
104 | #-------------------------------------------------------------------------------------------------- |
105 | |
106 | # The following 2 functions hook into the pr functions |
107 | @@ -395,16 +459,16 @@ |
108 | return item |
109 | |
110 | def process_sms_via_api(): |
111 | - "Controller for SMS api processing - to be called via cron" |
112 | + "Controller for SMS api processing - to be called via cron" |
113 | |
114 | - msg.process_outbox(contact_method = 2) |
115 | - return |
116 | + msg.process_outbox(contact_method = 2) |
117 | + return |
118 | |
119 | def process_email_via_api(): |
120 | - "Controller for Email api processing - to be called via cron" |
121 | + "Controller for Email api processing - to be called via cron" |
122 | |
123 | - msg.process_outbox(contact_method = 1) |
124 | - return |
125 | + msg.process_outbox(contact_method = 1) |
126 | + return |
127 | |
128 | def process_sms_via_tropo(): |
129 | "Controller for SMS tropo processing - to be called via cron" |
130 | @@ -632,4 +696,3 @@ |
131 | response.s3.pagination = True |
132 | |
133 | return shn_rest_controller(module, resource, listadd=False) |
134 | - |
135 | |
136 | === modified file 'models/000_config.py' |
137 | --- models/000_config.py 2010-10-10 10:16:05 +0000 |
138 | +++ models/000_config.py 2010-10-11 20:32:47 +0000 |
139 | @@ -27,6 +27,14 @@ |
140 | deployment_settings.auth.registration_requires_approval = False |
141 | deployment_settings.auth.openid = False |
142 | |
143 | +# Twitter OAuth settings: |
144 | +# Register an app at http://twitter.com/apps |
145 | +# (select Aplication Type: Client) |
146 | +# You'll get your consumer_key and consumer_secret from twitter |
147 | +# Keep these empty if you don't need twitter integration |
148 | +deployment_settings.oauth.consumer_key="" |
149 | +deployment_settings.oauth.consumer_secret="" |
150 | + |
151 | # Base settings |
152 | # Set this to the Public URL of the instance |
153 | deployment_settings.base.public_url = "http://127.0.0.1:8000" |
154 | @@ -254,7 +262,7 @@ |
155 | pr_address = {"importer" : True}, |
156 | pr_pe_contact = {"importer" : True}, |
157 | pr_presence = {"importer" : True}, |
158 | - pr_identity = {"importer" : True}, |
159 | + pr_identity = {"importer" : True}, |
160 | pr_person = {"importer" : True}, |
161 | pr_group = {"importer" : True}, |
162 | pr_group_membership = {"importer" : True}, |
163 | @@ -327,18 +335,18 @@ |
164 | # module_type = 10, |
165 | # ), |
166 | importer = Storage( |
167 | - name_nice = "Spreadsheet Importer", |
168 | - description = "Used to import data from spreadsheets into the database", |
169 | - module_type = 10, |
170 | + name_nice = "Spreadsheet Importer", |
171 | + description = "Used to import data from spreadsheets into the database", |
172 | + module_type = 10, |
173 | ), |
174 | survey = Storage( |
175 | - name_nice = "Survey Module", |
176 | - description = "Create, enter, and manage surveys.", |
177 | - module_type = 10, |
178 | + name_nice = "Survey Module", |
179 | + description = "Create, enter, and manage surveys.", |
180 | + module_type = 10, |
181 | ) |
182 | #lms = Storage( |
183 | # name_nice = T("Logistics Management System"), |
184 | # description = T("An intake system, a warehouse management system, commodity tracking, supply chain management, procurement and other asset and resource management capabilities."), |
185 | # module_type = 10 |
186 | # ), |
187 | -) |
188 | \ No newline at end of file |
189 | +) |
190 | |
191 | === modified file 'models/00_utils.py' |
192 | --- models/00_utils.py 2010-10-04 21:57:29 +0000 |
193 | +++ models/00_utils.py 2010-10-11 20:32:47 +0000 |
194 | @@ -38,7 +38,7 @@ |
195 | # Security Policy |
196 | #session.s3.self_registration = deployment_settings.get_security_self_registration() |
197 | session.s3.security_policy = deployment_settings.get_security_policy() |
198 | - |
199 | + |
200 | # We Audit if either the Global or Module asks us to |
201 | # (ignore gracefully if module author hasn't implemented this) |
202 | try: |
203 | |
204 | === modified file 'models/01_menu.py' |
205 | --- models/01_menu.py 2010-10-09 20:35:42 +0000 |
206 | +++ models/01_menu.py 2010-10-11 20:32:47 +0000 |
207 | @@ -91,6 +91,7 @@ |
208 | [T("Messaging"), False, "#",[ |
209 | [T("Global Messaging Settings"), False, URL(r=request, c="msg", f="setting", args=[1, "update"])], |
210 | [T("Email Settings"), False, URL(r=request, c="msg", f="email_settings", args=[1, "update"])], |
211 | + [T("Twitter Settings"), False, URL(r=request, c="msg", f="twitter_settings", args=[1, "update"])], |
212 | [T("Modem Settings"), False, URL(r=request, c="msg", f="modem_settings", args=[1, "update"])], |
213 | [T("Gateway Settings"), False, URL(r=request, c="msg", f="gateway_settings", args=[1, "update"])], |
214 | [T("Tropo Settings"), False, URL(r=request, c="msg", f="tropo_settings", args=[1, "update"])], |
215 | |
216 | === modified file 'models/msg.py' |
217 | --- models/msg.py 2010-10-10 20:14:10 +0000 |
218 | +++ models/msg.py 2010-10-11 20:32:47 +0000 |
219 | @@ -6,7 +6,6 @@ |
220 | |
221 | module = "msg" |
222 | if deployment_settings.has_module(module): |
223 | - |
224 | # Settings |
225 | resource = "setting" |
226 | tablename = "%s_%s" % (module, resource) |
227 | @@ -18,6 +17,53 @@ |
228 | table.outgoing_sms_handler.requires = IS_IN_SET(["Modem","Gateway","Tropo"], zero=None) |
229 | |
230 | #------------------------------------------------------------------------ |
231 | + resource="twitter_settings" |
232 | + tablename = "%s_%s" % (module, resource) |
233 | + table = db.define_table(tablename, |
234 | + Field("pin"), |
235 | + Field("oauth_key"), |
236 | + Field("oauth_secret"), |
237 | + Field("twitter_account"), |
238 | + migrate=migrate) |
239 | + table.oauth_key.writable = False |
240 | + table.oauth_secret.writable = False |
241 | + |
242 | + ### comment these 2 when debugging |
243 | + table.oauth_key.readable = False |
244 | + table.oauth_secret.readable = False |
245 | + |
246 | + table.twitter_account.writable = False |
247 | + |
248 | + def twitter_settings_onvalidation(form): |
249 | + """ Complete oauth: take tokens from session + pin from form, and do the 2nd API call to twitter """ |
250 | + if form.vars.pin and session.s3.twitter_request_key and session.s3.twitter_request_secret: |
251 | + try: |
252 | + import tweepy |
253 | + except: |
254 | + raise HTTP(501,body="can't import tweepy") |
255 | + |
256 | + oauth = tweepy.OAuthHandler(deployment_settings.oauth.consumer_key, |
257 | + deployment_settings.oauth.consumer_secret) |
258 | + oauth.set_request_token(session.s3.twitter_request_key,session.s3.twitter_request_secret) |
259 | + try: |
260 | + oauth.get_access_token(form.vars.pin) |
261 | + form.vars.oauth_key = oauth.access_token.key |
262 | + form.vars.oauth_secret = oauth.access_token.secret |
263 | + twitter = tweepy.API(oauth) |
264 | + form.vars.twitter_account = twitter.me().screen_name |
265 | + form.vars.pin = "" # we won't need it anymore |
266 | + return |
267 | + except tweepy.TweepError: |
268 | + session.error=T("Settings were reset because authenticating with twitter failed") |
269 | + # Either user asked to reset, or error - clear everything |
270 | + for k in ['oauth_key','oauth_secret','twitter_account']: |
271 | + form.vars[k] = None |
272 | + for k in ['twitter_request_key','twitter_request_secret']: |
273 | + session.s3[k] = "" |
274 | + |
275 | + s3xrc.model.configure(table, onvalidation=twitter_settings_onvalidation) |
276 | + |
277 | + #------------------------------------------------------------------------ |
278 | resource = "email_settings" |
279 | tablename = "%s_%s" % (module, resource) |
280 | table = db.define_table(tablename, |
281 | |
282 | === modified file 'modules/s3cfg.py' |
283 | --- modules/s3cfg.py 2010-09-21 06:34:46 +0000 |
284 | +++ modules/s3cfg.py 2010-10-11 20:32:47 +0000 |
285 | @@ -11,6 +11,7 @@ |
286 | self.database = Storage() |
287 | self.gis = Storage() |
288 | self.mail = Storage() |
289 | + self.oauth = Storage() |
290 | self.L10n = Storage() |
291 | self.security = Storage() |
292 | self.ui = Storage() |
293 | @@ -107,6 +108,12 @@ |
294 | def get_gis_spatialdb(self): |
295 | return self.gis.get("spatialdb", False) |
296 | |
297 | + # OAuth settings |
298 | + def get_oauth_consumer_key(self): |
299 | + return self.oauth.get("consumer_key","") |
300 | + def get_oauth_consumer_secret(self): |
301 | + return self.oauth.get("consumer_secret","") |
302 | + |
303 | # L10N Settings |
304 | def get_L10n_countries(self): |
305 | return self.L10n.get("countries", "") |
Hi,
This work looks great - you're really getting to grips with the framework :)
I'm not sure about the naming of these settings: settings. oauth.consumer_ key="" settings. oauth.consumer_ secret= ""
deployment_
deployment_
Would we now have a different oauth consumer key/secret for other applications? settings. twitter. oauth_consumer_ key="" settings. twitter. oauth_consumer_ secret= ""
If so then we should have twitter in the name to differentiate, e.g.:
deployment_
deployment_
& hence: oauth_consumer_ key(self) : oauth_consumer_ secret( self):
def get_twitter_
def get_twitter_
I'm happy to make the code changes & all below, just want clarity on your understanding.
I'm not sure I like assuming that non-GETs are POSTs, we do support HTTP PUT & DELETE too.
I'd personally prefer to be told about a missing library before a missing configuration as this can be a more significant blocker.
Also a few PEP8 cleanups needed (spaces after commas)- just try to bear it in mind for new code.
Many thanks - help clarify my understanding of OAuth & then I can merge/cleanup :)
F