Changeset fd6da93 in OpenWorkouts-current


Ignore:
Timestamp:
Feb 26, 2019, 11:11:43 AM (5 years ago)
Author:
Borja Lopez <borja@…>
Branches:
current, feature/docs, master
Children:
a4e4799
Parents:
d411dae
Message:

(#56) Add support for different locale/language:

  • Let users choose their lang/locale in the edit profile page
  • Set the currently selected locale as a cookie (following pyramid docs on how to set the locale using the default locale negotiator)
  • Save the locale setting for each user as an attribute on the User model
  • Set the proper locale as a cookie on login
  • Unset the locale cookie on logout

Default available locales for now are en (english) and es (spanish)

Location:
ow
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • ow/models/user.py

    rd411dae rfd6da93  
    3737        self.picture = kw.get('picture', None)  # blob
    3838        self.timezone = kw.get('timezone', 'UTC')
     39        self.locale = kw.get('locale', 'en')
    3940        self.__password = None
    4041        self.last_workout_id = 0
  • ow/schemas/user.py

    rd411dae rfd6da93  
    44
    55from ow.schemas.blob import FieldStorageBlob
     6from ow.utilities import get_available_locale_names
    67
    78_ = TranslationStringFactory('OpenWorkouts')
     
    7677                               whitelist=['jpg', 'jpeg', 'png', 'gif'])
    7778    timezone = validators.OneOf(common_timezones, if_missing='UTC')
     79    locale = validators.OneOf(
     80        [locale[0] for locale in get_available_locale_names()],
     81        if_missing='en'
     82    )
    7883
    7984
  • ow/static/css/main.css

    rd411dae rfd6da93  
    605605  border-radius: 4px;
    606606  border: 1px solid #e1e1e1;
     607}
     608.ow-forms select {
     609  width: 100%;
    607610}
    608611/* Generic button */
  • ow/static/less/ui/form.less

    rd411dae rfd6da93  
    4747        border: 1px solid @color-main-light;
    4848    }
     49    select {
     50        width: 100%;
     51    }
    4952}
  • ow/templates/edit_profile.pt

    rd411dae rfd6da93  
    4646                <div>
    4747                  <tal:c tal:condition="getattr(context, 'picture', None)">
    48 
    4948                      <label for="current_picture" i18n:translate="">
    5049                          Current picture:</label>
     
    101100                ${form.textarea('bio', rows=10, cols=50)}
    102101            </p>
    103             <div>
    104               <label for="timezone" i18n:translate="">Timezone:</label>
    105                 <small i18n:translate="">
     102            <div class="input-container ly-flex ly-2 has-gap">
     103              <div>
     104                <label for="timezone" i18n:translate="">Timezone:</label>
     105                <p>
     106                  <small i18n:translate="">
    106107                    All dates and times will be formatted for this timezone
    107                 </small>
     108                  </small>
     109                </p>
     110                ${form.errorlist('timezone')}
     111                ${form.select('timezone', timezones)}
     112              </div>
     113              <div>
     114                <label for="locale" i18n:translate="">Language:</label>
     115                <p>
     116                  <small i18n:translate="">
     117                    All texts in the user interface will appear in this language
     118                  </small>
     119                </p>
     120                ${form.errorlist('locale')}
     121                ${form.select('locale', available_locale_names, selected_value=current_locale)}
     122              </div>
    108123            </div>
    109             ${form.errorlist('timezone')}
    110             ${form.select('timezone', timezones)}
    111124        </fieldset>
    112125
  • ow/tests/views/test_user.py

    rd411dae rfd6da93  
    2020from PIL import Image
    2121
     22from pytz import common_timezones
     23
    2224from ow.models.root import OpenWorkouts
    2325from ow.models.user import User
    2426from ow.models.workout import Workout
    2527from ow.views.renderers import OWFormRenderer
     28from ow.utilities import get_available_locale_names
    2629import ow.views.user as user_views
    2730
     
    5154    def dummy_request(self, root):
    5255        request = DummyRequest()
     56        request.registry.settings = {
     57            'pyramid.default_locale_name': 'en'
     58        }
    5359        request.root = root
    5460        return request
     
    561567        assert rem.called
    562568        assert response.location == request.resource_url(john)
     569        # the response headers contain the proper set_cookie for the default
     570        # locale
     571        default_locale_name = request.registry.settings[
     572            'pyramid.default_locale_name']
     573        expected_locale_header = '_LOCALE_=' + default_locale_name + '; Path=/'
     574        assert response.headers['Set-Cookie'] == expected_locale_header
     575
     576    @patch('ow.views.user.remember')
     577    def test_login_post_ok_set_locale(self, rem, dummy_request, john):
     578        # same as the previous test, but this time the user has set a
     579        # locale different than the default one
     580        request = dummy_request
     581        request.method = 'POST'
     582        request.POST['submit'] = True
     583        request.POST['email'] = 'john.doe@example.net'
     584        request.POST['password'] = 's3cr3t'
     585        # verify the user first
     586        john.verified = True
     587        # set the locale
     588        john.locale = 'es'
     589        response = user_views.login(request.root, request)
     590        assert isinstance(response, HTTPFound)
     591        assert rem.called
     592        assert response.location == request.resource_url(john)
     593        # the response headers contain the proper set_cookie for the user
     594        # locale setting
     595        expected_locale_header = '_LOCALE_=es; Path=/'
     596        assert response.headers['Set-Cookie'] == expected_locale_header
    563597
    564598    @patch('ow.views.user.forget')
     
    569603        assert forg.called
    570604        assert response.location == request.resource_url(request.root)
     605        # the response headers contain the needed Set-Cookie header that
     606        # invalidates the _LOCALE_ cookie, preventing problems with users
     607        # sharing the same web browser (one locale setting being set for
     608        # another user)
     609        expected_locale_header = '_LOCALE_=; Max-Age=0; Path=/; expires='
     610        assert expected_locale_header in response.headers['Set-Cookie']
    571611
    572612    extensions = ('png', 'jpg', 'jpeg', 'gif')
     
    639679        # loaded user profile
    640680        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
    641                 'birth_date', 'height', 'weight', 'gender', 'timezone']
     681                'birth_date', 'height', 'weight', 'gender', 'timezone',
     682                'locale']
    642683        assert list(response['form'].data.keys()) == data
    643684        # and check the email to see data is properly loaded
    644685        assert response['form'].data['email'] == 'john.doe@example.net'
     686        assert response['timezones'] == common_timezones
     687        assert response[
     688            'available_locale_names'] == get_available_locale_names()
     689        assert response['current_locale'] == request.registry.settings[
     690            'pyramid.default_locale_name']
    645691
    646692    def test_edit_profile_post_ok(self, profile_post_request, john):
     
    654700        assert response.location == request.resource_url(user, 'profile')
    655701        assert user.bio == bio
     702
     703    def test_edit_profile_post_ok_change_locale(
     704            self, profile_post_request, john):
     705        request = profile_post_request
     706        user = john
     707        # Update the locale
     708        request.POST['locale'] = 'es'
     709        response = user_views.edit_profile(user, request)
     710        assert isinstance(response, HTTPFound)
     711        assert response.location == request.resource_url(user, 'profile')
     712        assert user.locale == 'es'
     713
     714    def test_edit_profile_post_ok_invalid_locale(
     715            self, profile_post_request, john):
     716        request = profile_post_request
     717        user = john
     718        # Update the locale with an invalid option
     719        request.POST['locale'] = 'XX'
     720        response = user_views.edit_profile(user, request)
     721        assert isinstance(response['form'], OWFormRenderer)
     722        # as an error happened, the current_locale had not changed
     723        assert response['form'].errors == {
     724            'locale': "Value must be one of: en; es (not 'XX')"}
     725        assert user.locale == 'en'
    656726
    657727    def test_edit_profile_post_missing_required(
  • ow/utilities.py

    rd411dae rfd6da93  
    296296        if value[0] <= dt.hour <= value[1]:
    297297            return key
     298
     299
     300def get_available_locale_names():
     301    """
     302    Return a list of tuples with info about available locale/language
     303    names.
     304
     305    The locale codes and names in this list match the available translations
     306    under ow/locale for the UI elements
     307    """
     308    return [
     309        ('en', _('English')),
     310        ('es', _('Spanish'))
     311    ]
  • ow/views/user.py

    rd411dae rfd6da93  
    2222from ..models.root import OpenWorkouts
    2323from ..views.renderers import OWFormRenderer
    24 from ..utilities import timedelta_to_hms, get_verification_token
     24from ..utilities import (
     25    timedelta_to_hms,
     26    get_verification_token,
     27    get_available_locale_names
     28)
    2529from ..mail import send_verification_email
    2630
     
    7680                password = request.POST.get('password', None)
    7781                if password is not None and user.check_password(password):
     82                    # look for the value of locale for this user, to set the
     83                    # LOCALE cookie, so the UI appears on the pre-selected lang
     84                    default_locale = request.registry.settings.get(
     85                        'pyramid.default_locale_name')
     86                    locale = getattr(user, 'locale', default_locale)
     87                    request.response.set_cookie('_LOCALE_', locale)
     88                    # log in the user and send back to the place he wanted to
     89                    # visit
    7890                    headers = remember(request, str(user.uid))
     91                    request.response.headers.extend(headers)
    7992                    redirect_url = return_to or request.resource_url(user)
    80                     return HTTPFound(location=redirect_url, headers=headers)
     93                    return HTTPFound(location=redirect_url,
     94                                     headers=request.response.headers)
    8195                else:
    8296                    message = _('Wrong password')
     
    100114@view_config(context=OpenWorkouts, name='logout')
    101115def logout(context, request):
     116    request.response.delete_cookie('_LOCALE_')
    102117    headers = forget(request)
    103     return HTTPFound(location=request.resource_url(context), headers=headers)
     118    request.response.headers.extend(headers)
     119    return HTTPFound(location=request.resource_url(context),
     120                     headers=request.response.headers)
    104121
    105122
     
    319336    renderer='ow:templates/edit_profile.pt')
    320337def edit_profile(context, request):
     338    default_locale = request.registry.settings.get(
     339        'pyramid.default_locale_name')
     340    available_locale_names = get_available_locale_names()
     341    current_locale = request.cookies.get('_LOCALE_', default_locale)
    321342    # if not given a file there is an empty byte in POST, which breaks
    322343    # our blob storage validator.
     
    340361        # reindex
    341362        request.root.reindex(context)
     363        # set the cookie for the locale/lang
     364        request.response.set_cookie('_LOCALE_', form.data['locale'])
     365        current_locale = form.data['locale']
    342366        # Saved, send the user to the public view of her profile
    343         return HTTPFound(location=request.resource_url(context, 'profile'))
     367        return HTTPFound(location=request.resource_url(context, 'profile'),
     368                         headers=request.response.headers)
    344369
    345370    # prevent crashes on the form
     
    348373
    349374    return {'form': OWFormRenderer(form),
    350             'timezones': common_timezones}
     375            'timezones': common_timezones,
     376            'available_locale_names': available_locale_names,
     377            'current_locale': current_locale}
    351378
    352379
Note: See TracChangeset for help on using the changeset viewer.