source: OpenWorkouts-current/ow/views/user.py @ bddf042

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

(#7) Allow users profiles to be accessed using a more friendly url:

https://openworkouts.org/profile/NICKNAME

IMPORTANT: This change adds a new index to the catalog, so ensure you
update any existing databases after pulling.

Enter pshell and run this code:

root._update_indexes()
for user in root.users:

root.reindex(user)

  • Property mode set to 100644
File size: 12.0 KB
Line 
1import json
2from calendar import month_name
3from datetime import datetime, timezone, timedelta
4from decimal import Decimal
5
6from pyramid.httpexceptions import HTTPFound, HTTPNotFound
7from pyramid.view import view_config
8from pyramid.security import remember, forget
9from pyramid.response import Response
10from pyramid.i18n import TranslationStringFactory
11from pyramid_simpleform import Form, State
12from pytz import common_timezones
13
14from ..models.user import User
15from ..schemas.user import (
16    UserProfileSchema,
17    ChangePasswordSchema,
18    SignUpSchema,
19)
20from ..models.root import OpenWorkouts
21from ..views.renderers import OWFormRenderer
22from ..utilities import timedelta_to_hms
23
24_ = TranslationStringFactory('OpenWorkouts')
25
26
27@view_config(context=OpenWorkouts)
28def dashboard_redirect(context, request):
29    """
30    Send the user to his dashboard when accesing the root object,
31    send to the login page if the user is not logged in.
32    """
33    if request.authenticated_userid:
34        user = request.root.get_user_by_uid(request.authenticated_userid)
35        if user:
36            return HTTPFound(location=request.resource_url(user))
37        else:
38            # an authenticated user session, for an user that does not exist
39            # anymore, logout!
40            return HTTPFound(location=request.resource_url(context, 'logout'))
41    return HTTPFound(location=request.resource_url(context, 'login'))
42
43
44@view_config(
45    context=OpenWorkouts,
46    name='login',
47    renderer='ow:templates/login.pt')
48def login(context, request):
49    message = ''
50    email = ''
51    password = ''
52    return_to = request.params.get('return_to')
53    redirect_url = return_to or request.resource_url(request.root)
54
55    if 'submit' in request.POST:
56        email = request.POST.get('email', None)
57        user = context.get_user_by_email(email)
58        if user:
59            password = request.POST.get('password', None)
60            if password is not None and user.check_password(password):
61                headers = remember(request, str(user.uid))
62                redirect_url = return_to or request.resource_url(user)
63                return HTTPFound(location=redirect_url, headers=headers)
64            else:
65                message = _('Wrong password')
66        else:
67            message = _('Wrong email address')
68
69    return {
70        'message': message,
71        'email': email,
72        'password': password,
73        'redirect_url': redirect_url
74    }
75
76
77@view_config(context=OpenWorkouts, name='logout')
78def logout(context, request):
79    headers = forget(request)
80    return HTTPFound(location=request.resource_url(context), headers=headers)
81
82
83@view_config(
84    context=OpenWorkouts,
85    name='signup',
86    renderer='ow:templates/signup.pt')
87def signup(context, request):
88    state = State(emails=context.lowercase_emails,
89                  names=context.lowercase_nicknames)
90    form = Form(request, schema=SignUpSchema(), state=state)
91
92    if 'submit' in request.POST and form.validate():
93        user = form.bind(User(), exclude=['password_confirm'])
94        context.add_user(user)
95        # Send to login
96        return HTTPFound(location=request.resource_url(context))
97
98    return {
99        'form': OWFormRenderer(form)
100    }
101
102
103@view_config(
104    context=OpenWorkouts,
105    name='forgot-password',
106    renderer='ow:templates/forgot_password.pt')
107def recover_password(context, request):  # pragma: no cover
108    # WIP
109    Form(request)
110
111
112@view_config(
113    context=User,
114    permission='view',
115    renderer='ow:templates/dashboard.pt')
116def dashboard(context, request):
117    """
118    Render a dashboard for the current user
119    """
120    # Look at the year we are viewing, if none is passed in the request,
121    # pick up the latest/newer available with activity
122    viewing_year = request.GET.get('year', None)
123    if viewing_year is None:
124        available_years = context.activity_years
125        if available_years:
126            viewing_year = available_years[0]
127    else:
128        # ensure this is an integer
129        viewing_year = int(viewing_year)
130
131    # Same for the month, if there is a year set
132    viewing_month = None
133    if viewing_year:
134        viewing_month = request.GET.get('month', None)
135        if viewing_month is None:
136            available_months = context.activity_months(viewing_year)
137            if available_months:
138                # we pick up the latest month available for the year,
139                # which means the current month in the current year
140                viewing_month = available_months[-1]
141        else:
142            # ensure this is an integer
143            viewing_month = int(viewing_month)
144
145    # pick up the workouts to be shown in the dashboard
146    workouts = context.workouts(viewing_year, viewing_month)
147
148    return {
149        'current_year': datetime.now(timezone.utc).year,
150        'current_day_name': datetime.now(timezone.utc).strftime('%a'),
151        'month_name': month_name,
152        'viewing_year': viewing_year,
153        'viewing_month': viewing_month,
154        'workouts': workouts
155    }
156
157
158@view_config(
159    context=OpenWorkouts,
160    name='profile',
161    permission='view',
162    renderer='ow:templates/profile.pt')
163@view_config(
164    context=User,
165    permission='view',
166    name='profile',
167    renderer='ow:templates/profile.pt')
168def profile(context, request):
169    """
170    "public" profile view, showing some workouts from this user, her
171    basic info, stats, etc
172    """
173    if isinstance(context, OpenWorkouts):
174        nickname = request.subpath[0]
175        user = request.root.get_user_by_nickname(nickname)
176        if user is None:
177            return HTTPNotFound()
178    else:
179        user = context
180    now = datetime.now(timezone.utc)
181    year = int(request.GET.get('year', now.year))
182    month = int(request.GET.get('month', now.month))
183    week = request.GET.get('week', None)
184    workouts = user.workouts(year, month, week)
185    totals = {
186        'distance': Decimal(0),
187        'time': timedelta(0),
188        'elevation': Decimal(0)
189    }
190
191    for workout in workouts:
192        totals['distance'] += (
193            getattr(workout, 'distance', Decimal(0)) or Decimal(0))
194        totals['time'] += (
195            getattr(workout, 'duration', timedelta(0)) or timedelta(0))
196        totals['elevation'] += (
197            getattr(workout, 'uphill', Decimal(0)) or Decimal(0))
198
199    return {
200        'user': user,
201        'workouts': workouts,
202        'current_month': '{year}-{month}'.format(
203            year=str(year), month=str(month).zfill(2)),
204        'current_week': week,
205        'totals': totals
206    }
207
208
209@view_config(
210    context=User,
211    name='picture',
212    permission='view')
213def profile_picture(context, request):
214    return Response(
215        content_type='image',
216        body_file=context.picture.open())
217
218
219@view_config(
220    context=User,
221    permission='edit',
222    name='edit',
223    renderer='ow:templates/edit_profile.pt')
224def edit_profile(context, request):
225    # if not given a file there is an empty byte in POST, which breaks
226    # our blob storage validator.
227    # dirty fix until formencode fixes its api.is_empty method
228    if isinstance(request.POST.get('picture', None), bytes):
229        request.POST['picture'] = ''
230
231    nicknames = request.root.lowercase_nicknames
232    if context.nickname:
233        # remove the current user nickname from the list, preventing form
234        # validation error
235        nicknames.remove(context.nickname.lower())
236    state = State(emails=request.root.lowercase_emails, names=nicknames)
237    form = Form(request, schema=UserProfileSchema(), state=state, obj=context)
238
239    if 'submit' in request.POST and form.validate():
240        # No picture? do not override it
241        if not form.data['picture']:
242            del form.data['picture']
243        form.bind(context)
244        # reindex
245        request.root.reindex(context)
246        # Saved, send the user to the public view of her profile
247        return HTTPFound(location=request.resource_url(context, 'profile'))
248
249    # prevent crashes on the form
250    if 'picture' in form.data:
251        del form.data['picture']
252
253    return {'form': OWFormRenderer(form),
254            'timezones': common_timezones}
255
256
257@view_config(
258    context=User,
259    permission='edit',
260    name='passwd',
261    renderer='ow:templates/change_password.pt')
262def change_password(context, request):
263    form = Form(request, schema=ChangePasswordSchema(),
264                state=State(user=context))
265    if 'submit' in request.POST and form.validate():
266        context.password = form.data['password']
267        return HTTPFound(location=request.resource_url(context, 'profile'))
268    return {'form': OWFormRenderer(form)}
269
270
271@view_config(
272    context=User,
273    permission='view',
274    name='week')
275def week_stats(context, request):
276    stats = context.week_stats
277    json_stats = []
278    for day in stats:
279        hms = timedelta_to_hms(stats[day]['time'])
280        day_stats = {
281            'name': day.strftime('%a'),
282            'time': str(hms[0]).zfill(2),
283            'distance': int(round(stats[day]['distance'])),
284            'elevation': int(stats[day]['elevation']),
285            'workouts': stats[day]['workouts']
286        }
287        json_stats.append(day_stats)
288    return Response(content_type='application/json',
289                    charset='utf-8',
290                    body=json.dumps(json_stats))
291
292
293@view_config(
294    context=User,
295    permission='view',
296    name='monthly')
297def last_months_stats(context, request):
298    """
299    Return a json-encoded stream with statistics for the last 12 months
300    """
301    stats = context.yearly_stats
302    # this sets which month is 2 times in the stats, once this year, once
303    # the previous year. We will show it a bit different in the UI (showing
304    # the year too to prevent confusion)
305    repeated_month = datetime.now(timezone.utc).date().month
306    json_stats = []
307    for month in stats:
308        hms = timedelta_to_hms(stats[month]['time'])
309        name = month_name[month[1]][:3]
310        if month[1] == repeated_month:
311            name += ' ' + str(month[0])
312        month_stats = {
313            'id': str(month[0]) + '-' + str(month[1]).zfill(2),
314            'name': name,
315            'time': str(hms[0]).zfill(2),
316            'distance': int(round(stats[month]['distance'])),
317            'elevation': int(stats[month]['elevation']),
318            'workouts': stats[month]['workouts'],
319            'url': request.resource_url(
320                context, 'profile',
321                query={'year': str(month[0]), 'month': str(month[1])})
322        }
323        json_stats.append(month_stats)
324    return Response(content_type='application/json',
325                    charset='utf-8',
326                    body=json.dumps(json_stats))
327
328
329@view_config(
330    context=User,
331    permission='view',
332    name='weekly')
333def last_weeks_stats(context, request):
334    """
335    Return a json-encoded stream with statistics for the last 12-months, but
336    in a per-week basis
337    """
338    stats = context.weekly_year_stats
339    # this sets which month is 2 times in the stats, once this year, once
340    # the previous year. We will show it a bit different in the UI (showing
341    # the year too to prevent confusion)
342    repeated_month = datetime.now(timezone.utc).date().month
343    json_stats = []
344    for week in stats:
345        hms = timedelta_to_hms(stats[week]['time'])
346        name = month_name[week[1]][:3]
347        if week[1] == repeated_month:
348            name += ' ' + str(week[0])
349        week_stats = {
350            'id': '-'.join(
351                [str(week[0]), str(week[1]).zfill(2), str(week[2])]),
352            'week': str(week[3]),  # the number of week in the current month
353            'name': name,
354            'time': str(hms[0]).zfill(2),
355            'distance': int(round(stats[week]['distance'])),
356            'elevation': int(stats[week]['elevation']),
357            'workouts': stats[week]['workouts'],
358            'url': request.resource_url(
359                context, 'profile',
360                query={'year': str(week[0]),
361                       'month': str(week[1]),
362                       'week': str(week[2])})
363        }
364        json_stats.append(week_stats)
365    return Response(content_type='application/json',
366                    charset='utf-8',
367                    body=json.dumps(json_stats))
Note: See TracBrowser for help on using the repository browser.