Merge lp:~the-dod/sahana-eden/twitter-oauth into lp:sahana-eden

Proposed by The Dod
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
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.

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 :)

To post a comment you must log in.
Revision history for this message
Fran Boon (flavour) wrote : Posted in a previous version of this proposal

Hi,

This work looks great - you're really getting to grips with the framework :)

I'm not sure about the naming of these settings:
deployment_settings.oauth.consumer_key=""
deployment_settings.oauth.consumer_secret=""

Would we now have a different oauth consumer key/secret for other applications?
If so then we should have twitter in the name to differentiate, e.g.:
deployment_settings.twitter.oauth_consumer_key=""
deployment_settings.twitter.oauth_consumer_secret=""

& hence:
def get_twitter_oauth_consumer_key(self):
def get_twitter_oauth_consumer_secret(self):

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

review: Needs Information
Revision history for this message
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://twitter.com/apps -form would look like
http://zzzen.com/sahana-twitter-app-reg.jpg

[ The "via" link is a vanity thing. Cool for publicizing the org or a specific event/campaign/etc. See "2 turntables..." link at http://twitter.com/TheRealDod/status/14221966738 ]

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://zzzen.com/sahana-oauth-request.jpg
After clicking allow, twitter returns a PIN for the form, and the rest of the OAuth happens at onvalidate.

Revision history for this message
The Dod (the-dod) wrote : Posted in a previous version of this proposal

oops. s/["create","delete"]/["create","update"]/ (it's only in the comment here. not in the code :) )

1348. By Fran Boon

Merge thedod: Twitter OAuth support using tweepy. Also did general cleanup of Messaging

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'controllers/msg.py'
--- controllers/msg.py 2010-10-10 05:06:31 +0000
+++ controllers/msg.py 2010-10-11 20:32:47 +0000
@@ -12,13 +12,13 @@
1212
13# Options Menu (available in all Functions' Views)13# Options Menu (available in all Functions' Views)
14response.menu_options = [14response.menu_options = [
15 [T("Compose"), False, URL(r=request, c="msg", f="compose")],15 [T("Compose"), False, URL(r=request, c="msg", f="compose")],
16 [T("Distribution groups"), False, URL(r=request, f="group"), [16 [T("Distribution groups"), False, URL(r=request, f="group"), [
17 [T("List/Add"), 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")],18 [T("Group Memberships"), False, URL(r=request, f="group_membership")],
19 ]],19 ]],
20 [T("Log"), False, URL(r=request, f="log")],20 [T("Log"), False, URL(r=request, f="log")],
21 [T("Outbox"), False, URL(r=request, f="outbox")],21 [T("Outbox"), False, URL(r=request, f="outbox")],
22 #["CAP", False, URL(r=request, f="tbc")]22 #["CAP", False, URL(r=request, f="tbc")]
23]23]
2424
@@ -228,8 +228,8 @@
228 _title=T("Delete") + "|" + T("If this is set to True then mails will be deleted from the server after downloading.")))228 _title=T("Delete") + "|" + T("If this is set to True then mails will be deleted from the server after downloading.")))
229229
230 if not auth.has_membership(auth.id_group("Administrator")):230 if not auth.has_membership(auth.id_group("Administrator")):
231 session.error = UNAUTHORISED231 session.error = UNAUTHORISED
232 redirect(URL(r=request, f="index"))232 redirect(URL(r=request, f="index"))
233 # CRUD Strings233 # CRUD Strings
234 ADD_SETTING = T("Add Setting")234 ADD_SETTING = T("Add Setting")
235 VIEW_SETTINGS = T("View Settings")235 VIEW_SETTINGS = T("View Settings")
@@ -251,6 +251,70 @@
251 response.menu_options = admin_menu_options251 response.menu_options = admin_menu_options
252 return shn_rest_controller(module, "email_settings", listadd=False, deletable=False)252 return shn_rest_controller(module, "email_settings", listadd=False, deletable=False)
253253
254@auth.shn_requires_membership(1)
255def twitter_settings():
256 """ RESTful CRUD controller for twitter settings - appears in the administration menu """
257
258 # CRUD Strings
259 ADD_SETTING = T("Add Setting")
260 VIEW_SETTINGS = T("View Settings")
261 s3.crud_strings[tablename] = Storage(
262 title_create = ADD_SETTING,
263 title_display = T("Setting Details"),
264 title_list = VIEW_SETTINGS,
265 title_update = T("Authenticate system's Twitter account"),
266 title_search = T("Search Settings"),
267 subtitle_list = T("Settings"),
268 label_list_button = VIEW_SETTINGS,
269 label_create_button = ADD_SETTING,
270 msg_record_created = T("Setting added"),
271 msg_record_modified = T("System's Twitter account updated"),
272 msg_record_deleted = T("Setting deleted"),
273 msg_list_empty = T("No Settings currently defined")
274 )
275
276 def prep(jr):
277 if not (deployment_settings.oauth.consumer_key and deployment_settings.oauth.consumer_secret):
278 session.error=T("You should edit oauth_settings at models/000_config.py")
279 return True
280 try:
281 import tweepy
282 except:
283 session.error=T("Couldn't import tweepy library")
284 return True
285 oauth = tweepy.OAuthHandler(deployment_settings.oauth.consumer_key,
286 deployment_settings.oauth.consumer_secret)
287
288 resource = request.function
289 tablename = module + "_" + resource
290 table = db[tablename]
291
292 if jr.http == "GET" and jr.method in ["create","update"]: # We're showing the form
293 try:
294 session.s3.twitter_oauth_url = oauth.get_authorization_url()
295 session.s3.twitter_request_key = oauth.request_token.key
296 session.s3.twitter_request_secret = oauth.request_token.secret
297 except tweepy.TweepError:
298 session.error=T("problem connecting to twitter.com - please refresh")
299 return True
300 table.pin.readable = True
301 table.pin.label = SPAN(T("PIN number "),
302 A(T("from Twitter"), _href=T(session.s3.twitter_oauth_url), _target="_blank"),
303 T(" (leave empty to detach account)"))
304 table.pin.value = ""
305 table.twitter_account.label = T("Current twitter account")
306 return True
307 else: # Not showing form, no need for pin
308 table.pin.readable = False
309 table.pin.label = T("PIN") # won't be seen
310 table.pin.value = "" # but let's be on the safe side
311 return True
312 response.s3.prep = prep
313
314 response.menu_options = admin_menu_options
315 return shn_rest_controller(module, "twitter_settings", listadd=False, deletable=False)
316
317
254#--------------------------------------------------------------------------------------------------318#--------------------------------------------------------------------------------------------------
255319
256# The following 2 functions hook into the pr functions320# The following 2 functions hook into the pr functions
@@ -395,16 +459,16 @@
395 return item459 return item
396460
397def process_sms_via_api():461def process_sms_via_api():
398 "Controller for SMS api processing - to be called via cron"462 "Controller for SMS api processing - to be called via cron"
399463
400 msg.process_outbox(contact_method = 2)464 msg.process_outbox(contact_method = 2)
401 return465 return
402466
403def process_email_via_api():467def process_email_via_api():
404 "Controller for Email api processing - to be called via cron"468 "Controller for Email api processing - to be called via cron"
405469
406 msg.process_outbox(contact_method = 1)470 msg.process_outbox(contact_method = 1)
407 return471 return
408472
409def process_sms_via_tropo():473def process_sms_via_tropo():
410 "Controller for SMS tropo processing - to be called via cron"474 "Controller for SMS tropo processing - to be called via cron"
@@ -632,4 +696,3 @@
632 response.s3.pagination = True696 response.s3.pagination = True
633 697
634 return shn_rest_controller(module, resource, listadd=False)698 return shn_rest_controller(module, resource, listadd=False)
635
636699
=== modified file 'models/000_config.py'
--- models/000_config.py 2010-10-10 10:16:05 +0000
+++ models/000_config.py 2010-10-11 20:32:47 +0000
@@ -27,6 +27,14 @@
27deployment_settings.auth.registration_requires_approval = False27deployment_settings.auth.registration_requires_approval = False
28deployment_settings.auth.openid = False28deployment_settings.auth.openid = False
2929
30# Twitter OAuth settings:
31# Register an app at http://twitter.com/apps
32# (select Aplication Type: Client)
33# You'll get your consumer_key and consumer_secret from twitter
34# Keep these empty if you don't need twitter integration
35deployment_settings.oauth.consumer_key=""
36deployment_settings.oauth.consumer_secret=""
37
30# Base settings38# Base settings
31# Set this to the Public URL of the instance39# Set this to the Public URL of the instance
32deployment_settings.base.public_url = "http://127.0.0.1:8000"40deployment_settings.base.public_url = "http://127.0.0.1:8000"
@@ -254,7 +262,7 @@
254 pr_address = {"importer" : True},262 pr_address = {"importer" : True},
255 pr_pe_contact = {"importer" : True},263 pr_pe_contact = {"importer" : True},
256 pr_presence = {"importer" : True},264 pr_presence = {"importer" : True},
257 pr_identity = {"importer" : True},265 pr_identity = {"importer" : True},
258 pr_person = {"importer" : True},266 pr_person = {"importer" : True},
259 pr_group = {"importer" : True},267 pr_group = {"importer" : True},
260 pr_group_membership = {"importer" : True},268 pr_group_membership = {"importer" : True},
@@ -327,18 +335,18 @@
327 # module_type = 10,335 # module_type = 10,
328 # ),336 # ),
329 importer = Storage(337 importer = Storage(
330 name_nice = "Spreadsheet Importer",338 name_nice = "Spreadsheet Importer",
331 description = "Used to import data from spreadsheets into the database",339 description = "Used to import data from spreadsheets into the database",
332 module_type = 10,340 module_type = 10,
333 ),341 ),
334 survey = Storage(342 survey = Storage(
335 name_nice = "Survey Module",343 name_nice = "Survey Module",
336 description = "Create, enter, and manage surveys.",344 description = "Create, enter, and manage surveys.",
337 module_type = 10,345 module_type = 10,
338 )346 )
339 #lms = Storage(347 #lms = Storage(
340 # name_nice = T("Logistics Management System"),348 # name_nice = T("Logistics Management System"),
341 # description = T("An intake system, a warehouse management system, commodity tracking, supply chain management, procurement and other asset and resource management capabilities."),349 # description = T("An intake system, a warehouse management system, commodity tracking, supply chain management, procurement and other asset and resource management capabilities."),
342 # module_type = 10350 # module_type = 10
343 # ),351 # ),
344)
345\ No newline at end of file352\ No newline at end of file
353)
346354
=== modified file 'models/00_utils.py'
--- models/00_utils.py 2010-10-04 21:57:29 +0000
+++ models/00_utils.py 2010-10-11 20:32:47 +0000
@@ -38,7 +38,7 @@
38 # Security Policy38 # Security Policy
39 #session.s3.self_registration = deployment_settings.get_security_self_registration()39 #session.s3.self_registration = deployment_settings.get_security_self_registration()
40 session.s3.security_policy = deployment_settings.get_security_policy()40 session.s3.security_policy = deployment_settings.get_security_policy()
4141
42 # We Audit if either the Global or Module asks us to42 # We Audit if either the Global or Module asks us to
43 # (ignore gracefully if module author hasn't implemented this)43 # (ignore gracefully if module author hasn't implemented this)
44 try:44 try:
4545
=== modified file 'models/01_menu.py'
--- models/01_menu.py 2010-10-09 20:35:42 +0000
+++ models/01_menu.py 2010-10-11 20:32:47 +0000
@@ -91,6 +91,7 @@
91 [T("Messaging"), False, "#",[91 [T("Messaging"), False, "#",[
92 [T("Global Messaging Settings"), False, URL(r=request, c="msg", f="setting", args=[1, "update"])],92 [T("Global Messaging Settings"), False, URL(r=request, c="msg", f="setting", args=[1, "update"])],
93 [T("Email Settings"), False, URL(r=request, c="msg", f="email_settings", args=[1, "update"])],93 [T("Email Settings"), False, URL(r=request, c="msg", f="email_settings", args=[1, "update"])],
94 [T("Twitter Settings"), False, URL(r=request, c="msg", f="twitter_settings", args=[1, "update"])],
94 [T("Modem Settings"), False, URL(r=request, c="msg", f="modem_settings", args=[1, "update"])],95 [T("Modem Settings"), False, URL(r=request, c="msg", f="modem_settings", args=[1, "update"])],
95 [T("Gateway Settings"), False, URL(r=request, c="msg", f="gateway_settings", args=[1, "update"])],96 [T("Gateway Settings"), False, URL(r=request, c="msg", f="gateway_settings", args=[1, "update"])],
96 [T("Tropo Settings"), False, URL(r=request, c="msg", f="tropo_settings", args=[1, "update"])],97 [T("Tropo Settings"), False, URL(r=request, c="msg", f="tropo_settings", args=[1, "update"])],
9798
=== modified file 'models/msg.py'
--- models/msg.py 2010-10-10 20:14:10 +0000
+++ models/msg.py 2010-10-11 20:32:47 +0000
@@ -6,7 +6,6 @@
66
7module = "msg"7module = "msg"
8if deployment_settings.has_module(module):8if deployment_settings.has_module(module):
9
10 # Settings9 # Settings
11 resource = "setting"10 resource = "setting"
12 tablename = "%s_%s" % (module, resource)11 tablename = "%s_%s" % (module, resource)
@@ -18,6 +17,53 @@
18 table.outgoing_sms_handler.requires = IS_IN_SET(["Modem","Gateway","Tropo"], zero=None)17 table.outgoing_sms_handler.requires = IS_IN_SET(["Modem","Gateway","Tropo"], zero=None)
1918
20 #------------------------------------------------------------------------19 #------------------------------------------------------------------------
20 resource="twitter_settings"
21 tablename = "%s_%s" % (module, resource)
22 table = db.define_table(tablename,
23 Field("pin"),
24 Field("oauth_key"),
25 Field("oauth_secret"),
26 Field("twitter_account"),
27 migrate=migrate)
28 table.oauth_key.writable = False
29 table.oauth_secret.writable = False
30
31 ### comment these 2 when debugging
32 table.oauth_key.readable = False
33 table.oauth_secret.readable = False
34
35 table.twitter_account.writable = False
36
37 def twitter_settings_onvalidation(form):
38 """ Complete oauth: take tokens from session + pin from form, and do the 2nd API call to twitter """
39 if form.vars.pin and session.s3.twitter_request_key and session.s3.twitter_request_secret:
40 try:
41 import tweepy
42 except:
43 raise HTTP(501,body="can't import tweepy")
44
45 oauth = tweepy.OAuthHandler(deployment_settings.oauth.consumer_key,
46 deployment_settings.oauth.consumer_secret)
47 oauth.set_request_token(session.s3.twitter_request_key,session.s3.twitter_request_secret)
48 try:
49 oauth.get_access_token(form.vars.pin)
50 form.vars.oauth_key = oauth.access_token.key
51 form.vars.oauth_secret = oauth.access_token.secret
52 twitter = tweepy.API(oauth)
53 form.vars.twitter_account = twitter.me().screen_name
54 form.vars.pin = "" # we won't need it anymore
55 return
56 except tweepy.TweepError:
57 session.error=T("Settings were reset because authenticating with twitter failed")
58 # Either user asked to reset, or error - clear everything
59 for k in ['oauth_key','oauth_secret','twitter_account']:
60 form.vars[k] = None
61 for k in ['twitter_request_key','twitter_request_secret']:
62 session.s3[k] = ""
63
64 s3xrc.model.configure(table, onvalidation=twitter_settings_onvalidation)
65
66 #------------------------------------------------------------------------
21 resource = "email_settings"67 resource = "email_settings"
22 tablename = "%s_%s" % (module, resource)68 tablename = "%s_%s" % (module, resource)
23 table = db.define_table(tablename,69 table = db.define_table(tablename,
2470
=== modified file 'modules/s3cfg.py'
--- modules/s3cfg.py 2010-09-21 06:34:46 +0000
+++ modules/s3cfg.py 2010-10-11 20:32:47 +0000
@@ -11,6 +11,7 @@
11 self.database = Storage()11 self.database = Storage()
12 self.gis = Storage()12 self.gis = Storage()
13 self.mail = Storage()13 self.mail = Storage()
14 self.oauth = Storage()
14 self.L10n = Storage()15 self.L10n = Storage()
15 self.security = Storage()16 self.security = Storage()
16 self.ui = Storage()17 self.ui = Storage()
@@ -107,6 +108,12 @@
107 def get_gis_spatialdb(self):108 def get_gis_spatialdb(self):
108 return self.gis.get("spatialdb", False)109 return self.gis.get("spatialdb", False)
109 110
111 # OAuth settings
112 def get_oauth_consumer_key(self):
113 return self.oauth.get("consumer_key","")
114 def get_oauth_consumer_secret(self):
115 return self.oauth.get("consumer_secret","")
116
110 # L10N Settings117 # L10N Settings
111 def get_L10n_countries(self):118 def get_L10n_countries(self):
112 return self.L10n.get("countries", "")119 return self.L10n.get("countries", "")