Merge lp:~canonical-isd-hackers/canonical-identity-provider/redirect-url into lp:canonical-identity-provider/release

Proposed by Łukasz Czyżykowski
Status: Merged
Approved by: Łukasz Czyżykowski
Approved revision: no longer in the source branch.
Merged at revision: 126
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/redirect-url
Merge into: lp:canonical-identity-provider/release
Diff against target: 377 lines (+257/-28)
6 files modified
identityprovider/models/account.py (+13/-0)
identityprovider/templates/registration/logout.html (+45/-0)
identityprovider/tests/test_models_account.py (+23/-1)
identityprovider/tests/test_views_ui.py (+2/-6)
identityprovider/tests/test_views_ui_logout.py (+97/-0)
identityprovider/views/ui.py (+77/-21)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/redirect-url
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski (community) Approve
Danny Tamez (community) Needs Fixing
Review via email: mp+36171@code.launchpad.net

Commit message

Added return_to GET argument to /+logout, which enables people getting back to the original page from which they were logged out.

Description of the change

Added return_to GET argument to +logout view.

I created the view as class to easier handle logic in that view.

To post a comment you must log in.
Revision history for this message
Danny Tamez (zematynnad) wrote :

Looks great - just some nit picks...
typo on line 12 of the diff withing
is the test_test_ thing on lines 181 and 185 on purpose?
please put line 259 after 261 and remove the unused import on line 11 of identityprovider/views/ui.py

review: Needs Fixing
Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

Approving after David applied requested changes.

review: Approve
Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

Attempt to merge lp:~canonical-isd-hackers/canonical-identity-provider/redirect-url into lp:canonical-identity-provider failed due to merge conflicts:

text conflict in identityprovider/views/ui.py

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

Approving after resolving merge conflicts.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'identityprovider/models/account.py'
2--- identityprovider/models/account.py 2010-09-20 20:13:55 +0000
3+++ identityprovider/models/account.py 2010-09-22 08:06:21 +0000
4@@ -117,6 +117,19 @@
5 sites = self.openidrpsummary_set.order_by('date_last_used')
6 return sites.reverse()[:limit]
7
8+ def sites_with_active_sessions(self, age_hours=24):
9+ """
10+ Return list of sites on which user *may* have active sessions
11+
12+ Current rule of thumb is to return all sites accessed within previous
13+ 24 hours.
14+ """
15+ max_date_last_used = (datetime.datetime.now() -
16+ datetime.timedelta(hours=age_hours))
17+ sites = self.openidrpsummary_set.filter(
18+ date_last_used__gte=max_date_last_used)
19+ return sites.order_by('-date_last_used')
20+
21 def _get_preferredemail(self):
22 try:
23 return self.emailaddress_set.filter(
24
25=== added file 'identityprovider/templates/registration/logout.html'
26--- identityprovider/templates/registration/logout.html 1970-01-01 00:00:00 +0000
27+++ identityprovider/templates/registration/logout.html 2010-09-22 08:06:21 +0000
28@@ -0,0 +1,45 @@
29+{% extends "base.html" %}
30+{% load i18n %}
31+
32+{% comment %}
33+Copyright 2010 Canonical Ltd. This software is licensed under the
34+GNU Affero General Public License version 3 (see the file LICENSE).
35+{% endcomment %}
36+
37+{% block title %}{% trans "You have been logged out" %}{% endblock %}
38+
39+{% block text_title %}
40+ <h1 class="main">{% trans "You have been logged out" %}</h1>
41+{% endblock %}
42+
43+{% block content %}
44+ <p>{% trans "You have been logged out." %}
45+ {% if return_to_url %}
46+ {% blocktrans with return_to_url as url and return_to_site_name as site_name %}
47+ <span class="big">Return to <a href="{{ return_to_url }}">{{ site_name }}</a></span>{% endblocktrans %}
48+ {% endif %}
49+ </p>
50+
51+ {% if other_sites %}
52+ <div>
53+ <h2 class="main">{% trans "Sites you may still be logged to" %}</h2>
54+ <table class="listing">
55+ <thead>
56+ <tr>
57+ <th class="description">{% trans "Site" %}</th>
58+ <th class="date">{% trans "Last authenticated" %}</th>
59+ </tr>
60+ </thead>
61+ <tbody>
62+ {% for site in other_sites %}
63+ <tr>
64+ <td><a href="{{ site.trust_root }}">{{ site.trust_root }}</a></td>
65+ <td>{{ site.date_last_used|date:"Y-m-d" }}</td>
66+ </tr>
67+ {% endfor %}
68+ </tbody>
69+ </table>
70+ </div>
71+ {% endif %}
72+{% endblock %}
73+
74
75=== modified file 'identityprovider/tests/test_models_account.py'
76--- identityprovider/tests/test_models_account.py 2010-07-13 18:05:31 +0000
77+++ identityprovider/tests/test_models_account.py 2010-09-22 08:06:21 +0000
78@@ -1,7 +1,7 @@
79 # Copyright 2010 Canonical Ltd. This software is licensed under the
80 # GNU Affero General Public License version 3 (see the file LICENSE).
81
82-from datetime import datetime
83+from datetime import datetime, timedelta
84 from django.conf import settings
85 from django.contrib.auth.models import User
86 from identityprovider.tests.utils import BasicAccountTestCase
87@@ -292,6 +292,28 @@
88 self.assertEqual(password.password,
89 encrypt_launchpad_password('password', salt=salt))
90
91+ def test_sites_with_active_sessions_when_returns_empty_result(self):
92+ account = Account.objects.create_account(
93+ 'sites-account', 'sites@example.com', 'password')
94+ sites_count = account.sites_with_active_sessions().count()
95+ self.assertEqual(sites_count, 0)
96+
97+ def test_sites_with_active_sessions_when_one_site_is_active(self):
98+ account = Account.objects.create_account(
99+ 'sites-account', 'sites@example.com', 'password')
100+
101+ account.openidrpsummary_set.create(
102+ date_last_used=datetime.now(),
103+ trust_root='http://trust-root.example.com',
104+ openid_identifier='openid-identifier')
105+
106+ account.openidrpsummary_set.create(
107+ date_last_used=datetime.now() - timedelta(days=3),
108+ trust_root='http://trust-root-2.example.com')
109+
110+ sites_count = account.sites_with_active_sessions().count()
111+ self.assertEqual(sites_count, 1)
112+
113
114 class AccountPasswordTestCase(BasicAccountTestCase):
115 def test_unicode(self):
116
117=== modified file 'identityprovider/tests/test_views_ui.py'
118--- identityprovider/tests/test_views_ui.py 2010-09-21 22:04:10 +0000
119+++ identityprovider/tests/test_views_ui.py 2010-09-22 08:06:21 +0000
120@@ -20,7 +20,7 @@
121 from identityprovider.models import EmailAddress, Person, OpenIDRPConfig
122 from identityprovider.models.const import EmailStatus
123 from identityprovider.views import ui, server
124-from identityprovider.models import Account, AuthToken, EmailAddress
125+from identityprovider.models import Account, AuthToken
126 from identityprovider.models.authtoken import create_token
127 from identityprovider.models.captcha import Captcha
128 from identityprovider.models.const import (AccountStatus, EmailStatus,
129@@ -166,7 +166,7 @@
130 def test_logout(self):
131 self.authenticate()
132 r = self.client.get('/+logout')
133- self.assertRedirects(r, '/+login')
134+ self.assertTemplateUsed(r, 'registration/logout.html')
135
136 def test_logout_preserve_token(self):
137 self.authenticate()
138@@ -182,10 +182,6 @@
139 self.assertEquals(self.client.session.get(token),
140 'raw_orequest content')
141
142- def test_logout_no_preferredlanguage_attribute(self):
143- r = self.client.get('/+logout')
144- self.assertRedirects(r, '/+login')
145-
146 def create_token(self, token_type, email=None, redirection_url=None):
147 token = at.AuthToken.objects.create(
148 token_type=token_type,
149
150=== added file 'identityprovider/tests/test_views_ui_logout.py'
151--- identityprovider/tests/test_views_ui_logout.py 1970-01-01 00:00:00 +0000
152+++ identityprovider/tests/test_views_ui_logout.py 2010-09-22 08:06:21 +0000
153@@ -0,0 +1,97 @@
154+from django.test import TestCase
155+from django.conf import settings
156+from django.http import HttpResponse
157+from django.contrib.sessions.backends.cache import SessionStore
158+
159+from identityprovider.views.ui import LogoutView
160+from identityprovider.models.openidmodels import OpenIDRPConfig
161+from identityprovider.models.const import AccountCreationRationale
162+
163+
164+class LogoutViewTestCase(TestCase):
165+
166+ def setUp(self):
167+ self.view = LogoutView()
168+ self.cookies = {}
169+
170+ def test_get_language_succeeds_when_user_has_attribute(self):
171+ self.preferredlanguage = "en"
172+ self.assertEquals(self.view.get_language(self), "en")
173+ del self.preferredlanguage
174+
175+ def test_get_language_fails_when_user_has_no_attribute(self):
176+ self.assertEquals(self.view.get_language(object()), None)
177+
178+ def set_cookie(self, cookie_name, cookie_value):
179+ self.cookies[cookie_name] = cookie_value
180+
181+ def test_set_language_with_language_equal_to_none(self):
182+ self.view.set_language(self, None)
183+ self.assertEquals(self.cookies, {})
184+
185+ def test_set_language_with_language_equal_to_en(self):
186+ self.view.set_language(self, "en")
187+ self.assertEquals(self.cookies[settings.LANGUAGE_COOKIE_NAME], "en")
188+
189+ def test_set_orequest_when_token_and_orequset_is_passed(self):
190+ session = {}
191+ self.view.set_orequest(session, 'token', 'orequest')
192+ self.assertEquals(session['token'], 'orequest')
193+
194+ def test_set_orequest_when_token_is_none(self):
195+ session = {}
196+ self.view.set_orequest(session, None, 'orequest')
197+ self.assertFalse('orequest' in session.values())
198+
199+ def test_set_orequest_when_orequest_is_none(self):
200+ session = {}
201+ self.view.set_orequest(session, 'toekn', None)
202+ self.assertFalse('token' in session)
203+
204+ def create_openid_rp_config(self, trust_root):
205+ OpenIDRPConfig.objects.create(
206+ trust_root=trust_root,
207+ displayname="Example",
208+ creation_rationale=AccountCreationRationale.UNKNOWN)
209+
210+ def test_get_return_to_url_whithout_referer_and_url_is_recognized(self):
211+ trust_root = 'http://example.com/r'
212+ self.create_openid_rp_config(trust_root)
213+ return_to = self.view.get_return_to_url(trust_root, None)
214+ self.assertEquals(return_to, trust_root)
215+
216+ def test_get_return_to_url_when_referer_mismatches_known_url(self):
217+ trust_root = 'http://example.com/r'
218+ self.create_openid_rp_config(trust_root)
219+ return_to = self.view.get_return_to_url(trust_root,
220+ 'http://r.example.com')
221+ self.assertTrue(return_to is None)
222+
223+ def test_get_return_to_url_when_url_is_unknown(self):
224+ return_to = self.view.get_return_to_url('http://unknown.example.com',
225+ None)
226+ self.assertTrue(return_to is None)
227+
228+ def test_get_return_to_url_when_return_to_is_none(self):
229+ return_to = self.view.get_return_to_url(None, None)
230+
231+ self.assertTrue(return_to is None)
232+
233+
234+ def sites_with_active_sessions(self):
235+ return []
236+
237+ def test_call(self):
238+ """Sanity check test, make sure that all code in __call__ is runable."""
239+ self.user = self
240+ self.session = SessionStore()
241+ self.GET = {}
242+ self.META = {}
243+ response = self.view(self, None)
244+
245+ self.assertTrue(isinstance(response, HttpResponse))
246+
247+ del self.user
248+ del self.session
249+ del self.GET
250+ del self.META
251
252=== modified file 'identityprovider/views/ui.py'
253--- identityprovider/views/ui.py 2010-09-21 22:13:44 +0000
254+++ identityprovider/views/ui.py 2010-09-22 08:06:21 +0000
255@@ -22,21 +22,21 @@
256 from django.utils.translation import ugettext as _
257
258 from identityprovider.branding import current_brand
259-from identityprovider.decorators import (dont_cache, guest_required, limitlogin,
260- requires_cookies)
261+from identityprovider.decorators import (check_readonly, dont_cache,
262+ guest_required, limitlogin, requires_cookies)
263 from identityprovider.forms import (LoginForm, ForgotPasswordForm,
264 ResetPasswordForm, OldNewAccountForm, NewAccountForm, ConfirmNewAccountForm,
265 TokenForm)
266 from identityprovider.models import (Account, AccountPassword, AuthToken,
267 AuthTokenFactory, EmailAddress, get_type_of_token, verify_token_string)
268+from identityprovider.models.captcha import Captcha
269 from identityprovider.models.const import (AccountStatus, EmailStatus,
270 LoginTokenType)
271+from identityprovider.models.openidmodels import OpenIDRPConfig
272 import identityprovider.signed as signed
273 from identityprovider.utils import (encrypt_launchpad_password,
274 format_address, get_person_and_account_by_email, polite_form_errors,
275 CannotResetPasswordException, PersonAndAccountNotFoundException)
276-from identityprovider.decorators import check_readonly
277-from identityprovider.models.captcha import Captcha
278 from identityprovider.views.utils import get_rpconfig, set_session_token_info
279
280
281@@ -98,23 +98,79 @@
282 return True
283
284
285-def logout(request, token=None):
286- request.token = token
287- try:
288- lang = request.user.preferredlanguage
289- except AttributeError:
290- lang = None
291-
292- # We don't want to lose session[token] when we log the user out
293- raw_orequest = request.session.get(token, None)
294- auth.logout(request)
295- if token is not None and raw_orequest is not None:
296- request.session[token] = raw_orequest
297- request.session['message'] = _("You have successfully logged out")
298- response = HttpResponseRedirect('+login')
299- if lang:
300- response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
301- return response
302+class LogoutView(object):
303+
304+ def get_language(self, user):
305+ try:
306+ return user.preferredlanguage
307+ except AttributeError:
308+ return None
309+
310+ def set_language(self, response, language):
311+ if language:
312+ response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
313+
314+ def get_return_to_url(self, return_to, http_referer):
315+ if not return_to:
316+ return None
317+ known = OpenIDRPConfig.objects.filter(
318+ trust_root=return_to).count() == 1
319+ if known:
320+ if http_referer is not None and http_referer != return_to:
321+ return None
322+ return return_to
323+ else:
324+ return None
325+
326+ def set_orequest(self, session, token, raw_orequest):
327+ if token is not None and raw_orequest is not None:
328+ session[token] = raw_orequest
329+
330+ def get_other_sites(self, user, trust_root):
331+ if not hasattr(user, 'sites_with_active_sessions'):
332+ return []
333+ return [site for site in user.sites_with_active_sessions()
334+ if trust_root != site.trust_root]
335+
336+ def get_site_name(self, trust_root):
337+ if not trust_root:
338+ return None
339+ sites = list(OpenIDRPConfig.objects.filter(trust_root=trust_root))
340+ if sites:
341+ return sites[0].displayname
342+ else:
343+ return trust_root
344+
345+ def compute_context(self, user, return_to, referrer):
346+ return_to = self.get_return_to_url(return_to, referrer)
347+
348+ context = {
349+ 'return_to_url': return_to,
350+ 'return_to_site_name': self.get_site_name(return_to),
351+ 'other_sites': self.get_other_sites(user, return_to),
352+ }
353+ return context
354+
355+ def __call__(self, request, token):
356+ lang = self.get_language(request.user)
357+
358+ context = self.compute_context(
359+ request.user,
360+ request.GET.get('return_to', None),
361+ request.META.get('HTTP_REFERER', None))
362+
363+ # We don't want to lose session[token] when we log the user out
364+ raw_orequest = request.session.get(token, None)
365+ auth.logout(request)
366+ self.set_orequest(request.session, token, raw_orequest)
367+
368+ response = render_to_response('registration/logout.html', context)
369+
370+ self.set_language(response, lang)
371+ return response
372+
373+
374+logout = LogoutView()
375
376
377 def enter_token(request, token=None):