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

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

(#39) Do not allow duplicated workouts by default when uploading track files.
We still allow users to add duplicates if they want, by checking a checkbox
we show in the upload workout form when we find a possible duplicate.

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