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
RevLine 
[5bdfbfb]1from decimal import Decimal
[5ec3a0b]2from datetime import datetime, timedelta, time, timezone
[b3374f6]3import json
[5ec3a0b]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
[d517001]12from pyramid.i18n import TranslationStringFactory
[5ec3a0b]13
14from ..schemas.workout import (
15    UploadedWorkoutSchema,
16    ManualWorkoutSchema,
17    UpdateWorkoutSchema
18)
19from ..models.workout import Workout
20from ..models.user import User
[d517001]21from ..utilities import slugify, save_map_screenshot, part_of_day
[5ec3a0b]22from ..catalog import get_catalog, reindex_object, remove_from_catalog
23
24
[d517001]25_ = TranslationStringFactory('OpenWorkouts')
26
27
[5ec3a0b]28@view_config(
29    context=User,
[78af3d1]30    permission='edit',
[5ec3a0b]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
[e52a502]43        #
44        # exclude also the distance field, we have to convert to decimal
45        # before adding it
[5ec3a0b]46        excluded = ['duration_hours', 'duration_minutes', 'duration_seconds',
[e52a502]47                    'start_date', 'start_time', 'distance']
[5ec3a0b]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
[d517001]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')
[e52a502]65        workout.distance = Decimal(form.data['distance'])
[5ec3a0b]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,
[78af3d1]76    permission='edit',
[5ec3a0b]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    """
[74b9c4d]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
[5ec3a0b]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,
[78af3d1]111    permission='edit',
[5ec3a0b]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
[5bdfbfb]141        # ensure distance is a decimal
142        context.distance = Decimal(context.distance)
[5ec3a0b]143        catalog = get_catalog(context)
144        reindex_object(catalog, context)
145        return HTTPFound(location=request.resource_url(context))
146
[fb327e1]147    # round some values before rendering
148    if form.data['distance']:
149        form.data['distance'] = round(form.data['distance'], 2)
150
[5ec3a0b]151    return {
152        'form': FormRenderer(form)
153    }
154
155
156@view_config(
157    context=Workout,
[78af3d1]158    permission='edit',
[5ec3a0b]159    name='update-from-file',
160    renderer='ow:templates/update_workout_from_file.pt')
161def update_workout_from_file(context, request):
[74b9c4d]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
[5ec3a0b]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,
[78af3d1]187    permission='delete',
[5ec3a0b]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,
[78af3d1]205    permission='view',
[5ec3a0b]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.
[78af3d1]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.
[5ec3a0b]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())
[d1c4782]246
247
248@view_config(
249    context=Workout,
250    name='map',
251    renderer='ow:templates/workout-map.pt')
252def workout_map(context, request):
253    """
[78af3d1]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.
[d1c4782]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}
[b3374f6]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.