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
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", "")