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

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

Fixed some cosmetic details (empty lines, whitespaces, indentation).
Fixed a test that was breaking depending on which days you ran it.

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