Merge lp:~cjohnston/qa-dashboard/user-edit-profile into lp:qa-dashboard
- user-edit-profile
- Merge into dev
Status: | Merged |
---|---|
Approved by: | Chris Johnston |
Approved revision: | 646 |
Merged at revision: | 645 |
Proposed branch: | lp:~cjohnston/qa-dashboard/user-edit-profile |
Merge into: | lp:qa-dashboard |
Diff against target: |
369 lines (+285/-6) 8 files modified
common/forms.py (+24/-0) common/static/css/new-style.css (+84/-0) common/templates/form.html (+26/-0) common/templates/layout.html (+2/-1) common/tests/__init__.py (+1/-0) common/tests/test_profile.py (+116/-0) common/views.py (+31/-5) qa_dashboard/urls.py (+1/-0) |
To merge this branch: | bzr merge lp:~cjohnston/qa-dashboard/user-edit-profile |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan (community) | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Chris Johnston | Needs Resubmitting | ||
Review via email: mp+192043@code.launchpad.net |
Commit message
Adds the ability for a user to edit their profile
Description of the change
Andy Doan (doanac) wrote : | # |
Chris Johnston (cjohnston) wrote : | # |
> 1) This looks a little shady:
> 216 + try:
> 217 + profile = UserProfile.
> 218 + except UserProfile.
> 219 + profile = UserProfile(
> Why would request.user not exist?
>
It isn't trying request.user, it's trying the UserProfile, which a user may or may not have.
> 2) This seems too verbose:
> 165 - <li>Logged in as: <a href="http://
> }}">{{ user.username }}</a></li>
> 166 + <li>Logged in as: <strong><a href="http://
> user.username }}">{{ user.username }}</a><
> 167 + <li><a href="{% url 'edit_profile' %}">Edit profile</a>
>
> Why not just have the logged-in link take you to your profile. ie:
>
> <li>Logged in as: <a href="{% url 'edit_profile' %}">{{ user.username
> }}</a></li>
Is that obvious enough? Would people know that they can click it to change their settings?
- 645. By Chris Johnston
-
Add ability for a user to edit their profile
Andy Doan (doanac) wrote : | # |
> > 2) This seems too verbose:
> > 165 - <li>Logged in as: <a href="http://
> > }}">{{ user.username }}</a></li>
> > 166 + <li>Logged in as: <strong><a href="http://
> > user.username }}">{{ user.username }}</a><
> > 167 + <li><a href="{% url 'edit_profile' %}">Edit profile</a>
> >
> > Why not just have the logged-in link take you to your profile. ie:
> >
> > <li>Logged in as: <a href="{% url 'edit_profile' %}">{{ user.username
> > }}</a></li>
>
> Is that obvious enough? Would people know that they can click it to change
> their settings?
Seemed so to me. Maybe we should ask a few other people though. I guess I just didn't see the point in a link that takes you to launchpad (and off our site)
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:645
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Joe Talbott (joetalbott) wrote : | # |
On Mon, Oct 21, 2013 at 08:09:21PM -0000, Andy Doan wrote:
> > > 2) This seems too verbose:
> > > 165 - <li>Logged in as: <a href="http://
> > > }}">{{ user.username }}</a></li>
> > > 166 + <li>Logged in as: <strong><a href="http://
> > > user.username }}">{{ user.username }}</a><
> > > 167 + <li><a href="{% url 'edit_profile' %}">Edit profile</a>
> > >
> > > Why not just have the logged-in link take you to your profile. ie:
> > >
> > > <li>Logged in as: <a href="{% url 'edit_profile' %}">{{ user.username
> > > }}</a></li>
> >
> > Is that obvious enough? Would people know that they can click it to change
> > their settings?
>
> Seemed so to me. Maybe we should ask a few other people though. I guess I just didn't see the point in a link that takes you to launchpad (and off our site)
I'm +1 for staying on our site.
- 646. By Chris Johnston
-
Update per review
Chris Johnston (cjohnston) : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:646
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Preview Diff
1 | === added file 'common/forms.py' |
2 | --- common/forms.py 1970-01-01 00:00:00 +0000 |
3 | +++ common/forms.py 2013-10-21 21:02:25 +0000 |
4 | @@ -0,0 +1,24 @@ |
5 | +# QA Dashboard |
6 | +# Copyright 2012-2013 Canonical Ltd. |
7 | + |
8 | +# This program is free software: you can redistribute it and/or modify it |
9 | +# under the terms of the GNU Affero General Public License version 3, as |
10 | +# published by the Free Software Foundation. |
11 | + |
12 | +# This program is distributed in the hope that it will be useful, but |
13 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
14 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
15 | +# PURPOSE. See the GNU Affero General Public License for more details. |
16 | + |
17 | +# You should have received a copy of the GNU Affero General Public License |
18 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | + |
20 | +from django.forms import ModelForm |
21 | + |
22 | +from common.models import UserProfile |
23 | + |
24 | + |
25 | +class EditProfileForm(ModelForm): |
26 | + class Meta: |
27 | + model = UserProfile |
28 | + fields = ['use_private_url', ] |
29 | |
30 | === added file 'common/static/css/new-style.css' |
31 | --- common/static/css/new-style.css 1970-01-01 00:00:00 +0000 |
32 | +++ common/static/css/new-style.css 2013-10-21 21:02:25 +0000 |
33 | @@ -0,0 +1,84 @@ |
34 | +form fieldset { |
35 | + border-radius : 4px; |
36 | + background-repeat : no-repeat; |
37 | + background-color : #efeeec; |
38 | + background-position : -15px -15px; |
39 | + border : 0; |
40 | + margin-bottom : 8px; |
41 | + padding : 15px 20px 15px 94px; |
42 | +} |
43 | +form fieldset h3 { |
44 | + border-bottom : 1px dotted #dfdcd9; |
45 | + margin-bottom : 9px; |
46 | + padding-bottom : 10px; |
47 | +} |
48 | +form fieldset li:first-child { |
49 | + margin-top : 0; |
50 | +} |
51 | +form input[type="text"], form textarea { |
52 | + border-radius : 2px; |
53 | + background : #fff; |
54 | + border : #999 solid 1px; |
55 | + font-family : Ubuntu, Arial, "libra sans", sans-serif; |
56 | + display : block; |
57 | + padding : 4px; |
58 | +} |
59 | +form input:focus, form textarea:focus { |
60 | + border : #000 solid 1px; |
61 | +} |
62 | +form textarea[readonly="readonly"] { |
63 | + color : #999; |
64 | +} |
65 | +form input[type="checkbox"], form input[type="radio"] { |
66 | + margin : 0; |
67 | + width : auto; |
68 | + display: block; |
69 | +} |
70 | +form input[type="checkbox"] + label, form input[type="radio"] + label { |
71 | + display : inline; |
72 | + margin-left : 5px; |
73 | + vertical-align : middle; |
74 | + width : auto; |
75 | +} |
76 | +form input[type="submit"] { |
77 | + font-size : 1em; |
78 | + margin-bottom : 0.75em; |
79 | + border-radius : 4px; |
80 | + background-color : #dd4814; |
81 | + box-shadow : none; |
82 | + border : 0; |
83 | + color : #fff; |
84 | + display : block; |
85 | + padding : 10px 14px; |
86 | + text-shadow : none; |
87 | + width : auto; |
88 | + margin-bottom : 0; |
89 | +} |
90 | +form input[type="submit"]:hover { |
91 | + background : #dd4814; |
92 | +} |
93 | +form label { |
94 | + cursor : pointer; |
95 | + display : block; |
96 | + margin-bottom : 4px; |
97 | +} |
98 | +form label span { |
99 | + color : #df382c; |
100 | +} |
101 | +form ul { |
102 | + margin-left : 0; |
103 | +} |
104 | +form li { |
105 | + list-style : none outside none; |
106 | + margin-top : 14px; |
107 | +} |
108 | +form button[type="submit"] { |
109 | + border : 0; |
110 | + display : inline-block; |
111 | + font-family : Ubuntu, Arial, "libra sans", sans-serif; |
112 | + text-decoration : none; |
113 | + font-weight : 300; |
114 | +} |
115 | +form input[type="reset"] { |
116 | + display : none; |
117 | +} |
118 | |
119 | === added file 'common/templates/form.html' |
120 | --- common/templates/form.html 1970-01-01 00:00:00 +0000 |
121 | +++ common/templates/form.html 2013-10-21 21:02:25 +0000 |
122 | @@ -0,0 +1,26 @@ |
123 | +{% extends "smokeng/smoke_layout.html" %} |
124 | + |
125 | +{% block page_name %}{{ form_title }}{%endblock %} |
126 | +{% block content %} |
127 | +<div class="grid_7"> |
128 | + <form action="{{ request.path_info }}" method="POST">{% csrf_token %} |
129 | + <fieldset class='fieldset-1'> |
130 | + <h3>{{ form_title }}</h3> |
131 | + {{ form.as_ul }} |
132 | + <input type="submit" name="submit" value="Save" class="submit-button" /> |
133 | + </fieldset> |
134 | + </form> |
135 | +</div> |
136 | +<script type="text/javascript"><!-- |
137 | +$(document).ready(function(){ |
138 | + $('span[rel*=help]').colorTip({color:'orange'}); |
139 | +}); |
140 | +--></script> |
141 | +<style> |
142 | +form ul { |
143 | + height: 12em; |
144 | + overflow-y: scroll; |
145 | + overflow-x: hidden; |
146 | +} |
147 | +</style> |
148 | +{% endblock %} |
149 | |
150 | === modified file 'common/templates/layout.html' |
151 | --- common/templates/layout.html 2013-10-11 20:43:53 +0000 |
152 | +++ common/templates/layout.html 2013-10-21 21:02:25 +0000 |
153 | @@ -9,6 +9,7 @@ |
154 | <link href='{% static "css/grid.css" %}' rel='stylesheet' type='text/css' /> |
155 | <link href='{% static "css/jquery.dataTables.css" %}' rel='stylesheet' type='text/css' /> |
156 | <link href='{% static "css/style.css" %}' rel='stylesheet' type='text/css' /> |
157 | + <link href='{% static "css/new-style.css" %}' rel='stylesheet' type='text/css' /> |
158 | <title>{% block page_name %}{% endblock %} | Ubuntu CI Dashboard</title> |
159 | <script src='http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js' type="text/javascript"></script> |
160 | <script src='{% static "js/jquery.dataTables.min.js" %}' type="text/javascript"></script> |
161 | @@ -23,7 +24,7 @@ |
162 | <div class="header-login"> |
163 | <ul> |
164 | {% if user.is_authenticated %} |
165 | - <li>Logged in as: <a href="http://launchpad.net/~{{ user.username }}">{{ user.username }}</a></li> |
166 | + <li>Logged in as: <strong><a href="{% url 'edit_profile' %}">{{ user.username }}</a></strong></li> |
167 | {% if user.is_staff %} |
168 | <li><a href="/admin" title="Admin">Admin</a></li> |
169 | {% endif %} |
170 | |
171 | === modified file 'common/tests/__init__.py' |
172 | --- common/tests/__init__.py 2013-10-08 15:22:19 +0000 |
173 | +++ common/tests/__init__.py 2013-10-21 21:02:25 +0000 |
174 | @@ -20,3 +20,4 @@ |
175 | from test_plugins import * |
176 | from test_buildids import * |
177 | from test_myserializer import * |
178 | +from test_profile import * |
179 | |
180 | === added file 'common/tests/test_profile.py' |
181 | --- common/tests/test_profile.py 1970-01-01 00:00:00 +0000 |
182 | +++ common/tests/test_profile.py 2013-10-21 21:02:25 +0000 |
183 | @@ -0,0 +1,116 @@ |
184 | +# QA Dashboard |
185 | +# Copyright 2012-2013 Canonical Ltd. |
186 | + |
187 | +# This program is free software: you can redistribute it and/or modify it |
188 | +# under the terms of the GNU Affero General Public License version 3, as |
189 | +# published by the Free Software Foundation. |
190 | + |
191 | +# This program is distributed in the hope that it will be useful, but |
192 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
193 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
194 | +# PURPOSE. See the GNU Affero General Public License for more details. |
195 | + |
196 | +# You should have received a copy of the GNU Affero General Public License |
197 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
198 | + |
199 | +from django import test as djangotest |
200 | +from django.test.client import Client |
201 | +from django.contrib.auth.models import User |
202 | +from django.core.urlresolvers import reverse |
203 | + |
204 | +from smokeng.models import SmokeImage |
205 | + |
206 | + |
207 | +class UserProfileTestCase(djangotest.TestCase): |
208 | + |
209 | + def setUp(self): |
210 | + self.c = Client() |
211 | + self.user = User.objects.create( |
212 | + username='testuser', |
213 | + first_name='Test', |
214 | + last_name='User', |
215 | + ) |
216 | + self.user.set_password('password') |
217 | + self.user.save() |
218 | + |
219 | + self.image = SmokeImage.objects.create( |
220 | + release="saucy", |
221 | + arch="mako", |
222 | + variant="touch_mir", |
223 | + build_number="20130401", |
224 | + ) |
225 | + self.assert_(self.image) |
226 | + |
227 | + def tearDown(self): |
228 | + self.c.logout() |
229 | + |
230 | + def login(self): |
231 | + logged_in = self.c.login(username='testuser', password='password') |
232 | + self.assertTrue(logged_in) |
233 | + |
234 | + def _get_page_url(self): |
235 | + url = reverse( |
236 | + 'smokeng_overview', |
237 | + args=( |
238 | + self.image.release, |
239 | + ), |
240 | + ) |
241 | + return url |
242 | + |
243 | + def test_not_logged_in_no_edit_profile_link(self): |
244 | + """ |
245 | + Test that a user who is not logged in does not have a link to edit |
246 | + their profile |
247 | + """ |
248 | + response = self.c.get(self._get_page_url()) |
249 | + self.assertContains(response, '/edit/', 0) |
250 | + |
251 | + def test_logged_in_edit_profile_link(self): |
252 | + """ |
253 | + Test that a user that is logged in has a link to edit their profile |
254 | + """ |
255 | + self.login() |
256 | + response = self.c.get(self._get_page_url()) |
257 | + self.assertContains(response, '/edit/', 1) |
258 | + |
259 | + def test_not_logged_in_edit_profile_url(self): |
260 | + """ |
261 | + Test that a user who tries to edit their profile without being logged |
262 | + in is redirected |
263 | + """ |
264 | + response = self.c.get(reverse('edit_profile')) |
265 | + self.assertEquals(response.status_code, 302) |
266 | + |
267 | + def test_edit_profile_url(self): |
268 | + """ |
269 | + Test edit profile url |
270 | + """ |
271 | + self.login() |
272 | + response = self.c.get(reverse('edit_profile')) |
273 | + self.assertEquals(response.status_code, 200) |
274 | + self.assertTemplateUsed(response, 'form.html') |
275 | + |
276 | + def test_user_save_profile(self): |
277 | + """ |
278 | + Tests that a user can update their profile |
279 | + """ |
280 | + self.login() |
281 | + response = self.c.get(reverse('edit_profile')) |
282 | + self.assertEqual(response.status_code, 200) |
283 | + |
284 | + # Define data to fill our the form |
285 | + use_private_url = True |
286 | + user = self.user |
287 | + csrf_token = '%s' % response.context['csrf_token'] |
288 | + |
289 | + # Post the form |
290 | + post = self.c.post( |
291 | + reverse('edit_profile'), |
292 | + data={ |
293 | + 'csrfmiddlewaretoken': csrf_token, |
294 | + 'use_private_url': use_private_url, |
295 | + 'user': user, |
296 | + } |
297 | + ) |
298 | + |
299 | + self.assertEqual(post.status_code, 302) |
300 | |
301 | === modified file 'common/views.py' |
302 | --- common/views.py 2013-10-11 15:24:21 +0000 |
303 | +++ common/views.py 2013-10-21 21:02:25 +0000 |
304 | @@ -13,6 +13,7 @@ |
305 | # You should have received a copy of the GNU Affero General Public License |
306 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
307 | |
308 | +from django.contrib.auth.decorators import login_required |
309 | from django.views.decorators.http import require_GET |
310 | from django.shortcuts import render_to_response |
311 | from django.template import RequestContext |
312 | @@ -21,11 +22,9 @@ |
313 | from django.http import HttpResponseRedirect |
314 | |
315 | from common.plugin_helper import ExtensionManager |
316 | - |
317 | -from common.bread_crumbs import ( |
318 | - BreadCrumb, |
319 | - BreadCrumbTrail, |
320 | -) |
321 | +from common.bread_crumbs import BreadCrumb, BreadCrumbTrail |
322 | +from common.models import UserProfile |
323 | +from common.forms import EditProfileForm |
324 | |
325 | |
326 | @BreadCrumb("CI Dashboard") |
327 | @@ -51,3 +50,30 @@ |
328 | def logout_view(request): |
329 | logout(request) |
330 | return HttpResponseRedirect('/') |
331 | + |
332 | + |
333 | +@login_required |
334 | +def edit_profile(request): |
335 | + form_title = 'Edit profile' |
336 | + try: |
337 | + profile = UserProfile.objects.get(user=request.user) |
338 | + except UserProfile.DoesNotExist: |
339 | + profile = UserProfile(user=request.user) |
340 | + |
341 | + if request.method == 'POST': |
342 | + form = EditProfileForm(data=request.POST, instance=profile) |
343 | + if form.is_valid(): |
344 | + form.save() |
345 | + return HttpResponseRedirect('/') |
346 | + else: |
347 | + form = EditProfileForm(instance=profile) |
348 | + |
349 | + context = { |
350 | + 'form': form, |
351 | + 'form_title': form_title, |
352 | + } |
353 | + return render_to_response( |
354 | + 'form.html', |
355 | + context, |
356 | + RequestContext(request) |
357 | + ) |
358 | |
359 | === modified file 'qa_dashboard/urls.py' |
360 | --- qa_dashboard/urls.py 2013-10-11 15:24:21 +0000 |
361 | +++ qa_dashboard/urls.py 2013-10-21 21:02:25 +0000 |
362 | @@ -23,6 +23,7 @@ |
363 | urlpatterns = patterns( |
364 | '', |
365 | url(r'^$', 'common.views.index', name='index'), |
366 | + url(r'^edit/$', 'common.views.edit_profile', name='edit_profile'), |
367 | url(r'^admin/', include(admin.site.urls)), |
368 | url(r'^\+oops$', 'common.views.cause_oops', name='cause-oops'), |
369 | url(r'^logout$', 'common.views.logout_view', name='logout'), |
1) This looks a little shady: objects. get(user= request. user) DoesNotExist: user=request. user)
216 + try:
217 + profile = UserProfile.
218 + except UserProfile.
219 + profile = UserProfile(
Why would request.user not exist?
2) This seems too verbose: launchpad. net/~{{ user.username }}">{{ user.username }}</a></li> launchpad. net/~{{ user.username }}">{{ user.username }}</a>< /strong> </li>
165 - <li>Logged in as: <a href="http://
166 + <li>Logged in as: <strong><a href="http://
167 + <li><a href="{% url 'edit_profile' %}">Edit profile</a>
Why not just have the logged-in link take you to your profile. ie:
<li>Logged in as: <a href="{% url 'edit_profile' %}">{{ user.username }}</a></li>