From 8d06eb3aae79548eee7239313e49458e3f017432 Mon Sep 17 00:00:00 2001 From: nasief Date: Mon, 11 Sep 2017 14:19:16 +0300 Subject: [PATCH 1/3] Support language on uer level and update password validator --- README.rst | 5 +++++ accountsplus/__init__.py | 2 +- accountsplus/context_managers.py | 15 +++++++++++++++ accountsplus/forms.py | 12 +++++++++++- accountsplus/middleware.py | 24 ++++++++++++++++++++++++ accountsplus/models.py | 4 ++++ accountsplus/settings.py | 19 +++++++++++++++++++ accountsplus/views.py | 11 ++++++++++- requirements.txt | 2 +- 9 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 accountsplus/context_managers.py diff --git a/README.rst b/README.rst index 201bcf9..363df6a 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/accountsplus/__init__.py b/accountsplus/__init__.py index 8f6b514..55fbdd6 100644 --- a/accountsplus/__init__.py +++ b/accountsplus/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.4.2' +__version__ = '1.4.3' default_app_config = 'accountsplus.apps.AccountsConfig' diff --git a/accountsplus/context_managers.py b/accountsplus/context_managers.py new file mode 100644 index 0000000..f5a1a8b --- /dev/null +++ b/accountsplus/context_managers.py @@ -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) diff --git a/accountsplus/forms.py b/accountsplus/forms.py index 6c9fbbc..c91a1b8 100644 --- a/accountsplus/forms.py +++ b/accountsplus/forms.py @@ -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() @@ -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) diff --git a/accountsplus/middleware.py b/accountsplus/middleware.py index 3caa62b..ae64e358 100644 --- a/accountsplus/middleware.py +++ b/accountsplus/middleware.py @@ -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() + diff --git a/accountsplus/models.py b/accountsplus/models.py index 2f6d487..c8a9856 100644 --- a/accountsplus/models.py +++ b/accountsplus/models.py @@ -16,6 +16,8 @@ import timezone_field import localflavor.us.models +import settings + logger = logging.getLogger(__name__) @@ -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() diff --git a/accountsplus/settings.py b/accountsplus/settings.py index d281463..6457653 100644 --- a/accountsplus/settings.py +++ b/accountsplus/settings.py @@ -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) @@ -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) diff --git a/accountsplus/views.py b/accountsplus/views.py index 015afae..fbe8cd3 100644 --- a/accountsplus/views.py +++ b/accountsplus/views.py @@ -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, @@ -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, diff --git a/requirements.txt b/requirements.txt index 2d1a8fc..e747de7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ flake8 pep8 pyflakes django-timezone-field>=2.0rc1 -django-localflavor +django-localflavor==1.3 bcrypt==3.1.0 mock git+https://github.com/foundertherapy/django-axes.git@remove_dependency_on_ip From c0b52af50092ff7ba5f30085fe2554ab49f8ad3e Mon Sep 17 00:00:00 2001 From: Nasief Khlaif Date: Thu, 24 May 2018 13:25:17 +0300 Subject: [PATCH 2/3] New Password Validator --- accountsplus/validators.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/accountsplus/validators.py b/accountsplus/validators.py index 51b5898..6884cf4 100644 --- a/accountsplus/validators.py +++ b/accountsplus/validators.py @@ -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]' + 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') From ea0d8e4f6f414820eb30e70a9e37144c74f33c36 Mon Sep 17 00:00:00 2001 From: Nasief Khlaif Date: Thu, 24 May 2018 16:33:32 +0300 Subject: [PATCH 3/3] Update version --- accountsplus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accountsplus/__init__.py b/accountsplus/__init__.py index 55fbdd6..32b7758 100644 --- a/accountsplus/__init__.py +++ b/accountsplus/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.4.3' +__version__ = '1.4.5' default_app_config = 'accountsplus.apps.AccountsConfig'