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

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

(#7) Show year/month/weekly stats in the dashboard for the user,

including a bar chart for activity during the current week

  • Property mode set to 100644
File size: 26.6 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        response = user_views.profile(john, request)
270        assert response == {}
271
272    def test_login_get(self, dummy_request):
273        """
274        GET request to access the login page
275        """
276        request = dummy_request
277        response = user_views.login(request.root, request)
278        assert response['message'] == ''
279        assert response['email'] == ''
280        assert response['password'] == ''
281        assert response['redirect_url'] == request.resource_url(request.root)
282
283    def test_login_get_return_to(self, dummy_request, john):
284        """
285        GET request to access the login page, if there is a page set to where
286        the user should be sent to, the response "redirect_url" key will have
287        such url
288        """
289        request = dummy_request
290        workout = john.workouts()[0]
291        workout_url = request.resource_url(workout)
292        request.params['return_to'] = workout_url
293        response = user_views.login(request.root, request)
294        assert response['redirect_url'] == workout_url
295
296    def test_login_post_wrong_email(self, dummy_request):
297        request = dummy_request
298        request.method = 'POST'
299        request.POST['submit'] = True
300        request.POST['email'] = 'jack@example.net'
301        response = user_views.login(request.root, request)
302        assert response['message'] == u'Wrong email address'
303
304    def test_login_post_wrong_password(self, dummy_request):
305        request = dummy_request
306        request.method = 'POST'
307        request.POST['submit'] = True
308        request.POST['email'] = 'john.doe@example.net'
309        request.POST['password'] = 'badpassword'
310        response = user_views.login(request.root, request)
311        assert response['message'] == u'Wrong password'
312
313    @patch('ow.views.user.remember')
314    def test_login_post_ok(self, rem, dummy_request, john):
315        request = dummy_request
316        request.method = 'POST'
317        request.POST['submit'] = True
318        request.POST['email'] = 'john.doe@example.net'
319        request.POST['password'] = 's3cr3t'
320        response = user_views.login(request.root, request)
321        assert isinstance(response, HTTPFound)
322        assert rem.called
323        assert response.location == request.resource_url(john)
324
325    @patch('ow.views.user.forget')
326    def test_logout(self, forg, dummy_request):
327        request = dummy_request
328        response = user_views.logout(request.root, request)
329        assert isinstance(response, HTTPFound)
330        assert forg.called
331        assert response.location == request.resource_url(request.root)
332
333    extensions = ('png', 'jpg', 'jpeg', 'gif')
334
335    @pytest.mark.parametrize('extension', extensions)
336    def test_profile_picture(self, extension, dummy_request, john):
337        """
338        GET request to get the profile picture of an user.
339        """
340        request = dummy_request
341        # Get the user
342        user = john
343        # Get the path to the image, then open it and copy it to a new Blob
344        # object
345        path = 'fixtures/image.' + extension
346        image_path = os.path.join(
347            os.path.dirname(os.path.dirname(__file__)), path)
348        blob = Blob()
349        with open(image_path, 'rb') as infile, blob.open('w') as out:
350            infile.seek(0)
351            copyfileobj(infile, out)
352
353        # Set the blob with the picture
354        user.picture = blob
355
356        # Call the profile_picture view
357        response = user_views.profile_picture(user, request)
358        assert isinstance(response, Response)
359        assert response.status_int == 200
360        assert response.content_type == 'image'
361
362    def test_edit_profile_get(self, dummy_request, john):
363        """
364        GET request to the edit profile page, returns the form ready to
365        be rendered
366        """
367        request = dummy_request
368        user = john
369        response = user_views.edit_profile(user, request)
370        assert isinstance(response['form'], OWFormRenderer)
371        # no errors in the form (first load)
372        assert response['form'].errorlist() == ''
373        # the form carries along the proper data keys, taken from the
374        # loaded user profile
375        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
376                'birth_date', 'height', 'weight', 'gender', 'timezone']
377        assert list(response['form'].data.keys()) == data
378        # and check the email to see data is properly loaded
379        assert response['form'].data['email'] == 'john.doe@example.net'
380
381    def test_edit_profile_post_ok(self, profile_post_request, john):
382        request = profile_post_request
383        user = john
384        # Update the bio field
385        bio = 'Some text about this user'
386        request.POST['bio'] = bio
387        response = user_views.edit_profile(user, request)
388        assert isinstance(response, HTTPFound)
389        assert response.location == request.resource_url(user, 'profile')
390        assert user.bio == bio
391
392    def test_edit_profile_post_missing_required(
393            self, profile_post_request, john):
394        request = profile_post_request
395        request.POST['email'] = ''
396        user = john
397        response = user_views.edit_profile(user, request)
398        assert isinstance(response['form'], OWFormRenderer)
399        # error on the missing email field
400        error = u'Please enter an email address'
401        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
402        assert response['form'].errorlist() == html_error
403        assert response['form'].errors_for('email') == [error]
404
405    def test_edit_profile_post_ok_picture_empty_bytes(
406            self, profile_post_request, john):
407        """
408        POST request with an empty picture, the content of
409        request['POST'].picture is a empty bytes string (b'') which triggers
410        a bug in formencode, we put a fix in place, test that
411        (more in ow.user.views.edit_profile)
412        """
413        # for the purposes of this test, we can mock the picture
414        picture = Mock()
415        john.picture = picture
416        request = profile_post_request
417        user = john
418        # Mimic what happens when a picture is not provided by the user
419        request.POST['picture'] = b''
420        response = user_views.edit_profile(user, request)
421        assert isinstance(response, HTTPFound)
422        assert response.location == request.resource_url(user, 'profile')
423        assert user.picture == picture
424
425    def test_edit_profile_post_ok_missing_picture(
426            self, profile_post_request, john):
427        """
428        POST request without picture
429        """
430        # for the purposes of this test, we can mock the picture
431        picture = Mock()
432        john.picture = picture
433        request = profile_post_request
434        user = john
435        # No pic is provided in the request POST values
436        del request.POST['picture']
437        response = user_views.edit_profile(user, request)
438        assert isinstance(response, HTTPFound)
439        assert response.location == request.resource_url(user, 'profile')
440        assert user.picture == picture
441
442    def test_edit_profile_post_ok_nickname(self, profile_post_request, john):
443        """
444        User with a nickname set saves profile without changing the profile,
445        we have to be sure there are no "nickname already in use" errors
446        """
447        request = profile_post_request
448        user = john
449        user.nickname = 'mr_jones'
450        # add the nickname, the default post request has not a nickname set
451        request.POST['nickname'] = 'mr_jones'
452        response = user_views.edit_profile(user, request)
453        assert isinstance(response, HTTPFound)
454        assert response.location == request.resource_url(user, 'profile')
455
456    def test_change_password_get(self, dummy_request, john):
457        request = dummy_request
458        user = john
459        response = user_views.change_password(user, request)
460        assert isinstance(response['form'], OWFormRenderer)
461        # no errors in the form (first load)
462        assert response['form'].errorlist() == ''
463
464    def test_change_password_post_ok(self, passwd_post_request, john):
465        request = passwd_post_request
466        user = john
467        request.POST['old_password'] = 's3cr3t'
468        request.POST['password'] = 'h1dd3n s3cr3t'
469        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
470        response = user_views.change_password(user, request)
471        assert isinstance(response, HTTPFound)
472        assert response.location == request.resource_url(user, 'profile')
473        # password was changed
474        assert not user.check_password('s3cr3t')
475        assert user.check_password('h1dd3n s3cr3t')
476
477    def test_change_password_post_no_values(self, passwd_post_request, john):
478        request = passwd_post_request
479        user = john
480        response = user_views.change_password(user, request)
481        assert isinstance(response['form'], OWFormRenderer)
482        error = u'Please enter a value'
483        html_error = u'<ul class="error">'
484        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
485        html_error += '</ul>'
486        errorlist = response['form'].errorlist().replace('\n', '')
487        assert errorlist == html_error
488        assert response['form'].errors_for('old_password') == [error]
489        assert response['form'].errors_for('password') == [error]
490        assert response['form'].errors_for('password_confirm') == [error]
491        # password was not changed
492        assert user.check_password('s3cr3t')
493
494    def test_change_password_post_bad_old_password(
495            self, passwd_post_request, john):
496        request = passwd_post_request
497        user = john
498        request.POST['old_password'] = 'FAIL PASSWORD'
499        request.POST['password'] = 'h1dd3n s3cr3t'
500        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
501        response = user_views.change_password(user, request)
502        assert isinstance(response['form'], OWFormRenderer)
503        error = u'The given password does not match the existing one '
504        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
505        assert response['form'].errorlist() == html_error
506        assert response['form'].errors_for('old_password') == [error]
507        # password was not changed
508        assert user.check_password('s3cr3t')
509        assert not user.check_password('h1dd3n s3cr3t')
510
511    def test_change_password_post_password_mismatch(
512            self, passwd_post_request, john):
513        request = passwd_post_request
514        user = john
515        request.POST['old_password'] = 's3cr3t'
516        request.POST['password'] = 'h1dd3n s3cr3ts'
517        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
518        response = user_views.change_password(user, request)
519        assert isinstance(response['form'], OWFormRenderer)
520        error = u'Fields do not match'
521        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
522        assert response['form'].errorlist() == html_error
523        assert response['form'].errors_for('password_confirm') == [error]
524        # password was not changed
525        assert user.check_password('s3cr3t')
526        assert not user.check_password('h1dd3n s3cr3t')
527
528    def test_signup_get(self, dummy_request):
529        request = dummy_request
530        response = user_views.signup(request.root, request)
531        assert isinstance(response['form'], OWFormRenderer)
532        # no errors in the form (first load)
533        assert response['form'].errorlist() == ''
534
535    def test_signup_post_ok(self, signup_post_request):
536        request = signup_post_request
537        assert 'jack.black@example.net' not in request.root.emails
538        assert 'JackBlack' not in request.root.all_nicknames
539        response = user_views.signup(request.root, request)
540        assert isinstance(response, HTTPFound)
541        assert response.location == request.resource_url(request.root)
542        assert 'jack.black@example.net' in request.root.emails
543        assert 'JackBlack' in request.root.all_nicknames
544
545    def test_signup_missing_required(self, signup_post_request):
546        request = signup_post_request
547        request.POST['email'] = ''
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['form'], OWFormRenderer)
552        error = u'Please enter an email address'
553        html_error = '<ul class="error">'
554        html_error += '<li>' + error + '</li>'
555        html_error += '</ul>'
556        errorlist = response['form'].errorlist().replace('\n', '')
557        assert errorlist == html_error
558        assert response['form'].errors_for('email') == [error]
559        assert 'jack.black@example.net' not in request.root.emails
560        assert 'JackBlack' not in request.root.all_nicknames
561
562    def test_signup_existing_nickname(self, signup_post_request, john):
563        request = signup_post_request
564        # assign john a nickname first
565        john.nickname = 'john'
566        # now set it for the POST request
567        request.POST['nickname'] = 'john'
568        # check jack is not there yet
569        assert 'jack.black@example.net' not in request.root.emails
570        assert 'JackBlack' not in request.root.all_nicknames
571        # now signup as jack, but trying to set the nickname 'john'
572        response = user_views.signup(request.root, request)
573        assert isinstance(response['form'], OWFormRenderer)
574        error = u'Another user is already using the nickname john'
575        html_error = '<ul class="error">'
576        html_error += '<li>' + error + '</li>'
577        html_error += '</ul>'
578        errorlist = response['form'].errorlist().replace('\n', '')
579        assert errorlist == html_error
580        assert response['form'].errors_for('nickname') == [error]
581        # all the errors, and jack is not there
582        assert 'jack.black@example.net' not in request.root.emails
583        assert 'JackBlack' not in request.root.all_nicknames
584
585    def test_signup_existing_email(self, signup_post_request):
586        request = signup_post_request
587        request.POST['email'] = 'john.doe@example.net'
588        assert 'jack.black@example.net' not in request.root.emails
589        assert 'JackBlack' not in request.root.all_nicknames
590        response = user_views.signup(request.root, request)
591        assert isinstance(response['form'], OWFormRenderer)
592        error = u'Another user is already registered with the email '
593        error += u'john.doe@example.net'
594        html_error = '<ul class="error">'
595        html_error += '<li>' + error + '</li>'
596        html_error += '</ul>'
597        errorlist = response['form'].errorlist().replace('\n', '')
598        assert errorlist == html_error
599        assert response['form'].errors_for('email') == [error]
600        assert 'jack.black@example.net' not in request.root.emails
601        assert 'JackBlack' not in request.root.all_nicknames
602
603    def test_week_stats_no_stats(self, dummy_request, john):
604        response = user_views.week_stats(john, dummy_request)
605        assert isinstance(response, Response)
606        assert response.content_type == 'application/json'
607        # the body is a valid json-encoded stream
608        obj = json.loads(response.body)
609        assert obj == [
610            {'distance': 0, 'elevation': 0, 'name': 'Mon',
611             'time': '00', 'workouts': 0},
612            {'distance': 0, 'elevation': 0, 'name': 'Tue',
613             'time': '00', 'workouts': 0},
614            {'distance': 0, 'elevation': 0, 'name': 'Wed',
615             'time': '00', 'workouts': 0},
616            {'distance': 0, 'elevation': 0, 'name': 'Thu',
617             'time': '00', 'workouts': 0},
618            {'distance': 0, 'elevation': 0, 'name': 'Fri',
619             'time': '00', 'workouts': 0},
620            {'distance': 0, 'elevation': 0, 'name': 'Sat',
621             'time': '00', 'workouts': 0},
622            {'distance': 0, 'elevation': 0, 'name': 'Sun',
623             'time': '00', 'workouts': 0}
624        ]
625
626    def test_week_stats(self, dummy_request, john):
627        workout = Workout(
628            start=datetime.now(timezone.utc),
629            duration=timedelta(minutes=60),
630            distance=30,
631            elevation=540
632        )
633        john.add_workout(workout)
634        response = user_views.week_stats(john, dummy_request)
635        assert isinstance(response, Response)
636        assert response.content_type == 'application/json'
637        # the body is a valid json-encoded stream
638        obj = json.loads(response.body)
639        assert len(obj) == 7
640        for day in obj:
641            if datetime.now(timezone.utc).strftime('%a') == day['name']:
642                day['distance'] == 30
643                day['elevation'] == 540
644                day['time'] == '01'
645                day['workouts'] == 1
646            else:
647                day['distance'] == 0
648                day['elevation'] == 0
649                day['time'] == '00'
650                day['workouts'] == 0
Note: See TracBrowser for help on using the repository browser.