source: OpenWorkouts-current/ow/tests/models/test_workout.py @ 1d92bf2

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

(#37) Allow login using email address instead of username:

  • Use user uids as keys in the root folder for referencing user objects (instead of username)
  • Use uids for referencing users all over the place (auth, permissions, traversal urls, etc)
  • Replaced the username concept with nickname. This nickname will be used as a shortcut to access "public profile" pages for users
  • Reworked lots of basic methods in the OpenWorkouts root object (s/username/nickname, marked as properties some methods like users, emails, etc)
  • Added new add_user() and delete_user() helpers to the OpenWorkouts root object
  • Fixed bug in the dashboard redirect view, causing an endless loop if an authenticated user does not exist anymore when loading a page.
  • Lots of tests fixes, adaptations and catch up.
  • Property mode set to 100644
File size: 13.8 KB
Line 
1import os
2from io import BytesIO
3from datetime import datetime, timedelta, timezone
4from decimal import Decimal
5from unittest.mock import patch
6
7import pytest
8from pyramid.security import Allow, Everyone
9
10from ow.models.workout import Workout
11from ow.models.user import User
12from ow.models.root import OpenWorkouts
13
14
15class TestWorkoutModels(object):
16
17    @pytest.fixture
18    def root(self):
19        root = OpenWorkouts()
20        root['john'] = User(firstname='John', lastname='Doe',
21                            email='john.doe@example.net')
22        root['john'].password = 's3cr3t'
23        root['john']['1'] = Workout(
24            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
25            duration=timedelta(minutes=60),
26            distance=30
27        )
28        return root
29
30    def test__acl__(self, root):
31        # First check permissions for a workout without parent
32        permissions = [(Allow, Everyone, 'view'),
33                       (Allow, 'group:admins', 'edit')]
34        workout = Workout()
35        assert workout.__acl__() == permissions
36
37        # Now permissions on a workout that has been added to a user
38        uid = str(root['john'].uid)
39        permissions = [(Allow, uid, 'view'), (Allow, uid, 'edit')]
40        assert root['john']['1'].__acl__() == permissions
41
42    def test_runthrough(self, root):
43        """
44        Just a simple run through to see if those objects click
45        """
46        root['joe'] = User(firstname='Joe', lastname='Di Maggio')
47        assert(u'Joe Di Maggio' == root['joe'].fullname)
48        joe = root['joe']
49        start = datetime(2015, 6, 5, 19, 1, tzinfo=timezone.utc)
50        duration = timedelta(minutes=20)
51        distance = Decimal('0.25')  # 250m
52        w = Workout(start=start, duration=duration, sport='swimming',
53                    notes=u'Yay, I swam!', distance=distance)
54        joe['1'] = w
55        expected = datetime(2015, 6, 5, 19, 21, tzinfo=timezone.utc)
56        assert expected == joe['1'].end
57        assert 250 == joe['1'].distance * 1000
58
59    def test_workout_id(self, root):
60        assert root['john']['1'].workout_id == '1'
61
62    def test_end(self, root):
63        # workout without duration, it has no end
64        workout = Workout()
65        assert workout.end is None
66        # workout with duration, end is start_time + duration
67        expected = datetime(2015, 6, 28, 13, 55, tzinfo=timezone.utc)
68        assert root['john']['1'].end == expected
69
70    def test_start_date(self):
71        start_date = datetime.now()
72        workout = Workout(start=start_date)
73        assert workout.start_date == start_date.strftime('%d/%m/%Y')
74
75    def test_start_time(self):
76        start_date = datetime.now()
77        workout = Workout(start=start_date)
78        assert workout.start_time == start_date.strftime('%H:%M')
79
80    def test_split_duration(self):
81        # return the same hours, minutes, seconds we provided when creating
82        # the workout instance
83        duration = timedelta(hours=1, minutes=30, seconds=15)
84        workout = Workout(duration=duration)
85        assert workout.split_duration() == (1, 30, 15)
86        # If the duration is longer than a day, return the calculation of
87        # in hours, minutes, seconds too
88        duration = timedelta(days=1, hours=1, minutes=30, seconds=15)
89        workout = Workout(duration=duration)
90        assert workout.split_duration() == (25, 30, 15)
91
92    def test_duration_hours_minutes_seconds(self):
93        duration = timedelta(hours=1, minutes=30, seconds=15)
94        workout = Workout(duration=duration)
95        assert workout.duration_hours == '01'
96        assert workout.duration_minutes == '30'
97        assert workout.duration_seconds == '15'
98
99    def test_rounded_distance_no_value(self):
100        workout = Workout()
101        assert workout.rounded_distance == '-'
102
103    def test_rounded_distance(self):
104        workout = Workout()
105        workout.distance = 44.44444444
106        assert workout.rounded_distance == 44.4
107
108    def test_has_hr(self):
109        workout = Workout()
110        assert not workout.has_hr
111        workout.hr_min = 90
112        assert not workout.has_hr
113        workout.hr_max = 180
114        assert not workout.has_hr
115        workout.hr_avg = 120
116        assert workout.has_hr
117
118    def test_hr(self):
119        workout = Workout()
120        assert workout.hr is None
121        workout.hr_min = 90
122        assert workout.hr is None
123        workout.hr_max = 180
124        assert workout.hr is None
125        workout.hr_avg = 120
126        assert workout.hr['min'] == 90
127        assert workout.hr['max'] == 180
128        assert workout.hr['avg'] == 120
129
130    def test_has_cad(self):
131        workout = Workout()
132        assert not workout.has_cad
133        workout.cad_min = 0
134        assert not workout.has_cad
135        workout.cad_max = 110
136        assert not workout.has_cad
137        workout.cad_avg = 50
138        assert workout.has_cad
139
140    def test_cad(self):
141        workout = Workout()
142        assert workout.cad is None
143        workout.cad_min = 0
144        assert workout.cad is None
145        workout.cad_max = 110
146        assert workout.cad is None
147        workout.cad_avg = 50
148        assert workout.cad['min'] == 0
149        assert workout.cad['max'] == 110
150        assert workout.cad['avg'] == 50
151
152    def test_has_atemp(self):
153        workout = Workout()
154        assert not workout.has_atemp
155        workout.atemp_min = 0
156        assert not workout.has_atemp
157        workout.atemp_max = 12
158        assert not workout.has_atemp
159        workout.atemp_avg = 5
160        assert workout.has_atemp
161
162    def test_atemp(self):
163        workout = Workout()
164        assert workout.atemp is None
165        workout.atemp_min = 0
166        assert workout.atemp is None
167        workout.atemp_max = 12
168        assert workout.atemp is None
169        workout.atemp_avg = 5
170        assert workout.atemp['min'] == 0
171        assert workout.atemp['max'] == 12
172        assert workout.atemp['avg'] == 5
173
174    def test_load_from_file_invalid(self):
175        workout = Workout()
176        workout.tracking_filetype = 'alf'
177        with patch.object(workout, 'load_from_gpx') as lfg:
178            workout.load_from_file()
179            assert not lfg.called
180
181    def test_load_from_file_gpx(self):
182        workout = Workout()
183        workout.tracking_filetype = 'gpx'
184        with patch.object(workout, 'load_from_gpx') as lfg:
185            workout.load_from_file()
186            assert lfg.called
187
188    gpx_params = (
189        # GPX 1.0 file, no extensions
190        ('fixtures/20131013.gpx', {
191            'start': datetime(2013, 10, 13, 5, 28, 26, tzinfo=timezone.utc),
192            'duration': timedelta(seconds=27652),
193            'distance': Decimal(98.12598431852807),
194            'title': 'A ride I will never forget',
195            'blob': 'path',
196            'hr': {'min': None, 'max': None, 'avg': None},
197            'cad': {'min': None, 'max': None, 'avg': None},
198            'atemp': {'min': None, 'max': None, 'avg': None}}),
199        # GPX 1.0 file, no extensions, missing elevation
200        ('fixtures/20131013-without-elevation.gpx', {
201            'start': datetime(2013, 10, 13, 5, 28, 26, tzinfo=timezone.utc),
202            'duration': timedelta(seconds=27652),
203            'distance': Decimal(98.12598431852807),
204            'title': 'A ride I will never forget',
205            'blob': None,
206            'hr': {'min': None, 'max': None, 'avg': None},
207            'cad': {'min': None, 'max': None, 'avg': None},
208            'atemp': {'min': None, 'max': None, 'avg': None}}),
209        # GPX 1.1 file with extensions
210        ('fixtures/20160129-with-extensions.gpx', {
211            'start': datetime(2016, 1, 29, 8, 12, 9, tzinfo=timezone.utc),
212            'duration': timedelta(seconds=7028),
213            'distance': Decimal(48.37448557752049237024039030),
214            'title': 'Cota counterclockwise + end bonus',
215            'blob': 'path',
216            'hr': {'min': Decimal(100), 'max': Decimal(175),
217                   'avg': Decimal(148.365864144454008055618032813072)},
218            'cad': {'min': Decimal(0), 'max': Decimal(110),
219                    'avg': Decimal(67.41745485812553740326741187)},
220            'atemp': {'min': Decimal(-4), 'max': Decimal(14),
221                      'avg': Decimal(-0.3869303525365434221840068788)}}),
222        )
223
224    @pytest.mark.parametrize(('filename', 'expected'), gpx_params)
225    def test_load_from_gpx(self, filename, expected):
226        """
227        Load a gpx file located in tests/fixtures using the load_from_gpx()
228        method of the Workout model, then check that certain attrs on the
229        workout are updated correctly
230        """
231        # expected values
232        start = expected['start']
233        duration = expected['duration']
234        distance = expected['distance']
235        title = expected['title']
236        blob = expected['blob']
237        hr = expected['hr']
238        cad = expected['cad']
239        atemp = expected['atemp']
240
241        workout = Workout()
242
243        # Check the values are different by default
244        assert workout.start != start
245        assert workout.duration != duration
246        assert workout.distance != distance
247
248        gpx_file_path = os.path.join(
249            os.path.dirname(os.path.dirname(__file__)), filename)
250        with patch.object(workout, 'tracking_file') as tf:
251            with open(gpx_file_path, 'r') as gpx_file:
252                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
253                # Set the path to the blob object containing the gpx file.
254                # more info in models.workout.Workout.parse_gpx()
255                tf._p_blob_uncommitted = gpx_file_path
256                if blob is None:
257                    # set the uncommited blob to None, mimicing what happens
258                    # with a workout saved into the db (transaction.commit())
259                    tf._p_blob_uncommitted = None
260                    tf._p_blob_committed = gpx_file_path
261                # Without this, has_gpx() will return False
262                workout.tracking_filetype = 'gpx'
263                res = workout.load_from_gpx()
264                assert res is True
265                assert workout.start == start
266                assert workout.duration == duration
267                assert isinstance(workout.distance, Decimal)
268                assert round(workout.distance) == round(distance)
269                # The title of the workout is taken from the gpx file
270                assert workout.title == title
271                for k in hr.keys():
272                    # We use 'fail' as the fallback in the getattr call because
273                    # None is one of the posible values, and we want to be sure
274                    # those attrs are there
275                    #
276                    # The keys are the same for the hr, cad and atemp dicts, so
277                    # we can do all tests in one loop
278                    #
279                    # If the expected value is not None, use round() to avoid
280                    # problems when comparing long Decimal objects
281                    value = getattr(workout, 'hr_'+k, 'fail')
282                    if hr[k] is None:
283                        assert hr[k] == value
284                    else:
285                        assert round(hr[k]) == round(value)
286
287                    value = getattr(workout, 'cad_'+k, 'fail')
288                    if cad[k] is None:
289                        assert cad[k] == value
290                    else:
291                        assert round(cad[k]) == round(value)
292
293                    value = getattr(workout, 'atemp_'+k, 'fail')
294                    if atemp[k] is None:
295                        assert atemp[k] == value
296                    else:
297                        assert round(atemp[k]) == round(value)
298
299    def test_load_from_gpx_no_tracks(self):
300        """
301        If we load an empty (but valid) gpx file (i.e., no tracks information)
302        the attrs on the workout are not updated, the call to load_from_gpx()
303        returns False
304        """
305        workout = Workout()
306
307        # We do not check the start time, it would need some mocking on the
308        # datetime module and there is no need really
309        assert workout.duration is None
310        assert workout.distance is None
311
312        gpx_file_path = os.path.join(
313            os.path.dirname(os.path.dirname(__file__)),
314            'fixtures/empty.gpx')
315        with patch.object(workout, 'tracking_file') as tf:
316            with open(gpx_file_path, 'r') as gpx_file:
317                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
318                res = workout.load_from_gpx()
319                assert res is False
320                assert workout.duration is None
321                assert workout.distance is None
322                assert workout.title == ''
323                for k in ['max', 'min', 'avg']:
324                    for a in ['hr_', 'cad_', 'atemp_']:
325                        assert getattr(workout, a+k, 'fail') is None
326
327    def test_parse_gpx_no_gpx_file(self):
328        """
329        Test the behaviour of parse_gpx() when we call it on a workout without
330        a gpx tracking file. The behaviour of such method when the workout has
331        a gpx tracking file is covered by the test_load_from_gpx() test above
332        """
333        workout = Workout()
334        res = workout.parse_gpx()
335        assert res == {}
336
337    def test_has_tracking_file(self, root):
338        workout = root['john']['1']
339        # without tracking file
340        assert workout.has_tracking_file is False
341        # with tracking file
342        workout.tracking_file = 'faked tracking file'
343        assert workout.has_tracking_file is True
344
345    def test_has_gpx(self, root):
346        workout = root['john']['1']
347        # without tracking file
348        assert workout.has_gpx is False
349        workout.tracking_filetype = 'fit'
350        assert workout.has_gpx is False
351        # with non-gpx tracking file
352        workout.tracking_file = 'faked tracking file'
353        workout.tracking_filetype = 'fit'
354        assert workout.has_gpx is False
355        # with gpx tracking file
356        workout.tracking_file = 'faked tracking file'
357        workout.tracking_filetype = 'gpx'
358        assert workout.has_gpx is True
Note: See TracBrowser for help on using the repository browser.