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

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

(#51) Fixed profile images were too big:

  • Added new dependency (Pillow)
  • Modified the user profile picture view. Now it accepts a GET parameter (size) telling the size of the image we want. Only one value is needed, the code will scale the image appropiately.
  • Modified the dashboard, profile and edit_profile templates to ask for a smaller version (200px) of the user profile picture.

IMPORTANT: Ensure you install Pillow in any existing environments after pulling:

pip install Pillow

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