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

current
Last change on this file since e52a502 was e52a502, checked in by Borja Lopez <borja@…>, 5 years ago

(#73) Fixed broken "add workout manually" when provided with a distance value
with decimals.

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