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
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')
[5ec3a0b]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,
[78af3d1]72    permission='edit',
[5ec3a0b]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    """
[74b9c4d]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
[5ec3a0b]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,
[78af3d1]107    permission='edit',
[5ec3a0b]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
[5bdfbfb]137        # ensure distance is a decimal
138        context.distance = Decimal(context.distance)
[5ec3a0b]139        catalog = get_catalog(context)
140        reindex_object(catalog, context)
141        return HTTPFound(location=request.resource_url(context))
142
[fb327e1]143    # round some values before rendering
144    if form.data['distance']:
145        form.data['distance'] = round(form.data['distance'], 2)
146
[5ec3a0b]147    return {
148        'form': FormRenderer(form)
149    }
150
151
152@view_config(
153    context=Workout,
[78af3d1]154    permission='edit',
[5ec3a0b]155    name='update-from-file',
156    renderer='ow:templates/update_workout_from_file.pt')
157def update_workout_from_file(context, request):
[74b9c4d]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
[5ec3a0b]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,
[78af3d1]183    permission='delete',
[5ec3a0b]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,
[78af3d1]201    permission='view',
[5ec3a0b]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.
[78af3d1]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.
[5ec3a0b]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())
[d1c4782]242
243
244@view_config(
245    context=Workout,
246    name='map',
247    renderer='ow:templates/workout-map.pt')
248def workout_map(context, request):
249    """
[78af3d1]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.
[d1c4782]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}
[b3374f6]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.