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

currentfeature/docs
Last change on this file since d517001 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
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        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
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
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,
73    permission='edit',
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    """
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
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,
108    permission='edit',
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
138        # ensure distance is a decimal
139        context.distance = Decimal(context.distance)
140        catalog = get_catalog(context)
141        reindex_object(catalog, context)
142        return HTTPFound(location=request.resource_url(context))
143
144    # round some values before rendering
145    if form.data['distance']:
146        form.data['distance'] = round(form.data['distance'], 2)
147
148    return {
149        'form': FormRenderer(form)
150    }
151
152
153@view_config(
154    context=Workout,
155    permission='edit',
156    name='update-from-file',
157    renderer='ow:templates/update_workout_from_file.pt')
158def update_workout_from_file(context, request):
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
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,
184    permission='delete',
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,
202    permission='view',
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.
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.
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())
243
244
245@view_config(
246    context=Workout,
247    name='map',
248    renderer='ow:templates/workout-map.pt')
249def workout_map(context, request):
250    """
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.
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}
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.