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

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

(#58) Set a title automatically when adding manually a workout without
providing one.

The title is generated based on the only required data we have (starting
date and time) + sport (if provided).

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