Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support language on user level #31

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,8 @@ Quick start
3. For more information on those libraries, check the following docs::
1. [django-axes](https://django-axes.readthedocs.io/en/latest/)
2. [django-recaptcha](https://github.com/praekelt/django-recaptcha)


8. If you have i18n enabled within your application, you can set a preferred language for the user
1. If you define a default language `LANGUAGE_CODE` it will be used as default or `en`
2. Languages supported are those languages you define in your application in `LANGUAGES` setting
2 changes: 1 addition & 1 deletion accountsplus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '1.4.2'
__version__ = '1.4.5'

default_app_config = 'accountsplus.apps.AccountsConfig'
15 changes: 15 additions & 0 deletions accountsplus/context_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import unicode_literals

from contextlib import contextmanager

from django.utils import translation


@contextmanager
def language(lang):
old_language = translation.get_language()
try:
translation.activate(lang)
yield
finally:
translation.activate(old_language)
12 changes: 11 additions & 1 deletion accountsplus/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import django.forms
from django.conf import settings
from django.apps import apps
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm
from django.contrib.admin.forms import AdminAuthenticationForm

from captcha.fields import ReCaptchaField

import context_managers

class CaptchaForm(django.forms.Form):
captcha = ReCaptchaField()
Expand All @@ -31,3 +32,12 @@ class EmailBasedAdminAuthenticationForm(AdminAuthenticationForm):

def clean_username(self):
return self.data['username'].lower()


class CustomPasswordResetForm(PasswordResetForm):

def send_mail(self, subject_template_name, email_template_name,
context, from_email, to_email, html_email_template_name=None):
with context_managers.language(context['email_lang']):
super(CustomPasswordResetForm, self).send_mail(subject_template_name, email_template_name, context,
from_email, to_email, html_email_template_name)
24 changes: 24 additions & 0 deletions accountsplus/middleware.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
from __future__ import unicode_literals
import django.utils.timezone
from django.utils.deprecation import MiddlewareMixin
from django.utils import translation


class TimezoneMiddleware(MiddlewareMixin):

def process_request(self, request):
if request.user.is_authenticated() and request.user.timezone:
django.utils.timezone.activate(request.user.timezone)
else:
django.utils.timezone.deactivate()


class UserLanguageMiddleware(MiddlewareMixin):

# Update user preferred language each time a request has a new language and activate translation for that user.
# Should be added after LocaleMiddleware as it depends on having request.LANGUAGE_CODE configured there.
def process_request(self, request):
if hasattr(request, 'user'):
user = request.user
if hasattr(user, 'preferred_language'):
if not user.preferred_language or user.preferred_language != request.LANGUAGE_CODE:
user.preferred_language = request.LANGUAGE_CODE
user.save()
else:
translation.activate(user.preferred_language)
request.LANGUAGE_CODE = translation.get_language()

lang_in_url = request.GET.get('lang')
if lang_in_url:
translation.activate(lang_in_url)
request.LANGUAGE_CODE = translation.get_language()

4 changes: 4 additions & 0 deletions accountsplus/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import timezone_field
import localflavor.us.models

import settings

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -103,6 +105,8 @@ class BaseUser(django.contrib.auth.base_user.AbstractBaseUser, django.contrib.au
last_name = django.db.models.CharField(_('Last Name'), max_length=50)
email = django.db.models.EmailField(_('Email'), unique=True)
timezone = timezone_field.TimeZoneField(default='America/New_York')
preferred_language = django.db.models.CharField(_('Preferred Language'), choices=settings.SUPPORTED_LANGUAGES,
blank=True, null=True, max_length=10)

objects = UserManager()

Expand Down
19 changes: 19 additions & 0 deletions accountsplus/settings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.apps import apps
from django.utils.translation import ugettext_lazy as _


# Default values
LOCKOUT_TEMPLATE = 'accounts/locked_out.html'

ENGLISH_LANGUAGE = 'en-us'
SPANISH_LANGUAGE = 'es'
FRENCH_LANGUAGE = 'fr'
PORTUGUESE_LANGUAGE = 'pt'
ARABIC_LANGUAGE = 'ar'
DEFAULT_SUPPORTED_LANGUAGES = (
(ENGLISH_LANGUAGE, _('English')),
(SPANISH_LANGUAGE, _('Spanish')),
(FRENCH_LANGUAGE, _('French')),
(PORTUGUESE_LANGUAGE, _('Portuguese')),
(ARABIC_LANGUAGE, _('Arabic')),
)


def get_setting(setting_str, is_required, default_value=None):
try:
return getattr(settings, setting_str)
Expand Down Expand Up @@ -60,3 +75,7 @@ def get_lockout_template():
LOGIN_FAILURE_LIMIT = int(get_login_failure_limit())
LOCKOUT_URL = str(get_lockout_url())
LOCKOUT_TEMPLATE = get_lockout_template()


SUPPORTED_LANGUAGES = get_setting('LANGUAGES', False, DEFAULT_SUPPORTED_LANGUAGES)
DEFAULT_LANGUAGE = get_setting('LANGUAGE_CODE', False, ENGLISH_LANGUAGE)
12 changes: 12 additions & 0 deletions accountsplus/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,15 @@ def validate(self, password, user=None):
def get_help_text(self):
return _('Password should contain uppercase, lowercase, numeric values and at least '
'one of the following $@#!%*?&')


class CustomPasswordValidator(object):

def validate(self, password, user=None):
regex = '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nasief will this regex allow special characters? have you tested it locally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@waseem-omar Yes I did. The validator here only makes restrictions related to lowercase, uppercase and digits other characters are allowed.

if not re.match(regex, password):
raise ValidationError(_('Password should contain uppercase, lowercase, numeric values and could contain a '
'special character'), code='password_is_weak')

def get_help_text(self):
return _('Password should contain uppercase, lowercase, numeric values and could contain a special character')
11 changes: 10 additions & 1 deletion accountsplus/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def password_reset(request,
template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html',
subject_template_name='registration/password_reset_subject.txt',
password_reset_form=django.contrib.auth.forms.PasswordResetForm,
password_reset_form=forms.CustomPasswordResetForm,
token_generator=django.contrib.auth.views.default_token_generator,
post_reset_redirect=None,
from_email=None,
Expand All @@ -172,6 +172,15 @@ def password_reset(request,
html_email_template_name=None,
extra_email_context=None):
User = django.contrib.auth.get_user_model()
# We set this always in the middleware to the preferred language of the user
if request.method == 'POST':
email = request.POST['email']
extra_email_context = extra_email_context or {}
try:
user = User.objects.get(email=email)
extra_email_context['email_lang'] = user.preferred_language or request.LANGUAGE_CODE
except User.DoesNotExist:
pass

response = django.contrib.auth.views.password_reset(
request, template_name, email_template_name,
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ flake8
pep8
pyflakes
django-timezone-field>=2.0rc1
django-localflavor
django-localflavor==1.3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danaspiegel do you think locking version like this the requirements.txt is acceptable? I think its used for testing only... so I'm assuming its ok... please note also Django also has fixed version above...

bcrypt==3.1.0
mock
git+https://github.com/foundertherapy/django-axes.git@remove_dependency_on_ip
Expand Down