source: OpenWorkouts-current/ow/views/workout.py @ 55470f9

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

Fixed some tests broken during the last code changes.
Fixed a bug in the calculations of the totals for the profile page
(we weren't taking in account possible None values for distance,
duration and specially elevation/uphill)

  • Property mode set to 100644
File size: 8.8 KB
Line 
1from decimal import Decimal
2from datetime import datetime, timedelta, time, timezone
3
4import gpxpy
5
6from pyramid.httpexceptions import HTTPFound, HTTPNotFound
7from pyramid.view import view_config
8from pyramid.response import Response
9from pyramid_simpleform import Form
10from pyramid_simpleform.renderers import FormRenderer
11
12from ..schemas.workout import (
13    UploadedWorkoutSchema,
14    ManualWorkoutSchema,
15    UpdateWorkoutSchema
16)
17from ..models.workout import Workout
18from ..models.user import User
19from ..utilities import slugify
20from ..catalog import get_catalog, reindex_object, remove_from_catalog
21
22
23@view_config(
24    context=User,
25    name='add-workout-manually',
26    renderer='ow:templates/add_manual_workout.pt')
27def add_workout_manually(context, request):
28    form = Form(request, schema=ManualWorkoutSchema(),
29                defaults={'duration_hours': '0',
30                          'duration_minutes': '0',
31                          'duration_seconds': '0'})
32
33    if 'submit' in request.POST and form.validate():
34        # exclude the three duration_* and start_* fields, so they won't be
35        # "bind" to the object, we do calculate both the full duration in
36        # seconds and the full datetime "start" and we save that
37        excluded = ['duration_hours', 'duration_minutes', 'duration_seconds',
38                    'start_date', 'start_time']
39        workout = form.bind(Workout(), exclude=excluded)
40        duration = timedelta(hours=form.data['duration_hours'],
41                             minutes=form.data['duration_minutes'],
42                             seconds=form.data['duration_seconds'])
43        workout.duration = duration
44        # create a time object first using the given hours and minutes
45        start_time = time(form.data['start_time'][0],
46                          form.data['start_time'][1])
47        # combine the given start date with the built time object
48        start = datetime.combine(form.data['start_date'], start_time,
49                                 tzinfo=timezone.utc)
50        workout.start = start
51        context.add_workout(workout)
52        return HTTPFound(location=request.resource_url(workout))
53
54    return {
55        'form': FormRenderer(form)
56    }
57
58
59@view_config(
60    context=User,
61    name='add-workout',
62    renderer='ow:templates/add_workout.pt')
63def add_workout(context, request):
64    """
65    Add a workout uploading a tracking file
66    """
67    # if not given a file there is an empty byte in POST, which breaks
68    # our blob storage validator.
69    # dirty fix until formencode fixes its api.is_empty method
70    if isinstance(request.POST.get('tracking_file', None), bytes):
71        request.POST['tracking_file'] = ''
72
73    form = Form(request, schema=UploadedWorkoutSchema())
74
75    if 'submit' in request.POST and form.validate():
76        # Grab some information from the tracking file
77        trackfile_ext = request.POST['tracking_file'].filename.split('.')[-1]
78        # Create a Workout instance based on the input from the form
79        workout = form.bind(Workout())
80        # Add the type of tracking file
81        workout.tracking_filetype = trackfile_ext
82        # Add basic info gathered from the file
83        workout.load_from_file()
84        # Add the workout
85        context.add_workout(workout)
86        return HTTPFound(location=request.resource_url(workout))
87
88    return {
89        'form': FormRenderer(form)
90    }
91
92
93@view_config(
94    context=Workout,
95    name='edit',
96    renderer='ow:templates/edit_manual_workout.pt')
97def edit_workout(context, request):
98    """
99    Edit manually an existing workout. This won't let users attach/update
100    tracking files, just manually edit of the values.
101    """
102    form = Form(request, schema=ManualWorkoutSchema(), obj=context)
103    if 'submit' in request.POST and form.validate():
104        # exclude the three duration_* and start_* fields, so they won't be
105        # "bind" to the object, we do calculate both the full duration in
106        # seconds and the full datetime "start" and we save that
107        excluded = ['duration_hours', 'duration_minutes', 'duration_seconds',
108                    'start_date', 'start_time']
109
110        form.bind(context, exclude=excluded)
111
112        duration = timedelta(hours=form.data['duration_hours'],
113                             minutes=form.data['duration_minutes'],
114                             seconds=form.data['duration_seconds'])
115        context.duration = duration
116
117        # create a time object first using the given hours and minutes
118        start_time = time(form.data['start_time'][0],
119                          form.data['start_time'][1])
120        # combine the given start date with the built time object
121        start = datetime.combine(form.data['start_date'], start_time,
122                                 tzinfo=timezone.utc)
123        context.start = start
124        # ensure distance is a decimal
125        context.distance = Decimal(context.distance)
126        catalog = get_catalog(context)
127        reindex_object(catalog, context)
128        return HTTPFound(location=request.resource_url(context))
129
130    # round some values before rendering
131    if form.data['distance']:
132        form.data['distance'] = round(form.data['distance'], 2)
133
134    return {
135        'form': FormRenderer(form)
136    }
137
138
139@view_config(
140    context=Workout,
141    name='update-from-file',
142    renderer='ow:templates/update_workout_from_file.pt')
143def update_workout_from_file(context, request):
144    # if not given a file there is an empty byte in POST, which breaks
145    # our blob storage validator.
146    # dirty fix until formencode fixes its api.is_empty method
147    if isinstance(request.POST.get('tracking_file', None), bytes):
148        request.POST['tracking_file'] = ''
149
150    form = Form(request, schema=UpdateWorkoutSchema())
151    if 'submit' in request.POST and form.validate():
152        # Grab some information from the tracking file
153        trackfile_ext = request.POST['tracking_file'].filename.split('.')[-1]
154        # Update the type of tracking file
155        context.tracking_filetype = trackfile_ext
156        form.bind(context)
157        # Override basic info gathered from the file
158        context.load_from_file()
159        catalog = get_catalog(context)
160        reindex_object(catalog, context)
161        return HTTPFound(location=request.resource_url(context))
162    return {
163        'form': FormRenderer(form)
164    }
165
166
167@view_config(
168    context=Workout,
169    name='delete',
170    renderer='ow:templates/delete_workout.pt')
171def delete_workout(context, request):
172    """
173    Delete a workout
174    """
175    if 'submit' in request.POST:
176        if request.POST.get('delete', None) == 'yes':
177            catalog = get_catalog(context)
178            remove_from_catalog(catalog, context)
179            del request.root[request.authenticated_userid][context.workout_id]
180            return HTTPFound(location=request.resource_url(request.root))
181    return {}
182
183
184@view_config(
185    context=Workout,
186    renderer='ow:templates/workout.pt')
187def workout(context, request):
188    """
189    Details page for a workout
190    """
191    start_point = {}
192    if context.has_gpx:
193        with context.tracking_file.open() as gpx_file:
194            gpx_contents = gpx_file.read()
195            gpx_contents = gpx_contents.decode('utf-8')
196            gpx = gpxpy.parse(gpx_contents)
197            if gpx.tracks:
198                track = gpx.tracks[0]
199                center_point = track.get_center()
200                start_point = {'latitude': center_point.latitude,
201                               'longitude': center_point.longitude,
202                               'elevation': center_point.elevation}
203    return {'start_point': start_point}
204
205
206@view_config(
207    context=Workout,
208    name='gpx')
209def workout_gpx(context, request):
210    """
211    Return a gpx file with the workout tracking information, if any.
212    For now, simply return the gpx file if it has been attached to the
213    workout.
214    """
215    if not context.has_gpx:
216        return HTTPNotFound()
217    # Generate a proper file name to suggest on the download
218    gpx_slug = slugify(context.title) + '.gpx'
219    return Response(
220        content_type='application/xml',
221        content_disposition='attachment; filename="%s"' % gpx_slug,
222        body_file=context.tracking_file.open())
223
224
225@view_config(
226    context=Workout,
227    name='map',
228    renderer='ow:templates/workout-map.pt')
229def workout_map(context, request):
230    """
231    Render a page that has only a map with tracking info
232    """
233    start_point = {}
234    if context.has_gpx:
235        with context.tracking_file.open() as gpx_file:
236            gpx_contents = gpx_file.read()
237            gpx_contents = gpx_contents.decode('utf-8')
238            gpx = gpxpy.parse(gpx_contents)
239            if gpx.tracks:
240                track = gpx.tracks[0]
241                center_point = track.get_center()
242                start_point = {'latitude': center_point.latitude,
243                               'longitude': center_point.longitude,
244                               'elevation': center_point.elevation}
245    return {'start_point': start_point}
Note: See TracBrowser for help on using the repository browser.