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

currentfeature/docs
Last change on this file since 5bdfbfb was 5bdfbfb, checked in by borja <borja@…>, 5 years ago

(#7) Show year/month/weekly stats in the dashboard for the user,

including a bar chart for activity during the current week

  • Property mode set to 100644
File size: 8.7 KB
Line 
1from decimal import Decimal
2from datetime import datetime, timedelta, time, timezone
3
4import gpxpy
5
6from pyramid.httpexceptions import HTTPFound, HTTPNotFound
7from pyramid.view import view_config
8from pyramid.response import Response
9from pyramid_simpleform import Form
10from pyramid_simpleform.renderers import FormRenderer
11
12from ..schemas.workout import (
13    UploadedWorkoutSchema,
14    ManualWorkoutSchema,
15    UpdateWorkoutSchema
16)
17from ..models.workout import Workout
18from ..models.user import User
19from ..utilities import slugify
20from ..catalog import get_catalog, reindex_object, remove_from_catalog
21
22
23@view_config(
24    context=User,
25    name='add-workout-manually',
26    renderer='ow:templates/add_manual_workout.pt')
27def add_workout_manually(context, request):
28    form = Form(request, schema=ManualWorkoutSchema(),
29                defaults={'duration_hours': '0',
30                          'duration_minutes': '0',
31                          'duration_seconds': '0'})
32
33    if 'submit' in request.POST and form.validate():
34        # exclude the three duration_* and start_* fields, so they won't be
35        # "bind" to the object, we do calculate both the full duration in
36        # seconds and the full datetime "start" and we save that
37        excluded = ['duration_hours', 'duration_minutes', 'duration_seconds',
38                    'start_date', 'start_time']
39        workout = form.bind(Workout(), exclude=excluded)
40        duration = timedelta(hours=form.data['duration_hours'],
41                             minutes=form.data['duration_minutes'],
42                             seconds=form.data['duration_seconds'])
43        workout.duration = duration
44        # create a time object first using the given hours and minutes
45        start_time = time(form.data['start_time'][0],
46                          form.data['start_time'][1])
47        # combine the given start date with the built time object
48        start = datetime.combine(form.data['start_date'], start_time,
49                                 tzinfo=timezone.utc)
50        workout.start = start
51        context.add_workout(workout)
52        return HTTPFound(location=request.resource_url(workout))
53
54    return {
55        'form': FormRenderer(form)
56    }
57
58
59@view_config(
60    context=User,
61    name='add-workout',
62    renderer='ow:templates/add_workout.pt')
63def add_workout(context, request):
64    """
65    Add a workout uploading a tracking file
66    """
67    # if not given a file there is an empty byte in POST, which breaks
68    # our blob storage validator.
69    # dirty fix until formencode fixes its api.is_empty method
70    if isinstance(request.POST.get('tracking_file', None), bytes):
71        request.POST['tracking_file'] = ''
72
73    form = Form(request, schema=UploadedWorkoutSchema())
74
75    if 'submit' in request.POST and form.validate():
76        # Grab some information from the tracking file
77        trackfile_ext = request.POST['tracking_file'].filename.split('.')[-1]
78        # Create a Workout instance based on the input from the form
79        workout = form.bind(Workout())
80        # Add the type of tracking file
81        workout.tracking_filetype = trackfile_ext
82        # Add basic info gathered from the file
83        workout.load_from_file()
84        # Add the workout
85        context.add_workout(workout)
86        return HTTPFound(location=request.resource_url(workout))
87
88    return {
89        'form': FormRenderer(form)
90    }
91
92
93@view_config(
94    context=Workout,
95    name='edit',
96    renderer='ow:templates/edit_manual_workout.pt')
97def edit_workout(context, request):
98    """
99    Edit manually an existing workout. This won't let users attach/update
100    tracking files, just manually edit of the values.
101    """
102    form = Form(request, schema=ManualWorkoutSchema(), obj=context)
103    if 'submit' in request.POST and form.validate():
104        # exclude the three duration_* and start_* fields, so they won't be
105        # "bind" to the object, we do calculate both the full duration in
106        # seconds and the full datetime "start" and we save that
107        excluded = ['duration_hours', 'duration_minutes', 'duration_seconds',
108                    'start_date', 'start_time']
109
110        form.bind(context, exclude=excluded)
111
112        duration = timedelta(hours=form.data['duration_hours'],
113                             minutes=form.data['duration_minutes'],
114                             seconds=form.data['duration_seconds'])
115        context.duration = duration
116
117        # create a time object first using the given hours and minutes
118        start_time = time(form.data['start_time'][0],
119                          form.data['start_time'][1])
120        # combine the given start date with the built time object
121        start = datetime.combine(form.data['start_date'], start_time,
122                                 tzinfo=timezone.utc)
123        context.start = start
124        # ensure distance is a decimal
125        context.distance = Decimal(context.distance)
126        catalog = get_catalog(context)
127        reindex_object(catalog, context)
128        return HTTPFound(location=request.resource_url(context))
129
130    return {
131        'form': FormRenderer(form)
132    }
133
134
135@view_config(
136    context=Workout,
137    name='update-from-file',
138    renderer='ow:templates/update_workout_from_file.pt')
139def update_workout_from_file(context, request):
140    # if not given a file there is an empty byte in POST, which breaks
141    # our blob storage validator.
142    # dirty fix until formencode fixes its api.is_empty method
143    if isinstance(request.POST.get('tracking_file', None), bytes):
144        request.POST['tracking_file'] = ''
145
146    form = Form(request, schema=UpdateWorkoutSchema())
147    if 'submit' in request.POST and form.validate():
148        # Grab some information from the tracking file
149        trackfile_ext = request.POST['tracking_file'].filename.split('.')[-1]
150        # Update the type of tracking file
151        context.tracking_filetype = trackfile_ext
152        form.bind(context)
153        # Override basic info gathered from the file
154        context.load_from_file()
155        catalog = get_catalog(context)
156        reindex_object(catalog, context)
157        return HTTPFound(location=request.resource_url(context))
158    return {
159        'form': FormRenderer(form)
160    }
161
162
163@view_config(
164    context=Workout,
165    name='delete',
166    renderer='ow:templates/delete_workout.pt')
167def delete_workout(context, request):
168    """
169    Delete a workout
170    """
171    if 'submit' in request.POST:
172        if request.POST.get('delete', None) == 'yes':
173            catalog = get_catalog(context)
174            remove_from_catalog(catalog, context)
175            del request.root[request.authenticated_userid][context.workout_id]
176            return HTTPFound(location=request.resource_url(request.root))
177    return {}
178
179
180@view_config(
181    context=Workout,
182    renderer='ow:templates/workout.pt')
183def workout(context, request):
184    """
185    Details page for a workout
186    """
187    start_point = {}
188    if context.has_gpx:
189        with context.tracking_file.open() as gpx_file:
190            gpx_contents = gpx_file.read()
191            gpx_contents = gpx_contents.decode('utf-8')
192            gpx = gpxpy.parse(gpx_contents)
193            if gpx.tracks:
194                track = gpx.tracks[0]
195                center_point = track.get_center()
196                start_point = {'latitude': center_point.latitude,
197                               'longitude': center_point.longitude,
198                               'elevation': center_point.elevation}
199    return {'start_point': start_point}
200
201
202@view_config(
203    context=Workout,
204    name='gpx')
205def workout_gpx(context, request):
206    """
207    Return a gpx file with the workout tracking information, if any.
208    For now, simply return the gpx file if it has been attached to the
209    workout.
210    """
211    if not context.has_gpx:
212        return HTTPNotFound()
213    # Generate a proper file name to suggest on the download
214    gpx_slug = slugify(context.title) + '.gpx'
215    return Response(
216        content_type='application/xml',
217        content_disposition='attachment; filename="%s"' % gpx_slug,
218        body_file=context.tracking_file.open())
219
220
221@view_config(
222    context=Workout,
223    name='map',
224    renderer='ow:templates/workout-map.pt')
225def workout_map(context, request):
226    """
227    Render a page that has only a map with tracking info
228    """
229    start_point = {}
230    if context.has_gpx:
231        with context.tracking_file.open() as gpx_file:
232            gpx_contents = gpx_file.read()
233            gpx_contents = gpx_contents.decode('utf-8')
234            gpx = gpxpy.parse(gpx_contents)
235            if gpx.tracks:
236                track = gpx.tracks[0]
237                center_point = track.get_center()
238                start_point = {'latitude': center_point.latitude,
239                               'longitude': center_point.longitude,
240                               'elevation': center_point.elevation}
241    return {'start_point': start_point}
Note: See TracBrowser for help on using the repository browser.