source: OpenWorkouts-current/ow/tests/views/test_user.py @ dbfab70

currentfeature/docs
Last change on this file since dbfab70 was dbfab70, checked in by borja <borja@…>, 5 years ago

(#7) Show workouts in the profile page related to the highlighted/current month

selected in the yearly chart.

Also, allow users to click on the monthly bars on the chart, to choose that month.

  • Property mode set to 100644
File size: 27.2 KB
Line 
1import os
2import json
3from datetime import datetime, timedelta, timezone
4from shutil import copyfileobj
5from unittest.mock import Mock, patch
6
7import pytest
8
9from ZODB.blob import Blob
10
11from pyramid.testing import DummyRequest
12from pyramid.httpexceptions import HTTPFound
13from pyramid.response import Response
14
15from webob.multidict import MultiDict
16
17from ow.models.root import OpenWorkouts
18from ow.models.user import User
19from ow.models.workout import Workout
20from ow.views.renderers import OWFormRenderer
21import ow.views.user as user_views
22
23
24class TestUserViews(object):
25
26    @pytest.fixture
27    def john(self):
28        user = User(firstname='John', lastname='Doe',
29                    email='john.doe@example.net')
30        user.password = 's3cr3t'
31        return user
32
33    @pytest.fixture
34    def root(self, john):
35        root = OpenWorkouts()
36        root.add_user(john)
37        workout = Workout(
38            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
39            duration=timedelta(minutes=60),
40            distance=30
41        )
42        john.add_workout(workout)
43        return root
44
45    @pytest.fixture
46    def dummy_request(self, root):
47        request = DummyRequest()
48        request.root = root
49        return request
50
51    @pytest.fixture
52    def profile_post_request(self, root, john):
53        """
54        This is a valid POST request to update an user profile.
55        Form will validate, but nothing will be really updated/changed.
56        """
57        user = john
58        request = DummyRequest()
59        request.root = root
60        request.method = 'POST'
61        request.POST = MultiDict({
62            'submit': True,
63            'firstname': user.firstname,
64            'lastname': user.lastname,
65            'email': user.email,
66            'bio': user.bio,
67            'weight': user.weight,
68            'height': user.height,
69            'gender': user.gender,
70            'birth_date': user.birth_date,
71            'picture': user.picture,
72            })
73        return request
74
75    @pytest.fixture
76    def passwd_post_request(self, root):
77        """
78        This is a valid POST request to change the user password, but
79        the form will not validate (empty fields)
80        """
81        request = DummyRequest()
82        request.root = root
83        request.method = 'POST'
84        request.POST = MultiDict({
85            'submit': True,
86            'old_password': '',
87            'password': '',
88            'password_confirm': ''
89            })
90        return request
91
92    @pytest.fixture
93    def signup_post_request(self, root):
94        """
95        This is a valid POST request to signup a new user.
96        """
97        request = DummyRequest()
98        request.root = root
99        request.method = 'POST'
100        request.POST = MultiDict({
101            'submit': True,
102            'nickname': 'JackBlack',
103            'email': 'jack.black@example.net',
104            'firstname': 'Jack',
105            'lastname': 'Black',
106            'password': 'j4ck s3cr3t',
107            'password_confirm': 'j4ck s3cr3t'
108            })
109        return request
110
111    def test_dashboard_redirect_unauthenticated(self, root):
112        """
113        Anoymous access to the root object, send the user to the login page.
114
115        Instead of reusing the DummyRequest from the request fixture, we do
116        Mock fully the request here, because we need to use
117        authenticated_userid, which cannot be easily set in the DummyRequest
118        """
119        request = DummyRequest()
120        request.root = root
121        response = user_views.dashboard_redirect(root, request)
122        assert isinstance(response, HTTPFound)
123        assert response.location == request.resource_url(root, 'login')
124
125    def test_dashboard_redirect_authenticated(self, root, john):
126        """
127        Authenticated user accesing the root object, send the user to her
128        dashboard
129
130        Instead of reusing the DummyRequest from the request fixture, we do
131        Mock fully the request here, because we need to use
132        authenticated_userid, which cannot be easily set in the DummyRequest
133        """
134        alt_request = DummyRequest()
135        request = Mock()
136        request.root = root
137        request.authenticated_userid = str(john.uid)
138        request.resource_url = alt_request.resource_url
139        response = user_views.dashboard_redirect(root, request)
140        assert isinstance(response, HTTPFound)
141        assert response.location == request.resource_url(john)
142        # if authenticated_userid is the id of an user that does not exist
143        # anymore, we send the user to the logout page
144        request.authenticated_userid = 'faked-uid'
145        response = user_views.dashboard_redirect(root, request)
146        assert isinstance(response, HTTPFound)
147        assert response.location == request.resource_url(root, 'logout')
148
149    def test_dashboard(self, dummy_request, john):
150        """
151        Renders the user dashboard
152        """
153        request = dummy_request
154        response = user_views.dashboard(john, request)
155        assert len(response) == 6
156        assert 'month_name' in response.keys()
157        assert response['current_year'] == datetime.now().year
158        assert response['current_day_name'] == datetime.now().strftime('%a')
159        # this user has a single workout, in 2015
160        assert response['viewing_year'] == 2015
161        assert response['viewing_month'] == 6
162        assert response['workouts'] == [w for w in john.workouts()]
163
164    def test_dashboard_year(self, dummy_request, john):
165        """
166        Renders the user dashboard for a chosen year.
167        """
168        request = dummy_request
169        # first test the year for which we know there is a workout
170        request.GET['year'] = 2015
171        response = user_views.dashboard(john, request)
172        assert len(response) == 6
173        assert 'month_name' in response.keys()
174        assert response['current_year'] == datetime.now().year
175        assert response['current_day_name'] == datetime.now().strftime('%a')
176        # this user has a single workout, in 2015
177        assert response['viewing_year'] == 2015
178        assert response['viewing_month'] == 6
179        assert response['workouts'] == [w for w in john.workouts()]
180        # now, a year we know there is no workout info
181        request.GET['year'] = 2000
182        response = user_views.dashboard(john, request)
183        assert len(response) == 6
184        assert 'month_name' in response.keys()
185        assert response['current_year'] == datetime.now().year
186        assert response['current_day_name'] == datetime.now().strftime('%a')
187        # this user has a single workout, in 2015
188        assert response['viewing_year'] == 2000
189        # we have no data for that year and we didn't ask for a certain month,
190        # so the passing value for that is None
191        assert response['viewing_month'] is None
192        assert response['workouts'] == []
193
194    def test_dashboard_year_month(self, dummy_request, john):
195        """
196        Renders the user dashboard for a chosen year and month.
197        """
198        request = dummy_request
199        # first test the year/month for which we know there is a workout
200        request.GET['year'] = 2015
201        request.GET['month'] = 6
202        response = user_views.dashboard(john, request)
203        assert len(response) == 6
204        assert 'month_name' in response.keys()
205        assert response['current_year'] == datetime.now().year
206        assert response['current_day_name'] == datetime.now().strftime('%a')
207        # this user has a single workout, in 2015
208        assert response['viewing_year'] == 2015
209        assert response['viewing_month'] == 6
210        assert response['workouts'] == [w for w in john.workouts()]
211        # now, change month to one without values
212        request.GET['month'] = 2
213        response = user_views.dashboard(john, request)
214        assert len(response) == 6
215        assert 'month_name' in response.keys()
216        assert response['current_year'] == datetime.now().year
217        assert response['current_day_name'] == datetime.now().strftime('%a')
218        # this user has a single workout, in 2015
219        assert response['viewing_year'] == 2015
220        assert response['viewing_month'] == 2
221        assert response['workouts'] == []
222        # now the month with data, but in a different year
223        request.GET['year'] = 2010
224        request.GET['month'] = 6
225        response = user_views.dashboard(john, request)
226        assert len(response) == 6
227        assert 'month_name' in response.keys()
228        assert response['current_year'] == datetime.now().year
229        assert response['current_day_name'] == datetime.now().strftime('%a')
230        # this user has a single workout, in 2015
231        assert response['viewing_year'] == 2010
232        assert response['viewing_month'] == 6
233        assert response['workouts'] == []
234
235    def test_dashboard_month(self, dummy_request, john):
236        """
237        Passing a month without a year when rendering the dashboard. The last
238        year for which workout data is available is assumed
239        """
240        request = dummy_request
241        # Set a month without workout data
242        request.GET['month'] = 5
243        response = user_views.dashboard(john, request)
244        assert len(response) == 6
245        assert 'month_name' in response.keys()
246        assert response['current_year'] == datetime.now().year
247        assert response['current_day_name'] == datetime.now().strftime('%a')
248        # this user has a single workout, in 2015
249        assert response['viewing_year'] == 2015
250        assert response['viewing_month'] == 5
251        assert response['workouts'] == []
252        # now a month with data
253        request.GET['month'] = 6
254        response = user_views.dashboard(john, request)
255        assert len(response) == 6
256        assert 'month_name' in response.keys()
257        assert response['current_year'] == datetime.now().year
258        assert response['current_day_name'] == datetime.now().strftime('%a')
259        # this user has a single workout, in 2015
260        assert response['viewing_year'] == 2015
261        assert response['viewing_month'] == 6
262        assert response['workouts'] == [w for w in john.workouts()]
263
264    def test_profile(self, dummy_request, john):
265        """
266        Renders the user profile page
267        """
268        request = dummy_request
269        # profile page for the current day (no workouts avalable)
270        response = user_views.profile(john, request)
271        assert len(response.keys()) == 2
272        current_month = datetime.now(timezone.utc).strftime('%Y-%m')
273        assert response['current_month'] == current_month
274        assert response['workouts'] == []
275        # profile page for a previous date, that has workouts
276        request.GET['year'] = 2015
277        request.GET['month'] = 8
278        response = user_views.profile(john, request)
279        assert len(response.keys()) == 2
280        assert response['current_month'] == '2015-08'
281        assert response['workouts'] == john.workouts(2015, 8)
282
283    def test_login_get(self, dummy_request):
284        """
285        GET request to access the login page
286        """
287        request = dummy_request
288        response = user_views.login(request.root, request)
289        assert response['message'] == ''
290        assert response['email'] == ''
291        assert response['password'] == ''
292        assert response['redirect_url'] == request.resource_url(request.root)
293
294    def test_login_get_return_to(self, dummy_request, john):
295        """
296        GET request to access the login page, if there is a page set to where
297        the user should be sent to, the response "redirect_url" key will have
298        such url
299        """
300        request = dummy_request
301        workout = john.workouts()[0]
302        workout_url = request.resource_url(workout)
303        request.params['return_to'] = workout_url
304        response = user_views.login(request.root, request)
305        assert response['redirect_url'] == workout_url
306
307    def test_login_post_wrong_email(self, dummy_request):
308        request = dummy_request
309        request.method = 'POST'
310        request.POST['submit'] = True
311        request.POST['email'] = 'jack@example.net'
312        response = user_views.login(request.root, request)
313        assert response['message'] == u'Wrong email address'
314
315    def test_login_post_wrong_password(self, dummy_request):
316        request = dummy_request
317        request.method = 'POST'
318        request.POST['submit'] = True
319        request.POST['email'] = 'john.doe@example.net'
320        request.POST['password'] = 'badpassword'
321        response = user_views.login(request.root, request)
322        assert response['message'] == u'Wrong password'
323
324    @patch('ow.views.user.remember')
325    def test_login_post_ok(self, rem, dummy_request, john):
326        request = dummy_request
327        request.method = 'POST'
328        request.POST['submit'] = True
329        request.POST['email'] = 'john.doe@example.net'
330        request.POST['password'] = 's3cr3t'
331        response = user_views.login(request.root, request)
332        assert isinstance(response, HTTPFound)
333        assert rem.called
334        assert response.location == request.resource_url(john)
335
336    @patch('ow.views.user.forget')
337    def test_logout(self, forg, dummy_request):
338        request = dummy_request
339        response = user_views.logout(request.root, request)
340        assert isinstance(response, HTTPFound)
341        assert forg.called
342        assert response.location == request.resource_url(request.root)
343
344    extensions = ('png', 'jpg', 'jpeg', 'gif')
345
346    @pytest.mark.parametrize('extension', extensions)
347    def test_profile_picture(self, extension, dummy_request, john):
348        """
349        GET request to get the profile picture of an user.
350        """
351        request = dummy_request
352        # Get the user
353        user = john
354        # Get the path to the image, then open it and copy it to a new Blob
355        # object
356        path = 'fixtures/image.' + extension
357        image_path = os.path.join(
358            os.path.dirname(os.path.dirname(__file__)), path)
359        blob = Blob()
360        with open(image_path, 'rb') as infile, blob.open('w') as out:
361            infile.seek(0)
362            copyfileobj(infile, out)
363
364        # Set the blob with the picture
365        user.picture = blob
366
367        # Call the profile_picture view
368        response = user_views.profile_picture(user, request)
369        assert isinstance(response, Response)
370        assert response.status_int == 200
371        assert response.content_type == 'image'
372
373    def test_edit_profile_get(self, dummy_request, john):
374        """
375        GET request to the edit profile page, returns the form ready to
376        be rendered
377        """
378        request = dummy_request
379        user = john
380        response = user_views.edit_profile(user, request)
381        assert isinstance(response['form'], OWFormRenderer)
382        # no errors in the form (first load)
383        assert response['form'].errorlist() == ''
384        # the form carries along the proper data keys, taken from the
385        # loaded user profile
386        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
387                'birth_date', 'height', 'weight', 'gender', 'timezone']
388        assert list(response['form'].data.keys()) == data
389        # and check the email to see data is properly loaded
390        assert response['form'].data['email'] == 'john.doe@example.net'
391
392    def test_edit_profile_post_ok(self, profile_post_request, john):
393        request = profile_post_request
394        user = john
395        # Update the bio field
396        bio = 'Some text about this user'
397        request.POST['bio'] = bio
398        response = user_views.edit_profile(user, request)
399        assert isinstance(response, HTTPFound)
400        assert response.location == request.resource_url(user, 'profile')
401        assert user.bio == bio
402
403    def test_edit_profile_post_missing_required(
404            self, profile_post_request, john):
405        request = profile_post_request
406        request.POST['email'] = ''
407        user = john
408        response = user_views.edit_profile(user, request)
409        assert isinstance(response['form'], OWFormRenderer)
410        # error on the missing email field
411        error = u'Please enter an email address'
412        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
413        assert response['form'].errorlist() == html_error
414        assert response['form'].errors_for('email') == [error]
415
416    def test_edit_profile_post_ok_picture_empty_bytes(
417            self, profile_post_request, john):
418        """
419        POST request with an empty picture, the content of
420        request['POST'].picture is a empty bytes string (b'') which triggers
421        a bug in formencode, we put a fix in place, test that
422        (more in ow.user.views.edit_profile)
423        """
424        # for the purposes of this test, we can mock the picture
425        picture = Mock()
426        john.picture = picture
427        request = profile_post_request
428        user = john
429        # Mimic what happens when a picture is not provided by the user
430        request.POST['picture'] = b''
431        response = user_views.edit_profile(user, request)
432        assert isinstance(response, HTTPFound)
433        assert response.location == request.resource_url(user, 'profile')
434        assert user.picture == picture
435
436    def test_edit_profile_post_ok_missing_picture(
437            self, profile_post_request, john):
438        """
439        POST request without picture
440        """
441        # for the purposes of this test, we can mock the picture
442        picture = Mock()
443        john.picture = picture
444        request = profile_post_request
445        user = john
446        # No pic is provided in the request POST values
447        del request.POST['picture']
448        response = user_views.edit_profile(user, request)
449        assert isinstance(response, HTTPFound)
450        assert response.location == request.resource_url(user, 'profile')
451        assert user.picture == picture
452
453    def test_edit_profile_post_ok_nickname(self, profile_post_request, john):
454        """
455        User with a nickname set saves profile without changing the profile,
456        we have to be sure there are no "nickname already in use" errors
457        """
458        request = profile_post_request
459        user = john
460        user.nickname = 'mr_jones'
461        # add the nickname, the default post request has not a nickname set
462        request.POST['nickname'] = 'mr_jones'
463        response = user_views.edit_profile(user, request)
464        assert isinstance(response, HTTPFound)
465        assert response.location == request.resource_url(user, 'profile')
466
467    def test_change_password_get(self, dummy_request, john):
468        request = dummy_request
469        user = john
470        response = user_views.change_password(user, request)
471        assert isinstance(response['form'], OWFormRenderer)
472        # no errors in the form (first load)
473        assert response['form'].errorlist() == ''
474
475    def test_change_password_post_ok(self, passwd_post_request, john):
476        request = passwd_post_request
477        user = john
478        request.POST['old_password'] = 's3cr3t'
479        request.POST['password'] = 'h1dd3n s3cr3t'
480        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
481        response = user_views.change_password(user, request)
482        assert isinstance(response, HTTPFound)
483        assert response.location == request.resource_url(user, 'profile')
484        # password was changed
485        assert not user.check_password('s3cr3t')
486        assert user.check_password('h1dd3n s3cr3t')
487
488    def test_change_password_post_no_values(self, passwd_post_request, john):
489        request = passwd_post_request
490        user = john
491        response = user_views.change_password(user, request)
492        assert isinstance(response['form'], OWFormRenderer)
493        error = u'Please enter a value'
494        html_error = u'<ul class="error">'
495        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
496        html_error += '</ul>'
497        errorlist = response['form'].errorlist().replace('\n', '')
498        assert errorlist == html_error
499        assert response['form'].errors_for('old_password') == [error]
500        assert response['form'].errors_for('password') == [error]
501        assert response['form'].errors_for('password_confirm') == [error]
502        # password was not changed
503        assert user.check_password('s3cr3t')
504
505    def test_change_password_post_bad_old_password(
506            self, passwd_post_request, john):
507        request = passwd_post_request
508        user = john
509        request.POST['old_password'] = 'FAIL PASSWORD'
510        request.POST['password'] = 'h1dd3n s3cr3t'
511        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
512        response = user_views.change_password(user, request)
513        assert isinstance(response['form'], OWFormRenderer)
514        error = u'The given password does not match the existing one '
515        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
516        assert response['form'].errorlist() == html_error
517        assert response['form'].errors_for('old_password') == [error]
518        # password was not changed
519        assert user.check_password('s3cr3t')
520        assert not user.check_password('h1dd3n s3cr3t')
521
522    def test_change_password_post_password_mismatch(
523            self, passwd_post_request, john):
524        request = passwd_post_request
525        user = john
526        request.POST['old_password'] = 's3cr3t'
527        request.POST['password'] = 'h1dd3n s3cr3ts'
528        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
529        response = user_views.change_password(user, request)
530        assert isinstance(response['form'], OWFormRenderer)
531        error = u'Fields do not match'
532        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
533        assert response['form'].errorlist() == html_error
534        assert response['form'].errors_for('password_confirm') == [error]
535        # password was not changed
536        assert user.check_password('s3cr3t')
537        assert not user.check_password('h1dd3n s3cr3t')
538
539    def test_signup_get(self, dummy_request):
540        request = dummy_request
541        response = user_views.signup(request.root, request)
542        assert isinstance(response['form'], OWFormRenderer)
543        # no errors in the form (first load)
544        assert response['form'].errorlist() == ''
545
546    def test_signup_post_ok(self, signup_post_request):
547        request = signup_post_request
548        assert 'jack.black@example.net' not in request.root.emails
549        assert 'JackBlack' not in request.root.all_nicknames
550        response = user_views.signup(request.root, request)
551        assert isinstance(response, HTTPFound)
552        assert response.location == request.resource_url(request.root)
553        assert 'jack.black@example.net' in request.root.emails
554        assert 'JackBlack' in request.root.all_nicknames
555
556    def test_signup_missing_required(self, signup_post_request):
557        request = signup_post_request
558        request.POST['email'] = ''
559        assert 'jack.black@example.net' not in request.root.emails
560        assert 'JackBlack' not in request.root.all_nicknames
561        response = user_views.signup(request.root, request)
562        assert isinstance(response['form'], OWFormRenderer)
563        error = u'Please enter an email address'
564        html_error = '<ul class="error">'
565        html_error += '<li>' + error + '</li>'
566        html_error += '</ul>'
567        errorlist = response['form'].errorlist().replace('\n', '')
568        assert errorlist == html_error
569        assert response['form'].errors_for('email') == [error]
570        assert 'jack.black@example.net' not in request.root.emails
571        assert 'JackBlack' not in request.root.all_nicknames
572
573    def test_signup_existing_nickname(self, signup_post_request, john):
574        request = signup_post_request
575        # assign john a nickname first
576        john.nickname = 'john'
577        # now set it for the POST request
578        request.POST['nickname'] = 'john'
579        # check jack is not there yet
580        assert 'jack.black@example.net' not in request.root.emails
581        assert 'JackBlack' not in request.root.all_nicknames
582        # now signup as jack, but trying to set the nickname 'john'
583        response = user_views.signup(request.root, request)
584        assert isinstance(response['form'], OWFormRenderer)
585        error = u'Another user is already using the nickname john'
586        html_error = '<ul class="error">'
587        html_error += '<li>' + error + '</li>'
588        html_error += '</ul>'
589        errorlist = response['form'].errorlist().replace('\n', '')
590        assert errorlist == html_error
591        assert response['form'].errors_for('nickname') == [error]
592        # all the errors, and jack is not there
593        assert 'jack.black@example.net' not in request.root.emails
594        assert 'JackBlack' not in request.root.all_nicknames
595
596    def test_signup_existing_email(self, signup_post_request):
597        request = signup_post_request
598        request.POST['email'] = 'john.doe@example.net'
599        assert 'jack.black@example.net' not in request.root.emails
600        assert 'JackBlack' not in request.root.all_nicknames
601        response = user_views.signup(request.root, request)
602        assert isinstance(response['form'], OWFormRenderer)
603        error = u'Another user is already registered with the email '
604        error += u'john.doe@example.net'
605        html_error = '<ul class="error">'
606        html_error += '<li>' + error + '</li>'
607        html_error += '</ul>'
608        errorlist = response['form'].errorlist().replace('\n', '')
609        assert errorlist == html_error
610        assert response['form'].errors_for('email') == [error]
611        assert 'jack.black@example.net' not in request.root.emails
612        assert 'JackBlack' not in request.root.all_nicknames
613
614    def test_week_stats_no_stats(self, dummy_request, john):
615        response = user_views.week_stats(john, dummy_request)
616        assert isinstance(response, Response)
617        assert response.content_type == 'application/json'
618        # the body is a valid json-encoded stream
619        obj = json.loads(response.body)
620        assert obj == [
621            {'distance': 0, 'elevation': 0, 'name': 'Mon',
622             'time': '00', 'workouts': 0},
623            {'distance': 0, 'elevation': 0, 'name': 'Tue',
624             'time': '00', 'workouts': 0},
625            {'distance': 0, 'elevation': 0, 'name': 'Wed',
626             'time': '00', 'workouts': 0},
627            {'distance': 0, 'elevation': 0, 'name': 'Thu',
628             'time': '00', 'workouts': 0},
629            {'distance': 0, 'elevation': 0, 'name': 'Fri',
630             'time': '00', 'workouts': 0},
631            {'distance': 0, 'elevation': 0, 'name': 'Sat',
632             'time': '00', 'workouts': 0},
633            {'distance': 0, 'elevation': 0, 'name': 'Sun',
634             'time': '00', 'workouts': 0}
635        ]
636
637    def test_week_stats(self, dummy_request, john):
638        workout = Workout(
639            start=datetime.now(timezone.utc),
640            duration=timedelta(minutes=60),
641            distance=30,
642            elevation=540
643        )
644        john.add_workout(workout)
645        response = user_views.week_stats(john, dummy_request)
646        assert isinstance(response, Response)
647        assert response.content_type == 'application/json'
648        # the body is a valid json-encoded stream
649        obj = json.loads(response.body)
650        assert len(obj) == 7
651        for day in obj:
652            if datetime.now(timezone.utc).strftime('%a') == day['name']:
653                day['distance'] == 30
654                day['elevation'] == 540
655                day['time'] == '01'
656                day['workouts'] == 1
657            else:
658                day['distance'] == 0
659                day['elevation'] == 0
660                day['time'] == '00'
661                day['workouts'] == 0
Note: See TracBrowser for help on using the repository browser.