├── account ├── tests │ ├── __init__.py │ ├── templates │ │ └── account │ │ │ ├── admin_approval_sent.html │ │ │ ├── login.html │ │ │ ├── logout.html │ │ │ ├── signup.html │ │ │ ├── email │ │ │ ├── password_change.txt │ │ │ ├── email_confirmation_message.txt │ │ │ ├── email_confirmation_subject.txt │ │ │ ├── password_change_subject.txt │ │ │ ├── password_reset_subject.txt │ │ │ └── password_reset.txt │ │ │ ├── settings.html │ │ │ ├── email_confirm.html │ │ │ ├── password_change.html │ │ │ ├── signup_closed.html │ │ │ ├── password_reset_sent.html │ │ │ ├── password_reset_token.html │ │ │ ├── email_confirmation_sent.html │ │ │ └── password_reset_token_fail.html │ ├── urls.py │ ├── test_decorators.py │ ├── test_email_address.py │ ├── settings.py │ ├── test_models.py │ ├── test_auth.py │ ├── test_password.py │ └── test_commands.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── expunge_deleted.py │ │ ├── user_password_expiry.py │ │ └── user_password_history.py ├── migrations │ ├── __init__.py │ ├── 0007_alter_emailconfirmation_sent.py │ ├── 0006_alter_signupcode_max_uses.py │ ├── 0004_auto_20170416_1821.py │ ├── 0003_passwordexpiry_passwordhistory.py │ └── 0005_update_default_language.py ├── templatetags │ ├── __init__.py │ └── account_tags.py ├── __init__.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── tr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa_IR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sk_SK │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_TW │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── context_processors.py ├── fields.py ├── signals.py ├── decorators.py ├── mixins.py ├── managers.py ├── urls.py ├── admin.py ├── auth_backends.py ├── conf.py ├── languages.py ├── hooks.py ├── middleware.py ├── utils.py └── forms.py ├── .tx └── config ├── manage.py ├── .deepsource.toml ├── MANIFEST.in ├── tox.ini ├── docs ├── index.rst ├── conf.py ├── commands.rst ├── faq.rst ├── signals.rst ├── installation.rst ├── Makefile ├── migration.rst ├── settings.rst ├── templates.rst └── usage.rst ├── .gitignore ├── runtests.py ├── makemigrations.py ├── LICENSE ├── .github └── workflows │ └── ci.yaml ├── pyproject.toml ├── CHANGELOG.md └── README.md /account/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.3.2" 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/admin_approval_sent.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/tests/templates/account/login.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/email/password_change.txt: -------------------------------------------------------------------------------- 1 | bbb 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/settings.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/email/email_confirmation_message.txt: -------------------------------------------------------------------------------- 1 | bbbb -------------------------------------------------------------------------------- /account/tests/templates/account/email/email_confirmation_subject.txt: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /account/tests/templates/account/email/password_change_subject.txt: -------------------------------------------------------------------------------- 1 | aaa 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/email/password_reset_subject.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/password_reset_sent.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/password_reset_token.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/email_confirmation_sent.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/password_reset_token_fail.html: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /account/tests/templates/account/email/password_reset.txt: -------------------------------------------------------------------------------- 1 | {{ password_reset_url }} 2 | -------------------------------------------------------------------------------- /account/locale/ar/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/ar/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/ja/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/ja/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/fa_IR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/fa_IR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/sk_SK/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/sk_SK/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/zh_CN/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/zh_CN/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/locale/zh_TW/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinax/django-user-accounts/HEAD/account/locale/zh_TW/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /account/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, re_path 2 | 3 | urlpatterns = [ 4 | re_path(r"^", include("account.urls")), 5 | ] 6 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [django-user-accounts.djangopo] 5 | file_filter = account/locale//LC_MESSAGES/django.po 6 | source_lang = en 7 | source_file = account/locale/en/LC_MESSAGES/django.po 8 | -------------------------------------------------------------------------------- /account/context_processors.py: -------------------------------------------------------------------------------- 1 | from account.conf import settings 2 | from account.models import Account 3 | 4 | 5 | def account(request): 6 | ctx = { 7 | "account": Account.for_request(request), 8 | "ACCOUNT_OPEN_SIGNUP": settings.ACCOUNT_OPEN_SIGNUP, 9 | } 10 | return ctx 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "account.tests.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | exclude_patterns = [ 4 | "makemigrations.py", 5 | "runtests.py", 6 | "account/tests/**", 7 | "account/tests/test_*.py", 8 | ] 9 | 10 | [[analyzers]] 11 | name = "python" 12 | enabled = true 13 | 14 | [analyzers.meta] 15 | max_line_length = 120 16 | runtime_version = "3.x.x" 17 | -------------------------------------------------------------------------------- /account/management/commands/expunge_deleted.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from account.models import AccountDeletion 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | help = "Expunge accounts deleted more than 48 hours ago." 9 | 10 | def handle(self, *args, **options): 11 | count = AccountDeletion.expunge() 12 | print("{0} expunged.".format(count)) 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.py[cod] 2 | include CHANGELOG.md 3 | include LICENSE 4 | include README.md 5 | recursive-include account *.html 6 | recursive-include account *.txt 7 | recursive-include account/locale * 8 | recursive-include account/migrations *.py 9 | recursive-include docs *.rst 10 | exclude tox.ini 11 | recursive-exclude django_user_accounts.egg-info * 12 | exclude docs/conf.py 13 | exclude docs/Makefile 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E265,E501 3 | max-line-length = 100 4 | max-complexity = 10 5 | exclude = **/migrations,docs 6 | inline-quotes = double 7 | 8 | [coverage:run] 9 | source = account 10 | omit = account/conf.py,tests/*,account/migrations/* 11 | branch = true 12 | data_file = .coverage 13 | 14 | [coverage:report] 15 | omit = account/conf.py,tests/*,account/migrations/* 16 | exclude_lines = 17 | coverage: omit 18 | show_missing = True 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | django-user-accounts 3 | ==================== 4 | 5 | Provides user accounts to a Django project. 6 | 7 | Development 8 | ----------- 9 | 10 | The source repository can be found at https://github.com/pinax/django-user-accounts/ 11 | 12 | 13 | Contents 14 | ======== 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | installation 20 | usage 21 | settings 22 | templates 23 | signals 24 | commands 25 | migration 26 | faq 27 | -------------------------------------------------------------------------------- /account/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from account.conf import settings 4 | 5 | 6 | class TimeZoneField(models.CharField): 7 | 8 | def __init__(self, *args, **kwargs): 9 | defaults = { 10 | "max_length": 100, 11 | "default": "", 12 | "choices": settings.ACCOUNT_TIMEZONES, 13 | "blank": True, 14 | } 15 | defaults.update(kwargs) 16 | super(TimeZoneField, self).__init__(*args, **defaults) 17 | -------------------------------------------------------------------------------- /account/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | user_signed_up = django.dispatch.Signal() 4 | user_sign_up_attempt = django.dispatch.Signal() 5 | user_logged_in = django.dispatch.Signal() 6 | user_login_attempt = django.dispatch.Signal() 7 | signup_code_sent = django.dispatch.Signal() 8 | signup_code_used = django.dispatch.Signal() 9 | email_confirmed = django.dispatch.Signal() 10 | email_confirmation_sent = django.dispatch.Signal() 11 | password_changed = django.dispatch.Signal() 12 | password_expired = django.dispatch.Signal() 13 | -------------------------------------------------------------------------------- /account/migrations/0007_alter_emailconfirmation_sent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-12 21:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('account', '0006_alter_signupcode_max_uses'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='emailconfirmation', 15 | name='sent', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /account/migrations/0006_alter_signupcode_max_uses.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-12 20:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('account', '0005_update_default_language'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='signupcode', 15 | name='max_uses', 16 | field=models.PositiveIntegerField(default=1, verbose_name='max uses'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /account/migrations/0004_auto_20170416_1821.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-04-16 18:21 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0003_passwordexpiry_passwordhistory'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='passwordhistory', 17 | options={'verbose_name': 'password history', 'verbose_name_plural': 'password histories'}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | docs/_build/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | *.eggs 25 | .python-version 26 | 27 | # Pipfile 28 | Pipfile 29 | Pipfile.lock 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # IDEs 44 | .idea/ 45 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import django 6 | 7 | 8 | def runtests(*test_args): 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "account.tests.settings") 10 | django.setup() 11 | 12 | parent = os.path.dirname(os.path.abspath(__file__)) 13 | sys.path.insert(0, parent) 14 | 15 | from django.test.runner import DiscoverRunner 16 | runner_class = DiscoverRunner 17 | if not test_args: 18 | test_args = ["account/tests"] 19 | 20 | failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) 21 | sys.exit(failures) 22 | 23 | 24 | if __name__ == "__main__": 25 | runtests(*sys.argv[1:]) 26 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import sys 4 | 5 | 6 | extensions = [] 7 | templates_path = [] 8 | source_suffix = ".rst" 9 | master_doc = "index" 10 | project = "django-user-accounts" 11 | copyright_holder = "James Tauber and contributors" 12 | copyright = f"{datetime.datetime.now().year}, {copyright_holder}" 13 | exclude_patterns = ["_build"] 14 | pygments_style = "sphinx" 15 | html_theme = "default" 16 | htmlhelp_basename = f"{project}doc" 17 | latex_documents = [ 18 | ("index", f"{project}.tex", f"{project} Documentation", 19 | "Pinax", "manual"), 20 | ] 21 | man_pages = [ 22 | ("index", project, f"{project} Documentation", 23 | ["Pinax"], 1) 24 | ] 25 | 26 | sys.path.insert(0, os.pardir) 27 | m = __import__("account") 28 | 29 | version = m.__version__ 30 | release = version 31 | -------------------------------------------------------------------------------- /account/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.http import HttpResponse 4 | from django.test import TestCase 5 | 6 | from account.decorators import login_required 7 | 8 | 9 | @login_required 10 | def mock_view(request, *args, **kwargs): 11 | return HttpResponse("OK", status=200) 12 | 13 | 14 | class LoginRequiredDecoratorTestCase(TestCase): 15 | 16 | def test_authenticated_user_is_allowed(self): 17 | request = mock.MagicMock() 18 | request.user.is_authenticated = True 19 | response = mock_view(request) 20 | self.assertEqual(response.status_code, 200) 21 | 22 | def test_unauthenticated_user_gets_redirected(self): 23 | request = mock.MagicMock() 24 | request.user.is_authenticated = False 25 | response = mock_view(request) 26 | self.assertEqual(response.status_code, 302) 27 | -------------------------------------------------------------------------------- /account/tests/test_email_address.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.forms import ValidationError 3 | from django.test import TestCase, override_settings 4 | 5 | from account.models import EmailAddress 6 | 7 | 8 | @override_settings(ACCOUNT_EMAIL_UNIQUE=True) 9 | class UniqueEmailAddressTestCase(TestCase): 10 | def test_unique_email(self): 11 | user = User.objects.create_user("user1", email="user1@example.com", password="password") 12 | 13 | email_1 = EmailAddress(user=user, email="user2@example.com") 14 | email_1.full_clean() 15 | email_1.save() 16 | 17 | validation_error = False 18 | try: 19 | email_2 = EmailAddress(user=user, email="USER2@example.com") 20 | email_2.full_clean() 21 | email_2.save() 22 | except ValidationError: 23 | validation_error = True 24 | 25 | self.assertTrue(validation_error) 26 | -------------------------------------------------------------------------------- /account/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from django.contrib.auth import REDIRECT_FIELD_NAME 4 | 5 | from account.utils import handle_redirect_to_login 6 | 7 | 8 | def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): 9 | """ 10 | Decorator for views that checks that the user is logged in, redirecting 11 | to the log in page if necessary. 12 | """ 13 | def decorator(view_func): 14 | @functools.wraps(view_func) 15 | def _wrapped_view(request, *args, **kwargs): 16 | if request.user.is_authenticated: 17 | return view_func(request, *args, **kwargs) 18 | return handle_redirect_to_login( 19 | request, 20 | redirect_field_name=redirect_field_name, 21 | login_url=login_url 22 | ) 23 | return _wrapped_view 24 | if func: 25 | return decorator(func) 26 | return decorator 27 | -------------------------------------------------------------------------------- /account/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import REDIRECT_FIELD_NAME 2 | 3 | from account.conf import settings 4 | from account.utils import handle_redirect_to_login 5 | 6 | 7 | class LoginRequiredMixin: 8 | 9 | redirect_field_name = REDIRECT_FIELD_NAME 10 | login_url = None 11 | 12 | def dispatch(self, request, *args, **kwargs): 13 | self.request = request 14 | self.args = args 15 | self.kwargs = kwargs 16 | if request.user.is_authenticated: 17 | return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) 18 | return self.redirect_to_login() 19 | 20 | def get_login_url(self): 21 | return self.login_url or settings.ACCOUNT_LOGIN_URL 22 | 23 | def get_next_url(self): 24 | return self.request.get_full_path() 25 | 26 | def redirect_to_login(self): 27 | return handle_redirect_to_login( 28 | self.request, 29 | redirect_field_name=self.redirect_field_name, 30 | login_url=self.get_login_url(), 31 | next_url=self.get_next_url(), 32 | ) 33 | -------------------------------------------------------------------------------- /account/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class EmailAddressManager(models.Manager): 5 | 6 | def add_email(self, user, email, **kwargs): 7 | confirm = kwargs.pop("confirm", False) 8 | email_address = self.create(user=user, email=email, **kwargs) 9 | if confirm and not email_address.verified: 10 | email_address.send_confirmation() 11 | return email_address 12 | 13 | def get_primary(self, user): 14 | try: 15 | return self.get(user=user, primary=True) 16 | except self.model.DoesNotExist: 17 | return None 18 | 19 | def get_users_for(self, email): 20 | # this is a list rather than a generator because we probably want to 21 | # do a len() on it right away 22 | return [address.user for address in self.filter(verified=True, email=email)] 23 | 24 | 25 | class EmailConfirmationManager(models.Manager): 26 | 27 | def delete_expired_confirmations(self): 28 | for confirmation in self.all(): 29 | if confirmation.key_expired(): 30 | confirmation.delete() 31 | -------------------------------------------------------------------------------- /makemigrations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import django 6 | from django.conf import settings 7 | 8 | DEFAULT_SETTINGS = dict( 9 | INSTALLED_APPS=[ 10 | "django.contrib.auth", 11 | "django.contrib.contenttypes", 12 | "django.contrib.sites", 13 | "account", 14 | "account.tests" 15 | ], 16 | MIDDLEWARE_CLASSES=[], 17 | DATABASES={ 18 | "default": { 19 | "ENGINE": "django.db.backends.sqlite3", 20 | "NAME": ":memory:", 21 | } 22 | }, 23 | SITE_ID=1, 24 | ROOT_URLCONF="account.tests.urls", 25 | SECRET_KEY="notasecret", 26 | ) 27 | 28 | 29 | def run(*args): 30 | if not settings.configured: 31 | settings.configure(**DEFAULT_SETTINGS) 32 | 33 | django.setup() 34 | 35 | parent = os.path.dirname(os.path.abspath(__file__)) 36 | sys.path.insert(0, parent) 37 | 38 | django.core.management.call_command( 39 | "makemigrations", 40 | "account", 41 | *args 42 | ) 43 | 44 | 45 | if __name__ == "__main__": 46 | run(*sys.argv[1:]) 47 | -------------------------------------------------------------------------------- /account/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from account.views import ( 4 | ChangePasswordView, 5 | ConfirmEmailView, 6 | DeleteView, 7 | LoginView, 8 | LogoutView, 9 | PasswordResetTokenView, 10 | PasswordResetView, 11 | SettingsView, 12 | SignupView, 13 | ) 14 | 15 | urlpatterns = [ 16 | path("signup/", SignupView.as_view(), name="account_signup"), 17 | path("login/", LoginView.as_view(), name="account_login"), 18 | path("logout/", LogoutView.as_view(), name="account_logout"), 19 | path("confirm_email//", ConfirmEmailView.as_view(), name="account_confirm_email"), 20 | path("password/", ChangePasswordView.as_view(), name="account_password"), 21 | path("password/reset/", PasswordResetView.as_view(), name="account_password_reset"), 22 | path( 23 | "password/reset///", 24 | PasswordResetTokenView.as_view(), 25 | name="account_password_reset_token", 26 | ), 27 | path("settings/", SettingsView.as_view(), name="account_settings"), 28 | path("delete/", DeleteView.as_view(), name="account_delete"), 29 | ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-present James Tauber and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/commands.rst: -------------------------------------------------------------------------------- 1 | .. _commands: 2 | 3 | =================== 4 | Management Commands 5 | =================== 6 | 7 | user_password_history 8 | --------------------- 9 | 10 | Creates an initial password history for all users who don't already 11 | have a password history. 12 | 13 | Accepts two optional arguments:: 14 | 15 | -d --days - Sets the age of the current password. Default is 10 days. 16 | -f --force - Sets a new password history for ALL users, regardless of prior history. 17 | 18 | user_password_expiry 19 | -------------------- 20 | 21 | Creates a password expiry specific to one user. 22 | 23 | Password expiration checks use a global value (``ACCOUNT_PASSWORD_EXPIRY``) 24 | for the expiration time period. This value can be superseded on a per-user basis 25 | by creating a user password expiry. 26 | 27 | Requires one argument:: 28 | 29 | [] - username(s) of the user(s) who needs specific password expiry. 30 | 31 | Accepts one optional argument:: 32 | 33 | -e --expire - Sets the number of seconds for password expiration. 34 | Default is the current global ACCOUNT_PASSWORD_EXPIRY value. 35 | 36 | After creation, you can modify user password expiration from the Django 37 | admin. Find the desired user at ``/admin/account/passwordexpiry/`` and change the ``expiry`` value. 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Lints and Tests 2 | on: [push] 3 | jobs: 4 | lint: 5 | name: Linting 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Set up Python 3.11 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.11" 15 | 16 | - name: Install lint dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install ruff 20 | 21 | - name: Lint with ruff 22 | run: | 23 | ruff --format=github --target-version=py311 account 24 | 25 | test: 26 | name: Testing 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | python: 31 | - "3.8" 32 | - "3.9" 33 | - "3.10" 34 | - "3.11" 35 | django: 36 | - "3.2.*" 37 | - "4.2.*" 38 | exclude: 39 | - python: "3.11" 40 | django: "3.2.*" 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - name: Setup Python 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: ${{ matrix.python }} 49 | 50 | - name: Install Django 51 | shell: bash 52 | run: pip install Django==${{ matrix.django }} 'django-appconf>=1.0.4' 'pytz>=2020.4' 53 | 54 | - name: Running Python Tests 55 | shell: bash 56 | run: python3 runtests.py 57 | -------------------------------------------------------------------------------- /account/management/commands/user_password_expiry.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.core.management.base import LabelCommand 3 | 4 | from account.conf import settings 5 | from account.models import PasswordExpiry 6 | 7 | 8 | class Command(LabelCommand): 9 | 10 | help = "Create user-specific password expiration period." 11 | label = "username" 12 | 13 | def add_arguments(self, parser): 14 | super(Command, self).add_arguments(parser) 15 | parser.add_argument( 16 | "-e", "--expire", 17 | type=int, 18 | nargs="?", 19 | default=settings.ACCOUNT_PASSWORD_EXPIRY, 20 | help="number of seconds until password expires" 21 | ) 22 | 23 | def handle_label(self, username, **options): 24 | User = get_user_model() 25 | try: 26 | user = User.objects.get(username=username) 27 | except User.DoesNotExist: 28 | return 'User "{}" not found'.format(username) 29 | 30 | expire = options["expire"] 31 | 32 | # Modify existing PasswordExpiry or create new if needed. 33 | if not hasattr(user, "password_expiry"): 34 | PasswordExpiry.objects.create(user=user, expiry=expire) 35 | else: 36 | user.password_expiry.expiry = expire 37 | user.password_expiry.save() 38 | 39 | return 'User "{}" password expiration set to {} seconds'.format(username, expire) 40 | -------------------------------------------------------------------------------- /account/management/commands/user_password_history.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.core.management.base import BaseCommand 5 | 6 | import pytz 7 | from account.models import PasswordHistory 8 | 9 | 10 | class Command(BaseCommand): 11 | 12 | help = "Create password history for all users without existing history." 13 | 14 | def add_arguments(self, parser): 15 | parser.add_argument( 16 | "-d", "--days", 17 | type=int, 18 | nargs="?", 19 | default=10, 20 | help="age of current password (in days)" 21 | ) 22 | parser.add_argument( 23 | "-f", "--force", 24 | action="store_true", 25 | help="create new password history for all users, regardless of existing history" 26 | ) 27 | 28 | def handle(self, *args, **options): 29 | User = get_user_model() 30 | users = User.objects.all() 31 | if not options["force"]: 32 | users = users.filter(password_history=None) 33 | 34 | if not users: 35 | return "No users found without password history" 36 | 37 | days = options["days"] 38 | timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=days) 39 | 40 | # Create new PasswordHistory on `timestamp` 41 | PasswordHistory.objects.bulk_create( 42 | [PasswordHistory(user=user, timestamp=timestamp) for user in users] 43 | ) 44 | 45 | return "Password history set to {} for {} users".format(timestamp, len(users)) 46 | -------------------------------------------------------------------------------- /account/migrations/0003_passwordexpiry_passwordhistory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2016-09-13 08:55 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('account', '0002_fix_str'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='PasswordExpiry', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('expiry', models.PositiveIntegerField(default=0)), 24 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='password_expiry', to=settings.AUTH_USER_MODEL, verbose_name='user')), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='PasswordHistory', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('password', models.CharField(max_length=255)), 32 | ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), 33 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_history', to=settings.AUTH_USER_MODEL)), 34 | ], 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /account/tests/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | USE_TZ = True 3 | INSTALLED_APPS = [ 4 | "django.contrib.auth", 5 | "django.contrib.contenttypes", 6 | "django.contrib.sessions", 7 | "django.contrib.sites", 8 | "django.contrib.messages", 9 | "account", 10 | "account.tests", 11 | ] 12 | DATABASES = { 13 | "default": { 14 | "ENGINE": "django.db.backends.sqlite3", 15 | "NAME": ":memory:", 16 | } 17 | } 18 | SITE_ID = 1 19 | ROOT_URLCONF = "account.tests.urls" 20 | SECRET_KEY = "notasecret" 21 | TEMPLATES = [ 22 | { 23 | "BACKEND": "django.template.backends.django.DjangoTemplates", 24 | "DIRS": [ 25 | # insert your TEMPLATE_DIRS here 26 | ], 27 | "APP_DIRS": True, 28 | "OPTIONS": { 29 | "context_processors": [ 30 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 31 | # list if you haven"t customized them: 32 | "django.contrib.auth.context_processors.auth", 33 | "django.template.context_processors.debug", 34 | "django.template.context_processors.i18n", 35 | "django.template.context_processors.media", 36 | "django.template.context_processors.static", 37 | "django.template.context_processors.tz", 38 | "django.contrib.messages.context_processors.messages", 39 | ], 40 | }, 41 | }, 42 | ] 43 | MIDDLEWARE = [ 44 | "django.contrib.sessions.middleware.SessionMiddleware", 45 | "django.contrib.auth.middleware.AuthenticationMiddleware", 46 | "django.contrib.messages.middleware.MessageMiddleware" 47 | ] 48 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 49 | -------------------------------------------------------------------------------- /account/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from account.models import SignupCode 4 | 5 | 6 | class SignupCodeModelTestCase(TestCase): 7 | def test_exists_no_match(self): 8 | code = SignupCode(email="foobar@example.com", code="FOOFOO") 9 | code.save() 10 | 11 | self.assertFalse(SignupCode.exists(code="BARBAR")) 12 | self.assertFalse(SignupCode.exists(email="bar@example.com")) 13 | self.assertFalse(SignupCode.exists(email="bar@example.com", code="BARBAR")) 14 | self.assertFalse(SignupCode.exists()) 15 | 16 | def test_exists_email_only_match(self): 17 | code = SignupCode(email="foobar@example.com", code="FOOFOO") 18 | code.save() 19 | 20 | self.assertTrue(SignupCode.exists(email="foobar@example.com")) 21 | 22 | def test_exists_code_only_match(self): 23 | code = SignupCode(email="foobar@example.com", code="FOOFOO") 24 | code.save() 25 | 26 | self.assertTrue(SignupCode.exists(code="FOOFOO")) 27 | self.assertTrue(SignupCode.exists(email="bar@example.com", code="FOOFOO")) 28 | 29 | def test_exists_email_match_code_mismatch(self): 30 | code = SignupCode(email="foobar@example.com", code="FOOFOO") 31 | code.save() 32 | 33 | self.assertTrue(SignupCode.exists(email="foobar@example.com", code="BARBAR")) 34 | 35 | def test_exists_code_match_email_mismatch(self): 36 | code = SignupCode(email="foobar@example.com", code="FOOFOO") 37 | code.save() 38 | 39 | self.assertTrue(SignupCode.exists(email="bar@example.com", code="FOOFOO")) 40 | 41 | def test_exists_both_match(self): 42 | code = SignupCode(email="foobar@example.com", code="FOOFOO") 43 | code.save() 44 | 45 | self.assertTrue(SignupCode.exists(email="foobar@example.com", code="FOOFOO")) 46 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | 3 | === 4 | FAQ 5 | === 6 | 7 | This document is a collection of frequently asked questions about 8 | django-user-accounts. 9 | 10 | What is the difference between django-user-accounts and django.contrib.auth? 11 | ============================================================================ 12 | 13 | django-user-accounts is designed to supplement ``django.contrib.auth``. This 14 | app provides improved views for log in, password reset, log out and adds 15 | sign up functionality. We try not to duplicate code when Django provides a 16 | good implementation. For example, we did not re-implement password reset, but 17 | simply provide an improved view which calls into the secure Django password 18 | reset code. ``django.contrib.auth`` is still providing many of supporting 19 | elements such as ``User`` model, default authentication backends, helper 20 | functions and authorization. 21 | 22 | django-user-accounts takes your Django project from having simple log in, 23 | log out and password reset to a full blown account management system that you 24 | will end up building anyways. 25 | 26 | Why can email addresses get out of sync? 27 | ======================================== 28 | 29 | django-user-accounts stores email addresses in two locations. The default 30 | ``User`` model contains an ``email`` field and django-user-accounts provides an 31 | ``EmailAddress`` model. This latter is provided to support multiple email 32 | addresses per user. 33 | 34 | If you use a custom user model you can prevent the double storage. This is 35 | because you can choose not to do any email address storage. 36 | 37 | If you don't use a custom user model then make sure you take extra precaution. 38 | When editing email addresses either in the shell or admin make sure you update 39 | in both places. Only the primary email address is stored on the ``User`` model. 40 | -------------------------------------------------------------------------------- /account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from account.models import ( 4 | Account, 5 | AccountDeletion, 6 | EmailAddress, 7 | PasswordExpiry, 8 | PasswordHistory, 9 | SignupCode, 10 | ) 11 | 12 | 13 | class SignupCodeAdmin(admin.ModelAdmin): 14 | 15 | list_display = ["code", "max_uses", "use_count", "expiry", "created"] 16 | search_fields = ["code", "email"] 17 | list_filter = ["created"] 18 | raw_id_fields = ["inviter"] 19 | 20 | 21 | class AccountAdmin(admin.ModelAdmin): 22 | 23 | raw_id_fields = ["user"] 24 | 25 | def get_queryset(self, request): 26 | return super().get_queryset(request).select_related('user') 27 | 28 | 29 | class AccountDeletionAdmin(AccountAdmin): 30 | 31 | list_display = ["email", "date_requested", "date_expunged"] 32 | 33 | 34 | class EmailAddressAdmin(AccountAdmin): 35 | 36 | list_display = ["user", "email", "verified", "primary"] 37 | search_fields = ["email", "user__username"] 38 | 39 | def get_queryset(self, request): 40 | return super().get_queryset(request).select_related('user') 41 | 42 | 43 | class PasswordExpiryAdmin(admin.ModelAdmin): 44 | 45 | raw_id_fields = ["user"] 46 | 47 | 48 | class PasswordHistoryAdmin(admin.ModelAdmin): 49 | 50 | raw_id_fields = ["user"] 51 | list_display = ["user", "timestamp"] 52 | list_filter = ["user"] 53 | ordering = ["user__username", "-timestamp"] 54 | 55 | def get_queryset(self, request): 56 | return super().get_queryset(request).select_related('user') 57 | 58 | 59 | admin.site.register(Account, AccountAdmin) 60 | admin.site.register(SignupCode, SignupCodeAdmin) 61 | admin.site.register(AccountDeletion, AccountDeletionAdmin) 62 | admin.site.register(EmailAddress, EmailAddressAdmin) 63 | admin.site.register(PasswordExpiry, PasswordExpiryAdmin) 64 | admin.site.register(PasswordHistory, PasswordHistoryAdmin) 65 | -------------------------------------------------------------------------------- /docs/signals.rst: -------------------------------------------------------------------------------- 1 | .. _signals: 2 | 3 | ======= 4 | Signals 5 | ======= 6 | 7 | user_signed_up 8 | -------------- 9 | 10 | Triggered when a user signs up successfully. Providing arguments ``user`` 11 | (User instance) and ``form`` (form instance) as arguments. 12 | 13 | 14 | user_sign_up_attempt 15 | -------------------- 16 | 17 | Triggered when a user tried but failed to sign up. Providing arguments 18 | ``username`` (string), ``email`` (string) and ``result`` (boolean, False if 19 | the form did not validate). 20 | 21 | 22 | user_logged_in 23 | -------------- 24 | 25 | Triggered when a user logs in successfully. Providing arguments ``user`` 26 | (User instance) and ``form`` (form instance). 27 | 28 | 29 | user_login_attempt 30 | ------------------ 31 | 32 | Triggered when a user tries and fails to log in. Providing arguments 33 | ``username`` (string) and ``result`` (boolean, False if the form did not 34 | validate). 35 | 36 | 37 | signup_code_sent 38 | ---------------- 39 | 40 | Triggered when a signup code was sent. Providing argument ``signup_code`` 41 | (SignupCode instance). 42 | 43 | 44 | signup_code_used 45 | ---------------- 46 | 47 | Triggered when a user used a signup code. Providing argument 48 | ``signup_code_result`` (SignupCodeResult instance). 49 | 50 | 51 | email_confirmed 52 | --------------- 53 | 54 | Triggered when a user confirmed an email. Providing argument 55 | ``email_address`` (EmailAddress instance). 56 | 57 | 58 | email_confirmation_sent 59 | ----------------------- 60 | 61 | Triggered when an email confirmation was sent. Providing argument 62 | ``confirmation`` (EmailConfirmation instance). 63 | 64 | 65 | password_changed 66 | ---------------- 67 | 68 | Triggered when a user changes his password. Providing argument ``user`` 69 | (User instance). 70 | 71 | 72 | password_expired 73 | ---------------- 74 | 75 | Triggered when a user password is expired. Providing argument ``user`` 76 | (User instance). -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "django-user-accounts" 7 | authors = [{name = "Pinax Team", email = "team@pinaxproject.com"}] 8 | description = "a Django user account app" 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Environment :: Web Environment", 12 | "Framework :: Django", 13 | "Framework :: Django :: 3.2", 14 | "Framework :: Django :: 4.2", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | ] 25 | dependencies = [ 26 | "Django>=3.2", 27 | "django-appconf>=1.0.4", 28 | "pytz>=2020.4", 29 | ] 30 | dynamic = ["version"] 31 | 32 | [project.readme] 33 | file = "README.md" 34 | content-type = "text/markdown" 35 | 36 | [project.urls] 37 | Homepage = "http://github.com/pinax/django-user-accounts" 38 | 39 | [tool.isort] 40 | profile = "hug" 41 | src_paths = ["account"] 42 | multi_line_output = 3 43 | known_django = "django" 44 | known_third_party = "account,six,mock,appconf,jsonfield,pytz" 45 | sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" 46 | skip_glob = "account/migrations/*,docs" 47 | include_trailing_comma = "True" 48 | 49 | [tool.pytest.ini_options] 50 | testpaths = ["account/tests"] 51 | DJANGO_SETTINGS_MODULE = "account.tests.settings" 52 | 53 | [tool.ruff] 54 | line-length = 120 55 | 56 | [tool.ruff.per-file-ignores] 57 | "account/migrations/**.py" = ["E501"] 58 | 59 | [tool.setuptools] 60 | package-dir = {"" = "."} 61 | include-package-data = true 62 | zip-safe = false 63 | 64 | [tool.setuptools.dynamic] 65 | version = {attr = "account.__version__"} 66 | 67 | [tool.setuptools.package-data] 68 | account = ["locale/*/LC_MESSAGES/*"] 69 | -------------------------------------------------------------------------------- /account/auth_backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.backends import ModelBackend 3 | from django.db.models import Q 4 | 5 | from account.models import EmailAddress 6 | from account.utils import get_user_lookup_kwargs 7 | 8 | User = get_user_model() 9 | 10 | 11 | class AccountModelBackend(ModelBackend): 12 | """ 13 | This authentication backend ensures that the account is always selected 14 | on any query with the user, so we don't issue extra unnecessary queries 15 | """ 16 | 17 | def get_user(self, user_id): 18 | """Get the user and select account at the same time""" 19 | user = User._default_manager.filter(pk=user_id).select_related("account").first() 20 | if not user: 21 | return None 22 | return user if self.user_can_authenticate(user) else None 23 | 24 | 25 | class UsernameAuthenticationBackend(AccountModelBackend): 26 | """Username authentication""" 27 | 28 | def authenticate(self, request, username=None, password=None, **kwargs): 29 | """Authenticate the user based on user""" 30 | if username is None or password is None: 31 | return None 32 | 33 | try: 34 | lookup_kwargs = get_user_lookup_kwargs({ 35 | "{username}__iexact": username 36 | }) 37 | user = User.objects.get(**lookup_kwargs) 38 | except User.DoesNotExist: 39 | return None 40 | 41 | if user.check_password(password): 42 | return user 43 | 44 | 45 | class EmailAuthenticationBackend(AccountModelBackend): 46 | """Email authentication""" 47 | 48 | def authenticate(self, request, username=None, password=None, **kwargs): 49 | """Authenticate the user based email""" 50 | qs = EmailAddress.objects.filter(Q(primary=True) | Q(verified=True)) 51 | 52 | if username is None or password is None: 53 | return None 54 | 55 | try: 56 | email_address = qs.get(email__iexact=username) 57 | except EmailAddress.DoesNotExist: 58 | return None 59 | 60 | user = email_address.user 61 | if user.check_password(password): 62 | return user 63 | -------------------------------------------------------------------------------- /account/conf.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from django.conf import settings # noqa 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | from account.languages import LANGUAGES 7 | from account.timezones import TIMEZONES 8 | from appconf import AppConf 9 | 10 | 11 | def load_path_attr(path): 12 | i = path.rfind(".") 13 | module, attr = path[:i], path[i + 1:] 14 | try: 15 | mod = importlib.import_module(module) 16 | except ImportError as e: 17 | raise ImproperlyConfigured("Error importing {0}: '{1}'".format(module, e)) 18 | try: 19 | attr = getattr(mod, attr) 20 | except AttributeError: 21 | raise ImproperlyConfigured("Module '{0}' does not define a '{1}'".format(module, attr)) 22 | return attr 23 | 24 | 25 | class AccountAppConf(AppConf): 26 | 27 | OPEN_SIGNUP = True 28 | LOGIN_URL = "account_login" 29 | LOGOUT_URL = "account_logout" 30 | SIGNUP_REDIRECT_URL = "/" 31 | LOGIN_REDIRECT_URL = "/" 32 | LOGOUT_REDIRECT_URL = "/" 33 | PASSWORD_CHANGE_REDIRECT_URL = "account_password" 34 | PASSWORD_RESET_REDIRECT_URL = "account_login" 35 | PASSWORD_RESET_TOKEN_URL = "account_password_reset_token" 36 | PASSWORD_EXPIRY = 0 37 | PASSWORD_USE_HISTORY = False 38 | ACCOUNT_APPROVAL_REQUIRED = False 39 | PASSWORD_STRIP = True 40 | REMEMBER_ME_EXPIRY = 60 * 60 * 24 * 365 * 10 41 | USER_DISPLAY = lambda user: user.username # noqa 42 | CREATE_ON_SAVE = True 43 | EMAIL_UNIQUE = True 44 | EMAIL_CONFIRMATION_REQUIRED = False 45 | EMAIL_CONFIRMATION_EMAIL = True 46 | EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 47 | EMAIL_CONFIRMATION_AUTO_LOGIN = False 48 | EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "account_login" 49 | EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None 50 | EMAIL_CONFIRMATION_URL = "account_confirm_email" 51 | SETTINGS_REDIRECT_URL = "account_settings" 52 | NOTIFY_ON_PASSWORD_CHANGE = True 53 | DELETION_EXPUNGE_HOURS = 48 54 | DEFAULT_HTTP_PROTOCOL = "https" 55 | HOOKSET = "account.hooks.AccountDefaultHookSet" 56 | TIMEZONES = TIMEZONES 57 | LANGUAGES = LANGUAGES 58 | 59 | @staticmethod 60 | def configure_hookset(value): 61 | return load_path_attr(value)() 62 | -------------------------------------------------------------------------------- /account/languages.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.translation import get_language_info 3 | 4 | """ 5 | # List of language code and languages local names. 6 | # 7 | # This list is output of code: 8 | 9 | [ 10 | (code, get_language_info(code).get("name_local")) 11 | for code, lang in settings.LANGUAGES 12 | ] 13 | """ 14 | 15 | LANGUAGES = [ 16 | ("af", "Afrikaans"), 17 | ("ar", "العربيّة"), 18 | ("ast", "asturian"), 19 | ("az", "Azərbaycanca"), 20 | ("bg", "български"), 21 | ("be", "беларуская"), 22 | ("bn", "বাংলা"), 23 | ("br", "brezhoneg"), 24 | ("bs", "bosanski"), 25 | ("ca", "català"), 26 | ("cs", "česky"), 27 | ("cy", "Cymraeg"), 28 | ("da", "dansk"), 29 | ("de", "Deutsch"), 30 | ("el", "Ελληνικά"), 31 | ("en", "English"), 32 | ("en-au", "Australian English"), 33 | ("en-gb", "British English"), 34 | ("eo", "Esperanto"), 35 | ("es", "español"), 36 | ("es-ar", "español de Argentina"), 37 | ("es-mx", "español de Mexico"), 38 | ("es-ni", "español de Nicaragua"), 39 | ("es-ve", "español de Venezuela"), 40 | ("et", "eesti"), 41 | ("eu", "Basque"), 42 | ("fa", "فارسی"), 43 | ("fi", "suomi"), 44 | ("fr", "français"), 45 | ("fy", "frysk"), 46 | ("ga", "Gaeilge"), 47 | ("gl", "galego"), 48 | ("he", "עברית"), 49 | ("hi", "Hindi"), 50 | ("hr", "Hrvatski"), 51 | ("hu", "Magyar"), 52 | ("ia", "Interlingua"), 53 | ("id", "Bahasa Indonesia"), 54 | ("io", "ido"), 55 | ("is", "Íslenska"), 56 | ("it", "italiano"), 57 | ("ja", "日本語"), 58 | ("ka", "ქართული"), 59 | ("kk", "Қазақ"), 60 | ("km", "Khmer"), 61 | ("kn", "Kannada"), 62 | ("ko", "한국어"), 63 | ("lb", "Lëtzebuergesch"), 64 | ("lt", "Lietuviškai"), 65 | ("lv", "latvieš"), 66 | ("mk", "Македонски"), 67 | ("ml", "Malayalam"), 68 | ("mn", "Mongolian"), 69 | ("mr", "मराठी"), 70 | ("my", "မြန်မာဘာသာ"), 71 | ("nb", "norsk (bokmål)"), 72 | ("ne", "नेपाली"), 73 | ("nl", "Nederlands"), 74 | ("nn", "norsk (nynorsk)"), 75 | ("os", "Ирон"), 76 | ("pa", "Punjabi"), 77 | ("pl", "polski"), 78 | ("pt", "Português"), 79 | ("pt-br", "Português Brasileiro"), 80 | ("ro", "Română"), 81 | ("ru", "Русский"), 82 | ("sk", "slovenský"), 83 | ("sl", "Slovenščina"), 84 | ("sq", "shqip"), 85 | ("sr", "српски"), 86 | ("sr-latn", "srpski (latinica)"), 87 | ("sv", "svenska"), 88 | ("sw", "Kiswahili"), 89 | ("ta", "தமிழ்"), 90 | ("te", "తెలుగు"), 91 | ("th", "ภาษาไทย"), 92 | ("tr", "Türkçe"), 93 | ("tt", "Татарча"), 94 | ("udm", "Удмурт"), 95 | ("uk", "Українська"), 96 | ("ur", "اردو"), 97 | ("vi", "Tiếng Việt"), 98 | ("zh-cn", "简体中文"), 99 | ("zh-hans", "简体中文"), 100 | ("zh-hant", "繁體中文"), 101 | ("zh-tw", "繁體中文") 102 | ] 103 | 104 | DEFAULT_LANGUAGE = get_language_info(settings.LANGUAGE_CODE)["code"] 105 | -------------------------------------------------------------------------------- /account/migrations/0005_update_default_language.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-10-03 18:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0004_auto_20170416_1821'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='account', 17 | name='language', 18 | field=models.CharField(choices=[('af', 'Afrikaans'), ('ar', '\u0627\u0644\u0639\u0631\u0628\u064a\u0651\u0629'), ('ast', 'asturian'), ('az', 'Az\u0259rbaycanca'), ('bg', '\u0431\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438'), ('be', '\u0431\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f'), ('bn', '\u09ac\u09be\u0982\u09b2\u09be'), ('br', 'brezhoneg'), ('bs', 'bosanski'), ('ca', 'catal\xe0'), ('cs', '\u010desky'), ('cy', 'Cymraeg'), ('da', 'dansk'), ('de', 'Deutsch'), ('el', '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'), ('en', 'English'), ('en-au', 'Australian English'), ('en-gb', 'British English'), ('eo', 'Esperanto'), ('es', 'espa\xf1ol'), ('es-ar', 'espa\xf1ol de Argentina'), ('es-mx', 'espa\xf1ol de Mexico'), ('es-ni', 'espa\xf1ol de Nicaragua'), ('es-ve', 'espa\xf1ol de Venezuela'), ('et', 'eesti'), ('eu', 'Basque'), ('fa', '\u0641\u0627\u0631\u0633\u06cc'), ('fi', 'suomi'), ('fr', 'fran\xe7ais'), ('fy', 'frysk'), ('ga', 'Gaeilge'), ('gl', 'galego'), ('he', '\u05e2\u05d1\u05e8\u05d9\u05ea'), ('hi', 'Hindi'), ('hr', 'Hrvatski'), ('hu', 'Magyar'), ('ia', 'Interlingua'), ('id', 'Bahasa Indonesia'), ('io', 'ido'), ('is', '\xcdslenska'), ('it', 'italiano'), ('ja', '\u65e5\u672c\u8a9e'), ('ka', '\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8'), ('kk', '\u049a\u0430\u0437\u0430\u049b'), ('km', 'Khmer'), ('kn', 'Kannada'), ('ko', '\ud55c\uad6d\uc5b4'), ('lb', 'L\xebtzebuergesch'), ('lt', 'Lietuvi\u0161kai'), ('lv', 'latvie\u0161'), ('mk', '\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438'), ('ml', 'Malayalam'), ('mn', 'Mongolian'), ('mr', '\u092e\u0930\u093e\u0920\u0940'), ('my', '\u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e\u102c'), ('nb', 'norsk (bokm\xe5l)'), ('ne', '\u0928\u0947\u092a\u093e\u0932\u0940'), ('nl', 'Nederlands'), ('nn', 'norsk (nynorsk)'), ('os', '\u0418\u0440\u043e\u043d'), ('pa', 'Punjabi'), ('pl', 'polski'), ('pt', 'Portugu\xeas'), ('pt-br', 'Portugu\xeas Brasileiro'), ('ro', 'Rom\xe2n\u0103'), ('ru', '\u0420\u0443\u0441\u0441\u043a\u0438\u0439'), ('sk', 'slovensk\xfd'), ('sl', 'Sloven\u0161\u010dina'), ('sq', 'shqip'), ('sr', '\u0441\u0440\u043f\u0441\u043a\u0438'), ('sr-latn', 'srpski (latinica)'), ('sv', 'svenska'), ('sw', 'Kiswahili'), ('ta', '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd'), ('te', '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41'), ('th', '\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22'), ('tr', 'T\xfcrk\xe7e'), ('tt', '\u0422\u0430\u0442\u0430\u0440\u0447\u0430'), ('udm', '\u0423\u0434\u043c\u0443\u0440\u0442'), ('uk', '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430'), ('ur', '\u0627\u0631\u062f\u0648'), ('vi', 'Ti\xea\u0301ng Vi\xea\u0323t'), ('zh-cn', '\u7b80\u4f53\u4e2d\u6587'), ('zh-hans', '\u7b80\u4f53\u4e2d\u6587'), ('zh-hant', '\u7e41\u9ad4\u4e2d\u6587'), ('zh-tw', '\u7e41\u9ad4\u4e2d\u6587')], default='en', max_length=10, verbose_name='language'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /account/hooks.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import random 3 | 4 | from django import forms 5 | from django.core.mail import send_mail 6 | from django.template.loader import render_to_string 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from account.conf import settings 10 | 11 | 12 | class AccountDefaultHookSet: 13 | 14 | @staticmethod 15 | def send_invitation_email(to, ctx): 16 | subject = render_to_string("account/email/invite_user_subject.txt", ctx) 17 | message = render_to_string("account/email/invite_user.txt", ctx) 18 | send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) 19 | 20 | @staticmethod 21 | def send_confirmation_email(to, ctx): 22 | subject = render_to_string("account/email/email_confirmation_subject.txt", ctx) 23 | subject = "".join(subject.splitlines()) # remove superfluous line breaks 24 | message = render_to_string("account/email/email_confirmation_message.txt", ctx) 25 | send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) 26 | 27 | @staticmethod 28 | def send_password_change_email(to, ctx): 29 | subject = render_to_string("account/email/password_change_subject.txt", ctx) 30 | subject = "".join(subject.splitlines()) 31 | message = render_to_string("account/email/password_change.txt", ctx) 32 | send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) 33 | 34 | @staticmethod 35 | def send_password_reset_email(to, ctx): 36 | subject = render_to_string("account/email/password_reset_subject.txt", ctx) 37 | subject = "".join(subject.splitlines()) 38 | message = render_to_string("account/email/password_reset.txt", ctx) 39 | send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) 40 | 41 | @staticmethod 42 | def generate_random_token(extra=None, hash_func=hashlib.sha256): 43 | if extra is None: 44 | extra = [] 45 | bits = extra + [str(random.SystemRandom().getrandbits(512))] 46 | return hash_func("".join(bits).encode("utf-8")).hexdigest() 47 | 48 | def generate_signup_code_token(self, email=None): 49 | extra = [] 50 | if email: 51 | extra.append(email) 52 | return self.generate_random_token(extra) 53 | 54 | def generate_email_confirmation_token(self, email): 55 | return self.generate_random_token([email]) 56 | 57 | @staticmethod 58 | def get_user_credentials(form, identifier_field): 59 | return { 60 | "username": form.cleaned_data[identifier_field], 61 | "password": form.cleaned_data["password"], 62 | } 63 | 64 | @staticmethod 65 | def clean_password(password_new, password_new_confirm): 66 | if password_new != password_new_confirm: 67 | raise forms.ValidationError(_("You must type the same password each time.")) 68 | return password_new 69 | 70 | @staticmethod 71 | def account_delete_mark(deletion): 72 | deletion.user.is_active = False 73 | deletion.user.save() 74 | 75 | @staticmethod 76 | def account_delete_expunge(deletion): 77 | deletion.user.delete() 78 | 79 | 80 | class HookProxy: 81 | 82 | def __getattr__(self, attr): 83 | return getattr(settings.ACCOUNT_HOOKSET, attr) 84 | 85 | 86 | hookset = HookProxy() 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | BI indicates a backward incompatible change. Take caution when upgrading to a 4 | version with these. Your code will need to be updated to continue working. 5 | 6 | ## 3.3.2 7 | 8 | * #375 - Include migration for `SignupCode.max_uses` (closes #374) 9 | * #376 - Static analysis fixups 10 | 11 | ## 3.3.1 12 | 13 | * #373 - Re-include migrations in distribution 14 | 15 | ## 3.3.0 16 | 17 | * #370 Drop Django 2.2, fix timezone-aware comparison, packaging tweaks 18 | 19 | ## 3.2.1 20 | 21 | * #364 - Performance fix to admin classes 22 | 23 | ## 3.2.0 24 | 25 | * #363 - Django 4.0 compat: `ugettext_lazy` -> `gettext_lazy` 26 | 27 | ## 3.1.0 28 | 29 | * #205 - Bug fix on checking email against email not signup code 30 | * #225 - Fix case sensitivity mismatch on email addresses 31 | * #233 - Fix link to languages in docs 32 | * #247 - Update Spanish translations 33 | * #273 - Update German translations 34 | * #135 - Update Russian translations 35 | * #242 - Fix callbacks/hooks for account deletion 36 | * #251 (#249) - Allow overriding the password reset token url 37 | * #280 - Raise improper config error if signup view can't login 38 | * #348 (#337) - Make https the default protocol 39 | * #351 (#332) - Reduction in queries 40 | * #360 (#210) - Updates to docs 41 | * #361 (#141) - Added ability to override clean passwords 42 | * #362 - Updated CI to use Pinax Actions 43 | * Updates to packaging 44 | * Dropped Python 3.5 and Django 3.1 from test matrix 45 | * Added Python 3.10 to test matrix 46 | 47 | 48 | ## 3.0.3 49 | 50 | * Fix deprecated urls 51 | * Update template context processors docs 52 | * Fix deprecrated argument in signals 53 | * Update decorators for Django 3 54 | * Fix issue with lazy string 55 | * Drop deprecated `force_text()` 56 | 57 | ## 3.0.2 58 | 59 | * Drop Django 2.0 and Python 2,7, 3.4, and 3.5 support 60 | * Add Django 2.1, 2.2 and 3.0, and Python 3.7 and 3.8 support 61 | * Update packaging configs 62 | 63 | ## 2.0.3 64 | 65 | * fixed breaking change in 2.0.2 where context did not have uidb36 and token 66 | * improved documentation 67 | 68 | ## 2.0.2 69 | 70 | * fixed potentional security issue with leaking password reset tokens through HTTP Referer header 71 | * added `never_cache`, `csrf_protect` and `sensitive_post_parameters` to appropriate views 72 | 73 | ## 2.0.1 74 | 75 | @@@ todo 76 | 77 | ## 2.0.0 78 | 79 | * BI: moved account deletion callbacks to hooksets 80 | * BI: dropped Django 1.7 support 81 | * BI: dropped Python 3.2 support 82 | * BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value 83 | * added Django 1.10 support 84 | * added Turkish translations 85 | * fixed migration with language codes to dynamically set 86 | * added password expiration 87 | * added password stripping by default 88 | * added `ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN` feature (default is `False`) 89 | 90 | ## 1.3.0 91 | 92 | * added Python 3.5 and Django 1.9 compatibility 93 | * added Japanese translations 94 | * added model kwarg to SignupView.create_user enabling sign up for complex user hierarchies 95 | 96 | ## 1.2.0 97 | 98 | * added {% urlnext %} template tag 99 | 100 | ## 1.1.0 101 | 102 | * added Django 1.8 support 103 | * dropped Django 1.4, 1.6 and Python 2.6 support 104 | * improved test coverage 105 | * fixed edge case bugs in sign up codes 106 | * added Django migrations 107 | * added email notification on password change 108 | -------------------------------------------------------------------------------- /account/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate 2 | from django.contrib.auth.models import User 3 | from django.test import TestCase, override_settings 4 | 5 | 6 | @override_settings( 7 | AUTHENTICATION_BACKENDS=[ 8 | "account.auth_backends.UsernameAuthenticationBackend" 9 | ] 10 | ) 11 | class UsernameAuthenticationBackendTestCase(TestCase): 12 | 13 | def create_user(self, username, email, password): 14 | user = User.objects.create_user(username, email=email, password=password) 15 | return user 16 | 17 | def test_successful_auth(self): 18 | created_user = self.create_user("user1", "user1@example.com", "password") 19 | authed_user = authenticate(username="user1", password="password") 20 | self.assertTrue(authed_user is not None) 21 | self.assertEqual(created_user.pk, authed_user.pk) 22 | 23 | def test_unsuccessful_auth(self): 24 | authed_user = authenticate(username="user-does-not-exist", password="password") 25 | self.assertTrue(authed_user is None) 26 | 27 | def test_missing_credentials(self): 28 | self.create_user("user1", "user1@example.com", "password") 29 | self.assertTrue(authenticate() is None) 30 | self.assertTrue(authenticate(username="user1") is None) 31 | 32 | def test_successful_auth_django_2_1(self): 33 | created_user = self.create_user("user1", "user1@example.com", "password") 34 | request = None 35 | authed_user = authenticate(request, username="user1", password="password") 36 | self.assertTrue(authed_user is not None) 37 | self.assertEqual(created_user.pk, authed_user.pk) 38 | 39 | def test_unsuccessful_auth_django_2_1(self): 40 | request = None 41 | authed_user = authenticate(request, username="user-does-not-exist", password="password") 42 | self.assertTrue(authed_user is None) 43 | 44 | 45 | @override_settings( 46 | AUTHENTICATION_BACKENDS=[ 47 | "account.auth_backends.EmailAuthenticationBackend" 48 | ] 49 | ) 50 | class EmailAuthenticationBackendTestCase(TestCase): 51 | 52 | def create_user(self, username, email, password): 53 | user = User.objects.create_user(username, email=email, password=password) 54 | return user 55 | 56 | def test_successful_auth(self): 57 | created_user = self.create_user("user1", "user1@example.com", "password") 58 | authed_user = authenticate(username="user1@example.com", password="password") 59 | self.assertTrue(authed_user is not None) 60 | self.assertEqual(created_user.pk, authed_user.pk) 61 | 62 | def test_unsuccessful_auth(self): 63 | authed_user = authenticate(username="user-does-not-exist", password="password") 64 | self.assertTrue(authed_user is None) 65 | 66 | def test_missing_credentials(self): 67 | self.create_user("user1", "user1@example.com", "password") 68 | self.assertTrue(authenticate() is None) 69 | self.assertTrue(authenticate(username="user1@example.com") is None) 70 | 71 | def test_successful_auth_django_2_1(self): 72 | created_user = self.create_user("user1", "user1@example.com", "password") 73 | request = None 74 | authed_user = authenticate(request, username="user1@example.com", password="password") 75 | self.assertTrue(authed_user is not None) 76 | self.assertEqual(created_user.pk, authed_user.pk) 77 | 78 | def test_unsuccessful_auth_django_2_1(self): 79 | request = None 80 | authed_user = authenticate(request, username="user-does-not-exist", password="password") 81 | self.assertTrue(authed_user is None) 82 | -------------------------------------------------------------------------------- /account/locale/fa_IR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-user-accounts\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 11 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 12 | "Last-Translator: Brian Rosner \n" 13 | "Language-Team: Persian (Iran) (http://www.transifex.com/projects/p/django-user-accounts/language/fa_IR/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: fa_IR\n" 18 | "Plural-Forms: nplurals=1; plural=0;\n" 19 | 20 | #: forms.py:27 forms.py:107 21 | msgid "Username" 22 | msgstr "" 23 | 24 | #: forms.py:33 forms.py:79 25 | msgid "Password" 26 | msgstr "" 27 | 28 | #: forms.py:37 29 | msgid "Password (again)" 30 | msgstr "" 31 | 32 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 33 | msgid "Email" 34 | msgstr "" 35 | 36 | #: forms.py:52 37 | msgid "Usernames can only contain letters, numbers and underscores." 38 | msgstr "" 39 | 40 | #: forms.py:60 41 | msgid "This username is already taken. Please choose another." 42 | msgstr "" 43 | 44 | #: forms.py:67 forms.py:217 45 | msgid "A user is registered with this email address." 46 | msgstr "" 47 | 48 | #: forms.py:72 forms.py:162 forms.py:191 49 | msgid "You must type the same password each time." 50 | msgstr "" 51 | 52 | #: forms.py:83 53 | msgid "Remember Me" 54 | msgstr "" 55 | 56 | #: forms.py:96 57 | msgid "This account is inactive." 58 | msgstr "" 59 | 60 | #: forms.py:108 61 | msgid "The username and/or password you specified are not correct." 62 | msgstr "" 63 | 64 | #: forms.py:123 65 | msgid "The email address and/or password you specified are not correct." 66 | msgstr "" 67 | 68 | #: forms.py:138 69 | msgid "Current Password" 70 | msgstr "" 71 | 72 | #: forms.py:142 forms.py:180 73 | msgid "New Password" 74 | msgstr "" 75 | 76 | #: forms.py:146 forms.py:184 77 | msgid "New Password (again)" 78 | msgstr "" 79 | 80 | #: forms.py:156 81 | msgid "Please type your current password." 82 | msgstr "" 83 | 84 | #: forms.py:173 85 | msgid "Email address can not be found." 86 | msgstr "" 87 | 88 | #: forms.py:199 89 | msgid "Timezone" 90 | msgstr "" 91 | 92 | #: forms.py:205 93 | msgid "Language" 94 | msgstr "" 95 | 96 | #: models.py:34 97 | msgid "user" 98 | msgstr "" 99 | 100 | #: models.py:35 101 | msgid "timezone" 102 | msgstr "" 103 | 104 | #: models.py:37 105 | msgid "language" 106 | msgstr "" 107 | 108 | #: models.py:250 109 | msgid "email address" 110 | msgstr "" 111 | 112 | #: models.py:251 113 | msgid "email addresses" 114 | msgstr "" 115 | 116 | #: models.py:300 117 | msgid "email confirmation" 118 | msgstr "" 119 | 120 | #: models.py:301 121 | msgid "email confirmations" 122 | msgstr "" 123 | 124 | #: views.py:42 125 | #, python-brace-format 126 | msgid "Confirmation email sent to {email}." 127 | msgstr "" 128 | 129 | #: views.py:46 130 | #, python-brace-format 131 | msgid "The code {code} is invalid." 132 | msgstr "" 133 | 134 | #: views.py:379 135 | #, python-brace-format 136 | msgid "You have confirmed {email}." 137 | msgstr "" 138 | 139 | #: views.py:452 views.py:585 140 | msgid "Password successfully changed." 141 | msgstr "" 142 | 143 | #: views.py:664 144 | msgid "Account settings updated." 145 | msgstr "" 146 | 147 | #: views.py:748 148 | #, python-brace-format 149 | msgid "" 150 | "Your account is now inactive and your data will be expunged in the next " 151 | "{expunge_hours} hours." 152 | msgstr "" 153 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | Install the development version:: 8 | 9 | pip install django-user-accounts 10 | 11 | Make sure that ``django.contrib.sites`` is in ``INSTALLED_APPS`` and add 12 | ``account`` to this setting:::: 13 | 14 | INSTALLED_APPS = ( 15 | "django.contrib.sites", 16 | # ... 17 | "account", 18 | # ... 19 | ) 20 | 21 | See the list of :ref:`settings` to modify the default behavior of 22 | django-user-accounts and make adjustments for your website. 23 | 24 | Add ``account.urls`` to your URLs definition:: 25 | 26 | urlpatterns = patterns("", 27 | ... 28 | url(r"^account/", include("account.urls")), 29 | ... 30 | ) 31 | 32 | Add ``account.context_processors.account`` to ``context_processors``:: 33 | 34 | TEMPLATES = [ 35 | { 36 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 37 | 'DIRS': [ ], 38 | 'APP_DIRS': True, 39 | 'OPTIONS': { 40 | 'context_processors': [ 41 | 'django.template.context_processors.debug', 42 | 'django.template.context_processors.request', 43 | 'django.contrib.auth.context_processors.auth', 44 | 'django.contrib.messages.context_processors.messages', 45 | 46 | # add django-user-accounts context processor 47 | 'account.context_processors.account', 48 | ], 49 | }, 50 | }, 51 | ] 52 | 53 | Add ``account.middleware.LocaleMiddleware`` and 54 | ``account.middleware.TimezoneMiddleware`` to ``MIDDLEWARE_CLASSES``:: 55 | 56 | MIDDLEWARE_CLASSES = [ 57 | ... 58 | "account.middleware.LocaleMiddleware", 59 | "account.middleware.TimezoneMiddleware", 60 | ... 61 | ] 62 | 63 | Optionally include ``account.middleware.ExpiredPasswordMiddleware`` in 64 | ``MIDDLEWARE_CLASSES`` if you need password expiration support:: 65 | 66 | MIDDLEWARE_CLASSES = [ 67 | ... 68 | "account.middleware.ExpiredPasswordMiddleware", 69 | ... 70 | ] 71 | 72 | Set the authentication backends to the following:: 73 | 74 | AUTHENTICATION_BACKENDS = [ 75 | 'account.auth_backends.AccountModelBackend', 76 | 'django.contrib.auth.backends.ModelBackend' 77 | ] 78 | 79 | Once everything is in place make sure you run ``migrate`` to modify the 80 | database with the ``account`` app models. 81 | 82 | .. _dependencies: 83 | 84 | Dependencies 85 | ============ 86 | 87 | ``django.contrib.auth`` 88 | ----------------------- 89 | 90 | This is bundled with Django. It is enabled by default with all new Django 91 | projects, but if you adding django-user-accounts to an existing project you 92 | need to make sure ``django.contrib.auth`` is installed. 93 | 94 | ``django.contrib.sites`` 95 | ------------------------ 96 | 97 | This is bundled with Django. It is enabled by default with all new Django 98 | projects. It is used to provide links back to the site in emails or various 99 | places in templates that need an absolute URL. 100 | 101 | django-appconf_ 102 | --------------- 103 | 104 | We use django-appconf for app settings. It is listed in ``install_requires`` 105 | and will be installed when pip installs. 106 | 107 | .. _django-appconf: https://github.com/jezdez/django-appconf 108 | 109 | pytz_ 110 | ----- 111 | 112 | pytz is used for handling timezones for accounts. This dependency is critical 113 | due to its extensive dataset for timezones. 114 | 115 | .. _pytz: http://pypi.python.org/pypi/pytz/ 116 | -------------------------------------------------------------------------------- /account/locale/sk_SK/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-user-accounts\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 11 | "PO-Revision-Date: 2012-04-30 18:54+0000\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Slovak (Slovakia) (http://www.transifex.com/projects/p/django-user-accounts/language/sk_SK/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: sk_SK\n" 18 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" 19 | 20 | #: forms.py:27 forms.py:107 21 | msgid "Username" 22 | msgstr "" 23 | 24 | #: forms.py:33 forms.py:79 25 | msgid "Password" 26 | msgstr "" 27 | 28 | #: forms.py:37 29 | msgid "Password (again)" 30 | msgstr "" 31 | 32 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 33 | msgid "Email" 34 | msgstr "" 35 | 36 | #: forms.py:52 37 | msgid "Usernames can only contain letters, numbers and underscores." 38 | msgstr "" 39 | 40 | #: forms.py:60 41 | msgid "This username is already taken. Please choose another." 42 | msgstr "" 43 | 44 | #: forms.py:67 forms.py:217 45 | msgid "A user is registered with this email address." 46 | msgstr "" 47 | 48 | #: forms.py:72 forms.py:162 forms.py:191 49 | msgid "You must type the same password each time." 50 | msgstr "" 51 | 52 | #: forms.py:83 53 | msgid "Remember Me" 54 | msgstr "" 55 | 56 | #: forms.py:96 57 | msgid "This account is inactive." 58 | msgstr "" 59 | 60 | #: forms.py:108 61 | msgid "The username and/or password you specified are not correct." 62 | msgstr "" 63 | 64 | #: forms.py:123 65 | msgid "The email address and/or password you specified are not correct." 66 | msgstr "" 67 | 68 | #: forms.py:138 69 | msgid "Current Password" 70 | msgstr "" 71 | 72 | #: forms.py:142 forms.py:180 73 | msgid "New Password" 74 | msgstr "" 75 | 76 | #: forms.py:146 forms.py:184 77 | msgid "New Password (again)" 78 | msgstr "" 79 | 80 | #: forms.py:156 81 | msgid "Please type your current password." 82 | msgstr "" 83 | 84 | #: forms.py:173 85 | msgid "Email address can not be found." 86 | msgstr "" 87 | 88 | #: forms.py:199 89 | msgid "Timezone" 90 | msgstr "" 91 | 92 | #: forms.py:205 93 | msgid "Language" 94 | msgstr "" 95 | 96 | #: models.py:34 97 | msgid "user" 98 | msgstr "" 99 | 100 | #: models.py:35 101 | msgid "timezone" 102 | msgstr "" 103 | 104 | #: models.py:37 105 | msgid "language" 106 | msgstr "" 107 | 108 | #: models.py:250 109 | msgid "email address" 110 | msgstr "" 111 | 112 | #: models.py:251 113 | msgid "email addresses" 114 | msgstr "" 115 | 116 | #: models.py:300 117 | msgid "email confirmation" 118 | msgstr "" 119 | 120 | #: models.py:301 121 | msgid "email confirmations" 122 | msgstr "" 123 | 124 | #: views.py:42 125 | #, python-brace-format 126 | msgid "Confirmation email sent to {email}." 127 | msgstr "" 128 | 129 | #: views.py:46 130 | #, python-brace-format 131 | msgid "The code {code} is invalid." 132 | msgstr "" 133 | 134 | #: views.py:379 135 | #, python-brace-format 136 | msgid "You have confirmed {email}." 137 | msgstr "" 138 | 139 | #: views.py:452 views.py:585 140 | msgid "Password successfully changed." 141 | msgstr "" 142 | 143 | #: views.py:664 144 | msgid "Account settings updated." 145 | msgstr "" 146 | 147 | #: views.py:748 148 | #, python-brace-format 149 | msgid "" 150 | "Your account is now inactive and your data will be expunged in the next " 151 | "{expunge_hours} hours." 152 | msgstr "" 153 | -------------------------------------------------------------------------------- /account/templatetags/account_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.base import kwarg_re 3 | from django.template.defaulttags import URLNode 4 | from django.utils.html import conditional_escape 5 | from django.utils.http import urlencode 6 | 7 | from account.utils import user_display 8 | 9 | register = template.Library() 10 | 11 | 12 | class UserDisplayNode(template.Node): 13 | 14 | def __init__(self, user, as_var=None): 15 | self.user_var = template.Variable(user) 16 | self.as_var = as_var 17 | 18 | def render(self, context): 19 | user = self.user_var.resolve(context) 20 | display = user_display(user) 21 | if self.as_var: 22 | context[self.as_var] = display 23 | return "" 24 | return conditional_escape(display) 25 | 26 | 27 | @register.tag(name="user_display") 28 | def do_user_display(parser, token): # skipcq: PYL-W0613 29 | """ 30 | Example usage:: 31 | 32 | {% user_display user %} 33 | 34 | or if you need to use in a {% blocktrans %}:: 35 | 36 | {% user_display user as user_display} 37 | {% blocktrans %}{{ user_display }} has sent you a gift.{% endblocktrans %} 38 | 39 | """ 40 | bits = token.split_contents() 41 | if len(bits) == 2: 42 | user = bits[1] 43 | as_var = None 44 | elif len(bits) == 4: 45 | user = bits[1] 46 | as_var = bits[3] 47 | else: 48 | raise template.TemplateSyntaxError("'{0}' takes either two or four arguments".format(bits[0])) 49 | return UserDisplayNode(user, as_var) 50 | 51 | 52 | class URLNextNode(URLNode): 53 | 54 | @staticmethod 55 | def add_next(url, context): 56 | """ 57 | With both `redirect_field_name` and `redirect_field_value` available in 58 | the context, add on a querystring to handle "next" redirecting. 59 | """ 60 | if all( 61 | key in context for key in ["redirect_field_name", "redirect_field_value"] 62 | ) and context["redirect_field_value"]: 63 | url += "?" + urlencode({ 64 | context["redirect_field_name"]: context["redirect_field_value"], 65 | }) 66 | return url 67 | 68 | def render(self, context): 69 | url = super(URLNextNode, self).render(context) 70 | if self.asvar: 71 | url = context[self.asvar] 72 | # add on next handling 73 | url = self.add_next(url, context) 74 | if self.asvar: 75 | context[self.asvar] = url 76 | return "" 77 | return url 78 | 79 | 80 | @register.tag 81 | def urlnext(parser, token): 82 | """ 83 | {% url %} copied from Django 1.7. 84 | """ 85 | bits = token.split_contents() 86 | if len(bits) < 2: 87 | raise template.TemplateSyntaxError( 88 | "'%s' takes at least one argument" 89 | " (path to a view)" % bits[0] 90 | ) 91 | viewname = parser.compile_filter(bits[1]) 92 | args = [] 93 | kwargs = {} 94 | asvar = None 95 | bits = bits[2:] 96 | if len(bits) >= 2 and bits[-2] == "as": 97 | asvar = bits[-1] 98 | bits = bits[:-2] 99 | 100 | if len(bits) > 0: 101 | for bit in bits: 102 | match = kwarg_re.match(bit) 103 | if not match: 104 | raise template.TemplateSyntaxError("Malformed arguments to url tag") 105 | name, value = match.groups() 106 | if name: 107 | kwargs[name] = parser.compile_filter(value) 108 | else: 109 | args.append(parser.compile_filter(value)) 110 | 111 | return URLNextNode(viewname, args, kwargs, asvar) 112 | -------------------------------------------------------------------------------- /account/locale/ar/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-user-accounts\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 11 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 12 | "Last-Translator: Brian Rosner \n" 13 | "Language-Team: Arabic (http://www.transifex.com/projects/p/django-user-accounts/language/ar/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: ar\n" 18 | "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" 19 | 20 | #: forms.py:27 forms.py:107 21 | msgid "Username" 22 | msgstr "" 23 | 24 | #: forms.py:33 forms.py:79 25 | msgid "Password" 26 | msgstr "" 27 | 28 | #: forms.py:37 29 | msgid "Password (again)" 30 | msgstr "" 31 | 32 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 33 | msgid "Email" 34 | msgstr "" 35 | 36 | #: forms.py:52 37 | msgid "Usernames can only contain letters, numbers and underscores." 38 | msgstr "" 39 | 40 | #: forms.py:60 41 | msgid "This username is already taken. Please choose another." 42 | msgstr "" 43 | 44 | #: forms.py:67 forms.py:217 45 | msgid "A user is registered with this email address." 46 | msgstr "" 47 | 48 | #: forms.py:72 forms.py:162 forms.py:191 49 | msgid "You must type the same password each time." 50 | msgstr "" 51 | 52 | #: forms.py:83 53 | msgid "Remember Me" 54 | msgstr "" 55 | 56 | #: forms.py:96 57 | msgid "This account is inactive." 58 | msgstr "" 59 | 60 | #: forms.py:108 61 | msgid "The username and/or password you specified are not correct." 62 | msgstr "" 63 | 64 | #: forms.py:123 65 | msgid "The email address and/or password you specified are not correct." 66 | msgstr "" 67 | 68 | #: forms.py:138 69 | msgid "Current Password" 70 | msgstr "" 71 | 72 | #: forms.py:142 forms.py:180 73 | msgid "New Password" 74 | msgstr "" 75 | 76 | #: forms.py:146 forms.py:184 77 | msgid "New Password (again)" 78 | msgstr "" 79 | 80 | #: forms.py:156 81 | msgid "Please type your current password." 82 | msgstr "" 83 | 84 | #: forms.py:173 85 | msgid "Email address can not be found." 86 | msgstr "" 87 | 88 | #: forms.py:199 89 | msgid "Timezone" 90 | msgstr "" 91 | 92 | #: forms.py:205 93 | msgid "Language" 94 | msgstr "" 95 | 96 | #: models.py:34 97 | msgid "user" 98 | msgstr "" 99 | 100 | #: models.py:35 101 | msgid "timezone" 102 | msgstr "" 103 | 104 | #: models.py:37 105 | msgid "language" 106 | msgstr "" 107 | 108 | #: models.py:250 109 | msgid "email address" 110 | msgstr "" 111 | 112 | #: models.py:251 113 | msgid "email addresses" 114 | msgstr "" 115 | 116 | #: models.py:300 117 | msgid "email confirmation" 118 | msgstr "" 119 | 120 | #: models.py:301 121 | msgid "email confirmations" 122 | msgstr "" 123 | 124 | #: views.py:42 125 | #, python-brace-format 126 | msgid "Confirmation email sent to {email}." 127 | msgstr "" 128 | 129 | #: views.py:46 130 | #, python-brace-format 131 | msgid "The code {code} is invalid." 132 | msgstr "" 133 | 134 | #: views.py:379 135 | #, python-brace-format 136 | msgid "You have confirmed {email}." 137 | msgstr "" 138 | 139 | #: views.py:452 views.py:585 140 | msgid "Password successfully changed." 141 | msgstr "" 142 | 143 | #: views.py:664 144 | msgid "Account settings updated." 145 | msgstr "" 146 | 147 | #: views.py:748 148 | #, python-brace-format 149 | msgid "" 150 | "Your account is now inactive and your data will be expunged in the next " 151 | "{expunge_hours} hours." 152 | msgstr "" 153 | -------------------------------------------------------------------------------- /account/middleware.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse, urlunparse 2 | 3 | from django.contrib import messages 4 | from django.contrib.auth import REDIRECT_FIELD_NAME 5 | from django.http import HttpResponseRedirect, QueryDict 6 | from django.urls import resolve, reverse 7 | from django.utils import timezone, translation 8 | from django.utils.cache import patch_vary_headers 9 | from django.utils.deprecation import MiddlewareMixin as BaseMiddleware 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from account import signals 13 | from account.conf import settings 14 | from account.models import Account 15 | from account.utils import check_password_expired 16 | 17 | 18 | class LocaleMiddleware(BaseMiddleware): 19 | """ 20 | This is a very simple middleware that parses a request 21 | and decides what translation object to install in the current 22 | thread context depending on the user's account. This allows pages 23 | to be dynamically translated to the language the user desires 24 | (if the language is available, of course). 25 | """ 26 | 27 | @staticmethod 28 | def get_language_for_user(request): 29 | if request.user.is_authenticated: 30 | try: 31 | account = Account.objects.get(user=request.user) 32 | return account.language 33 | except Account.DoesNotExist: 34 | pass 35 | return translation.get_language_from_request(request) 36 | 37 | def process_request(self, request): 38 | translation.activate(self.get_language_for_user(request)) 39 | request.LANGUAGE_CODE = translation.get_language() 40 | 41 | @staticmethod 42 | def process_response(request, response): 43 | patch_vary_headers(response, ("Accept-Language",)) 44 | response["Content-Language"] = translation.get_language() 45 | translation.deactivate() 46 | return response 47 | 48 | 49 | class TimezoneMiddleware(BaseMiddleware): 50 | """ 51 | This middleware sets the timezone used to display dates in 52 | templates to the user's timezone. 53 | """ 54 | 55 | @staticmethod 56 | def process_request(request): 57 | try: 58 | account = getattr(request.user, "account", None) 59 | except Account.DoesNotExist: 60 | pass 61 | else: 62 | if account: 63 | tz = settings.TIME_ZONE if not account.timezone else account.timezone 64 | timezone.activate(tz) 65 | 66 | 67 | class ExpiredPasswordMiddleware(BaseMiddleware): 68 | 69 | def process_request(self, request): 70 | if request.user.is_authenticated and not request.user.is_staff: 71 | next_url = resolve(request.path).url_name 72 | # Authenticated users must be allowed to access 73 | # "change password" page and "log out" page. 74 | # even if password is expired. 75 | if next_url not in [ 76 | settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL, 77 | settings.ACCOUNT_LOGOUT_URL, 78 | ] and check_password_expired(request.user): 79 | signals.password_expired.send(sender=self, user=request.user) 80 | messages.add_message( 81 | request, 82 | messages.WARNING, 83 | _("Your password has expired. Please save a new password.") 84 | ) 85 | redirect_field_name = REDIRECT_FIELD_NAME 86 | 87 | change_password_url = reverse(settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL) 88 | url_bits = list(urlparse(change_password_url)) 89 | querystring = QueryDict(url_bits[4], mutable=True) 90 | querystring[redirect_field_name] = next_url 91 | url_bits[4] = querystring.urlencode(safe="/") 92 | 93 | return HttpResponseRedirect(urlunparse(url_bits)) 94 | -------------------------------------------------------------------------------- /account/locale/zh_TW/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # lueo , 2012 7 | # lueo , 2012 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: django-user-accounts\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 13 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 14 | "Last-Translator: Brian Rosner \n" 15 | "Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/django-user-accounts/language/zh_TW/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: zh_TW\n" 20 | "Plural-Forms: nplurals=1; plural=0;\n" 21 | 22 | #: forms.py:27 forms.py:107 23 | msgid "Username" 24 | msgstr "帳號" 25 | 26 | #: forms.py:33 forms.py:79 27 | msgid "Password" 28 | msgstr "密碼" 29 | 30 | #: forms.py:37 31 | msgid "Password (again)" 32 | msgstr "密碼 (確認)" 33 | 34 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 35 | msgid "Email" 36 | msgstr "Email" 37 | 38 | #: forms.py:52 39 | msgid "Usernames can only contain letters, numbers and underscores." 40 | msgstr "帳號只能使用英文字母,數字及底線。" 41 | 42 | #: forms.py:60 43 | msgid "This username is already taken. Please choose another." 44 | msgstr "該帳號已被使用,請再選擇一個。" 45 | 46 | #: forms.py:67 forms.py:217 47 | msgid "A user is registered with this email address." 48 | msgstr "該email已被其它使用者註冊。" 49 | 50 | #: forms.py:72 forms.py:162 forms.py:191 51 | msgid "You must type the same password each time." 52 | msgstr "您必須輸入同樣的密碼。" 53 | 54 | #: forms.py:83 55 | msgid "Remember Me" 56 | msgstr "記住我" 57 | 58 | #: forms.py:96 59 | msgid "This account is inactive." 60 | msgstr "該帳號目前停用。" 61 | 62 | #: forms.py:108 63 | msgid "The username and/or password you specified are not correct." 64 | msgstr "您提供的帳號或密碼不正確。" 65 | 66 | #: forms.py:123 67 | msgid "The email address and/or password you specified are not correct." 68 | msgstr "您提供的email或密碼不正確。" 69 | 70 | #: forms.py:138 71 | msgid "Current Password" 72 | msgstr "原密碼" 73 | 74 | #: forms.py:142 forms.py:180 75 | msgid "New Password" 76 | msgstr "新密碼" 77 | 78 | #: forms.py:146 forms.py:184 79 | msgid "New Password (again)" 80 | msgstr "新密碼 (再次輸入)" 81 | 82 | #: forms.py:156 83 | msgid "Please type your current password." 84 | msgstr "請輸入您的原密碼。" 85 | 86 | #: forms.py:173 87 | msgid "Email address can not be found." 88 | msgstr "" 89 | 90 | #: forms.py:199 91 | msgid "Timezone" 92 | msgstr "時區" 93 | 94 | #: forms.py:205 95 | msgid "Language" 96 | msgstr "語言" 97 | 98 | #: models.py:34 99 | msgid "user" 100 | msgstr "使用者" 101 | 102 | #: models.py:35 103 | msgid "timezone" 104 | msgstr "時區" 105 | 106 | #: models.py:37 107 | msgid "language" 108 | msgstr "語言" 109 | 110 | #: models.py:250 111 | msgid "email address" 112 | msgstr "email地址" 113 | 114 | #: models.py:251 115 | msgid "email addresses" 116 | msgstr "email地址" 117 | 118 | #: models.py:300 119 | msgid "email confirmation" 120 | msgstr "email確認" 121 | 122 | #: models.py:301 123 | msgid "email confirmations" 124 | msgstr "email確認" 125 | 126 | #: views.py:42 127 | #, python-brace-format 128 | msgid "Confirmation email sent to {email}." 129 | msgstr "" 130 | 131 | #: views.py:46 132 | #, python-brace-format 133 | msgid "The code {code} is invalid." 134 | msgstr "" 135 | 136 | #: views.py:379 137 | #, python-brace-format 138 | msgid "You have confirmed {email}." 139 | msgstr "" 140 | 141 | #: views.py:452 views.py:585 142 | msgid "Password successfully changed." 143 | msgstr "密碼已變更。" 144 | 145 | #: views.py:664 146 | msgid "Account settings updated." 147 | msgstr "帳號設定已更新。" 148 | 149 | #: views.py:748 150 | #, python-brace-format 151 | msgid "" 152 | "Your account is now inactive and your data will be expunged in the next " 153 | "{expunge_hours} hours." 154 | msgstr "" 155 | -------------------------------------------------------------------------------- /account/locale/zh_CN/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # jeff cai, 2014 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-user-accounts\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 12 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 13 | "Last-Translator: Brian Rosner \n" 14 | "Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-user-accounts/language/zh_CN/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: zh_CN\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: forms.py:27 forms.py:107 22 | msgid "Username" 23 | msgstr "用户名" 24 | 25 | #: forms.py:33 forms.py:79 26 | msgid "Password" 27 | msgstr "密码" 28 | 29 | #: forms.py:37 30 | msgid "Password (again)" 31 | msgstr "密码(确认)" 32 | 33 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 34 | msgid "Email" 35 | msgstr "电子邮件" 36 | 37 | #: forms.py:52 38 | msgid "Usernames can only contain letters, numbers and underscores." 39 | msgstr "用户名只能使用英文字母,数字和下划线。" 40 | 41 | #: forms.py:60 42 | msgid "This username is already taken. Please choose another." 43 | msgstr "该用户名已被使用,请选择其他的用户名。" 44 | 45 | #: forms.py:67 forms.py:217 46 | msgid "A user is registered with this email address." 47 | msgstr "该邮件地址已经被占用。" 48 | 49 | #: forms.py:72 forms.py:162 forms.py:191 50 | msgid "You must type the same password each time." 51 | msgstr "您必須輸入同樣的密碼。" 52 | 53 | #: forms.py:83 54 | msgid "Remember Me" 55 | msgstr "记住我" 56 | 57 | #: forms.py:96 58 | msgid "This account is inactive." 59 | msgstr "该帐号目前停用。" 60 | 61 | #: forms.py:108 62 | msgid "The username and/or password you specified are not correct." 63 | msgstr "您提供的用户名或密码不正确。" 64 | 65 | #: forms.py:123 66 | msgid "The email address and/or password you specified are not correct." 67 | msgstr "您提供的电子邮件或密码不正确。" 68 | 69 | #: forms.py:138 70 | msgid "Current Password" 71 | msgstr "当前密码" 72 | 73 | #: forms.py:142 forms.py:180 74 | msgid "New Password" 75 | msgstr "新密码" 76 | 77 | #: forms.py:146 forms.py:184 78 | msgid "New Password (again)" 79 | msgstr "新密码 (再次输入)" 80 | 81 | #: forms.py:156 82 | msgid "Please type your current password." 83 | msgstr "请输入您当前的密码。" 84 | 85 | #: forms.py:173 86 | msgid "Email address can not be found." 87 | msgstr "不能找到电子邮件地址。" 88 | 89 | #: forms.py:199 90 | msgid "Timezone" 91 | msgstr "时区" 92 | 93 | #: forms.py:205 94 | msgid "Language" 95 | msgstr "语言" 96 | 97 | #: models.py:34 98 | msgid "user" 99 | msgstr "用户" 100 | 101 | #: models.py:35 102 | msgid "timezone" 103 | msgstr "时区" 104 | 105 | #: models.py:37 106 | msgid "language" 107 | msgstr "语言" 108 | 109 | #: models.py:250 110 | msgid "email address" 111 | msgstr "邮件地址" 112 | 113 | #: models.py:251 114 | msgid "email addresses" 115 | msgstr "邮件地址" 116 | 117 | #: models.py:300 118 | msgid "email confirmation" 119 | msgstr "邮件确认" 120 | 121 | #: models.py:301 122 | msgid "email confirmations" 123 | msgstr "邮件确认" 124 | 125 | #: views.py:42 126 | #, python-brace-format 127 | msgid "Confirmation email sent to {email}." 128 | msgstr "确认邮件已经发送到{email}。" 129 | 130 | #: views.py:46 131 | #, python-brace-format 132 | msgid "The code {code} is invalid." 133 | msgstr "编码{code}不正确" 134 | 135 | #: views.py:379 136 | #, python-brace-format 137 | msgid "You have confirmed {email}." 138 | msgstr "您已经确认了{email}" 139 | 140 | #: views.py:452 views.py:585 141 | msgid "Password successfully changed." 142 | msgstr "密码修改成功。" 143 | 144 | #: views.py:664 145 | msgid "Account settings updated." 146 | msgstr "帐号设置已经修改。" 147 | 148 | #: views.py:748 149 | #, python-brace-format 150 | msgid "" 151 | "Your account is now inactive and your data will be expunged in the next " 152 | "{expunge_hours} hours." 153 | msgstr "您的帐号已经失效并且您的数据会在{expunge_hours}小时后清除。" 154 | -------------------------------------------------------------------------------- /account/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-user-accounts\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 11 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 12 | "Last-Translator: Brian Rosner \n" 13 | "Language-Team: Italian (http://www.transifex.com/projects/p/django-user-accounts/language/it/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: it\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: forms.py:27 forms.py:107 21 | msgid "Username" 22 | msgstr "Nome utente" 23 | 24 | #: forms.py:33 forms.py:79 25 | msgid "Password" 26 | msgstr "Password" 27 | 28 | #: forms.py:37 29 | msgid "Password (again)" 30 | msgstr "Password (di nuovo)" 31 | 32 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 33 | msgid "Email" 34 | msgstr "Email" 35 | 36 | #: forms.py:52 37 | msgid "Usernames can only contain letters, numbers and underscores." 38 | msgstr "Il nome utente può contenere solo lettere, numeri e trattini bassi." 39 | 40 | #: forms.py:60 41 | msgid "This username is already taken. Please choose another." 42 | msgstr "Questo nome utente è già utilizzato. Scegline un'altro." 43 | 44 | #: forms.py:67 forms.py:217 45 | msgid "A user is registered with this email address." 46 | msgstr "Esiste già un utente con questo indirizzo email." 47 | 48 | #: forms.py:72 forms.py:162 forms.py:191 49 | msgid "You must type the same password each time." 50 | msgstr "La password deve essere inserita due volte." 51 | 52 | #: forms.py:83 53 | msgid "Remember Me" 54 | msgstr "Ricordami" 55 | 56 | #: forms.py:96 57 | msgid "This account is inactive." 58 | msgstr "Questo account non è attivo." 59 | 60 | #: forms.py:108 61 | msgid "The username and/or password you specified are not correct." 62 | msgstr "Il nome utente e/o la password non sono corretti." 63 | 64 | #: forms.py:123 65 | msgid "The email address and/or password you specified are not correct." 66 | msgstr "L'indirizzo email e/o la password non sono corretti." 67 | 68 | #: forms.py:138 69 | msgid "Current Password" 70 | msgstr "Password Attuale" 71 | 72 | #: forms.py:142 forms.py:180 73 | msgid "New Password" 74 | msgstr "Nuova Password" 75 | 76 | #: forms.py:146 forms.py:184 77 | msgid "New Password (again)" 78 | msgstr "Nuova Password (di nuovo)" 79 | 80 | #: forms.py:156 81 | msgid "Please type your current password." 82 | msgstr "Inserisci la password attuale." 83 | 84 | #: forms.py:173 85 | msgid "Email address can not be found." 86 | msgstr "" 87 | 88 | #: forms.py:199 89 | msgid "Timezone" 90 | msgstr "Fuso orario" 91 | 92 | #: forms.py:205 93 | msgid "Language" 94 | msgstr "Lingua" 95 | 96 | #: models.py:34 97 | msgid "user" 98 | msgstr "utente" 99 | 100 | #: models.py:35 101 | msgid "timezone" 102 | msgstr "fuso orario" 103 | 104 | #: models.py:37 105 | msgid "language" 106 | msgstr "lingua" 107 | 108 | #: models.py:250 109 | msgid "email address" 110 | msgstr "indirizzo email" 111 | 112 | #: models.py:251 113 | msgid "email addresses" 114 | msgstr "indirizzi email" 115 | 116 | #: models.py:300 117 | msgid "email confirmation" 118 | msgstr "conferma indirizzo email" 119 | 120 | #: models.py:301 121 | msgid "email confirmations" 122 | msgstr "conferme indirizzi email" 123 | 124 | #: views.py:42 125 | #, python-brace-format 126 | msgid "Confirmation email sent to {email}." 127 | msgstr "" 128 | 129 | #: views.py:46 130 | #, python-brace-format 131 | msgid "The code {code} is invalid." 132 | msgstr "" 133 | 134 | #: views.py:379 135 | #, python-brace-format 136 | msgid "You have confirmed {email}." 137 | msgstr "" 138 | 139 | #: views.py:452 views.py:585 140 | msgid "Password successfully changed." 141 | msgstr "Password cambiata con successo." 142 | 143 | #: views.py:664 144 | msgid "Account settings updated." 145 | msgstr "Configurazione account aggiornata." 146 | 147 | #: views.py:748 148 | #, python-brace-format 149 | msgid "" 150 | "Your account is now inactive and your data will be expunged in the next " 151 | "{expunge_hours} hours." 152 | msgstr "" 153 | -------------------------------------------------------------------------------- /account/locale/nl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-user-accounts\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 11 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 12 | "Last-Translator: Brian Rosner \n" 13 | "Language-Team: Dutch (http://www.transifex.com/projects/p/django-user-accounts/language/nl/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: nl\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: forms.py:27 forms.py:107 21 | msgid "Username" 22 | msgstr "Gebruikersnaam" 23 | 24 | #: forms.py:33 forms.py:79 25 | msgid "Password" 26 | msgstr "Wachtwoord" 27 | 28 | #: forms.py:37 29 | msgid "Password (again)" 30 | msgstr "Wachtwoord (nogmaals)" 31 | 32 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 33 | msgid "Email" 34 | msgstr "Email" 35 | 36 | #: forms.py:52 37 | msgid "Usernames can only contain letters, numbers and underscores." 38 | msgstr "Gebruikersnamen kunnen alleen letters, nummers en liggende streepjes bevatten." 39 | 40 | #: forms.py:60 41 | msgid "This username is already taken. Please choose another." 42 | msgstr "De gebruikersnaam bestaat al. Kies een andere." 43 | 44 | #: forms.py:67 forms.py:217 45 | msgid "A user is registered with this email address." 46 | msgstr "Een gebruiker is geregistreerd met dit emailadres." 47 | 48 | #: forms.py:72 forms.py:162 forms.py:191 49 | msgid "You must type the same password each time." 50 | msgstr "Je moet iedere keer hetzelfde wachtwoord intypen." 51 | 52 | #: forms.py:83 53 | msgid "Remember Me" 54 | msgstr "Bewaar Mij" 55 | 56 | #: forms.py:96 57 | msgid "This account is inactive." 58 | msgstr "Dit account is niet actief." 59 | 60 | #: forms.py:108 61 | msgid "The username and/or password you specified are not correct." 62 | msgstr "De gebruikersnaam en/of wachtwoord die je hebt opgegeven zijn incorrect." 63 | 64 | #: forms.py:123 65 | msgid "The email address and/or password you specified are not correct." 66 | msgstr "Het emailadres en/of wachtwoord die je hebt opgegeven zijn incorrect." 67 | 68 | #: forms.py:138 69 | msgid "Current Password" 70 | msgstr "Huidige Wachtwoord" 71 | 72 | #: forms.py:142 forms.py:180 73 | msgid "New Password" 74 | msgstr "Nieuwe Wachtwoord" 75 | 76 | #: forms.py:146 forms.py:184 77 | msgid "New Password (again)" 78 | msgstr "Nieuwe Wachtwoord (nogmaals)" 79 | 80 | #: forms.py:156 81 | msgid "Please type your current password." 82 | msgstr "Type je huidige wachtwoord." 83 | 84 | #: forms.py:173 85 | msgid "Email address can not be found." 86 | msgstr "Email adres kan niet worden gevonden." 87 | 88 | #: forms.py:199 89 | msgid "Timezone" 90 | msgstr "Tijdzone" 91 | 92 | #: forms.py:205 93 | msgid "Language" 94 | msgstr "Taal" 95 | 96 | #: models.py:34 97 | msgid "user" 98 | msgstr "gebruiker" 99 | 100 | #: models.py:35 101 | msgid "timezone" 102 | msgstr "tijdzone" 103 | 104 | #: models.py:37 105 | msgid "language" 106 | msgstr "taal" 107 | 108 | #: models.py:250 109 | msgid "email address" 110 | msgstr "email adres" 111 | 112 | #: models.py:251 113 | msgid "email addresses" 114 | msgstr "email adressen" 115 | 116 | #: models.py:300 117 | msgid "email confirmation" 118 | msgstr "email bevestiging" 119 | 120 | #: models.py:301 121 | msgid "email confirmations" 122 | msgstr "email bevestigingen" 123 | 124 | #: views.py:42 125 | #, python-brace-format 126 | msgid "Confirmation email sent to {email}." 127 | msgstr "Bevestiging email verstuurd naar {email}." 128 | 129 | #: views.py:46 130 | #, python-brace-format 131 | msgid "The code {code} is invalid." 132 | msgstr "De code {code} is ongeldig." 133 | 134 | #: views.py:379 135 | #, python-brace-format 136 | msgid "You have confirmed {email}." 137 | msgstr "Je hebt {email} bevestigd." 138 | 139 | #: views.py:452 views.py:585 140 | msgid "Password successfully changed." 141 | msgstr "Wachtwoord met success gewijzigd." 142 | 143 | #: views.py:664 144 | msgid "Account settings updated." 145 | msgstr "Account instellingen aangepast." 146 | 147 | #: views.py:748 148 | #, python-brace-format 149 | msgid "" 150 | "Your account is now inactive and your data will be expunged in the next " 151 | "{expunge_hours} hours." 152 | msgstr "Je account is nu inactief and je data zal worden geschrapt in de komende {expunge_hours} uren." 153 | -------------------------------------------------------------------------------- /account/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Janusz Harkot , 2014 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-user-accounts\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 12 | "PO-Revision-Date: 2014-07-31 20:58+0000\n" 13 | "Last-Translator: Janusz Harkot \n" 14 | "Language-Team: Polish (http://www.transifex.com/projects/p/django-user-accounts/language/pl/)\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: pl\n" 19 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 20 | 21 | #: forms.py:27 forms.py:107 22 | msgid "Username" 23 | msgstr "Nazwa użytkownika" 24 | 25 | #: forms.py:33 forms.py:79 26 | msgid "Password" 27 | msgstr "Hasło" 28 | 29 | #: forms.py:37 30 | msgid "Password (again)" 31 | msgstr "Hasło (ponownie)" 32 | 33 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 34 | msgid "Email" 35 | msgstr "E-mail" 36 | 37 | #: forms.py:52 38 | msgid "Usernames can only contain letters, numbers and underscores." 39 | msgstr "Nazwa użytkownika może zawierać jedynie litery, cyfry i znak podkreślenia." 40 | 41 | #: forms.py:60 42 | msgid "This username is already taken. Please choose another." 43 | msgstr "Podana nazwa użytkownika jest w użyciu. Wybierz inną." 44 | 45 | #: forms.py:67 forms.py:217 46 | msgid "A user is registered with this email address." 47 | msgstr "W systemie jest już zarejestrowany użytkownik z tym adresem e-mail." 48 | 49 | #: forms.py:72 forms.py:162 forms.py:191 50 | msgid "You must type the same password each time." 51 | msgstr "Oba hasła muszą być identyczne." 52 | 53 | #: forms.py:83 54 | msgid "Remember Me" 55 | msgstr "Zapamiętaj mnie" 56 | 57 | #: forms.py:96 58 | msgid "This account is inactive." 59 | msgstr "Konto jest nieaktywne." 60 | 61 | #: forms.py:108 62 | msgid "The username and/or password you specified are not correct." 63 | msgstr "Podana nazwa użytkownika i/lub hasło są nieprawidłowe." 64 | 65 | #: forms.py:123 66 | msgid "The email address and/or password you specified are not correct." 67 | msgstr "Podany e-mail i/lub hasło są nieprawidłowe." 68 | 69 | #: forms.py:138 70 | msgid "Current Password" 71 | msgstr "Aktualne hasło" 72 | 73 | #: forms.py:142 forms.py:180 74 | msgid "New Password" 75 | msgstr "Nowe hasło" 76 | 77 | #: forms.py:146 forms.py:184 78 | msgid "New Password (again)" 79 | msgstr "Nowe hasło (ponownie)" 80 | 81 | #: forms.py:156 82 | msgid "Please type your current password." 83 | msgstr "Wprowadź swoje aktualne hasło." 84 | 85 | #: forms.py:173 86 | msgid "Email address can not be found." 87 | msgstr "Podany adres e-mail nie został znaleziony." 88 | 89 | #: forms.py:199 90 | msgid "Timezone" 91 | msgstr "Strefa czasowa" 92 | 93 | #: forms.py:205 94 | msgid "Language" 95 | msgstr "Język" 96 | 97 | #: models.py:34 98 | msgid "user" 99 | msgstr "użytkownik" 100 | 101 | #: models.py:35 102 | msgid "timezone" 103 | msgstr "strefa czasowa" 104 | 105 | #: models.py:37 106 | msgid "language" 107 | msgstr "język" 108 | 109 | #: models.py:250 110 | msgid "email address" 111 | msgstr "adres e-mail" 112 | 113 | #: models.py:251 114 | msgid "email addresses" 115 | msgstr "adresy e-mail" 116 | 117 | #: models.py:300 118 | msgid "email confirmation" 119 | msgstr "potwierdzenie e-mail" 120 | 121 | #: models.py:301 122 | msgid "email confirmations" 123 | msgstr "potwierdzenia e-mail" 124 | 125 | #: views.py:42 126 | #, python-brace-format 127 | msgid "Confirmation email sent to {email}." 128 | msgstr "E-mail z potwierdzeniem został wysłany na adres {email}." 129 | 130 | #: views.py:46 131 | #, python-brace-format 132 | msgid "The code {code} is invalid." 133 | msgstr "Kod {code} jest niepoprawny." 134 | 135 | #: views.py:379 136 | #, python-brace-format 137 | msgid "You have confirmed {email}." 138 | msgstr "Potwierdzono adres {email}." 139 | 140 | #: views.py:452 views.py:585 141 | msgid "Password successfully changed." 142 | msgstr "Hasło zostało zmienione." 143 | 144 | #: views.py:664 145 | msgid "Account settings updated." 146 | msgstr "Ustawienia konta zostały zaktualizowane." 147 | 148 | #: views.py:748 149 | #, python-brace-format 150 | msgid "" 151 | "Your account is now inactive and your data will be expunged in the next " 152 | "{expunge_hours} hours." 153 | msgstr "Twoje konto jest teraz nieaktywne, dane zostaną usunięte w ciągu następnych {expunge_hours} godzin." 154 | -------------------------------------------------------------------------------- /account/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Fábio C. Barrionuevo da Luz , 2014 7 | # Fábio C. Barrionuevo da Luz , 2014 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: django-user-accounts\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 13 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 14 | "Last-Translator: Brian Rosner \n" 15 | "Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/django-user-accounts/language/pt_BR/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: pt_BR\n" 20 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 21 | 22 | #: forms.py:27 forms.py:107 23 | msgid "Username" 24 | msgstr "Nome de Usuário" 25 | 26 | #: forms.py:33 forms.py:79 27 | msgid "Password" 28 | msgstr "Senha" 29 | 30 | #: forms.py:37 31 | msgid "Password (again)" 32 | msgstr "Senha (novamente)" 33 | 34 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 35 | msgid "Email" 36 | msgstr "Email" 37 | 38 | #: forms.py:52 39 | msgid "Usernames can only contain letters, numbers and underscores." 40 | msgstr "Nomes de usuário podem conter apenas letras, números e sublinhados." 41 | 42 | #: forms.py:60 43 | msgid "This username is already taken. Please choose another." 44 | msgstr "Este nome de usuário já está cadastrado. Por favor, escolha outro." 45 | 46 | #: forms.py:67 forms.py:217 47 | msgid "A user is registered with this email address." 48 | msgstr "Este endereço de email já foi registrado por outro usuário" 49 | 50 | #: forms.py:72 forms.py:162 forms.py:191 51 | msgid "You must type the same password each time." 52 | msgstr "Você deve digitar a mesma senha." 53 | 54 | #: forms.py:83 55 | msgid "Remember Me" 56 | msgstr "Lembrar-me" 57 | 58 | #: forms.py:96 59 | msgid "This account is inactive." 60 | msgstr "Esta conta está inativa." 61 | 62 | #: forms.py:108 63 | msgid "The username and/or password you specified are not correct." 64 | msgstr "O nome de usuário e/ou a senha informada não estão corretas" 65 | 66 | #: forms.py:123 67 | msgid "The email address and/or password you specified are not correct." 68 | msgstr "O email e/ou a senha informada não estão corretas" 69 | 70 | #: forms.py:138 71 | msgid "Current Password" 72 | msgstr "Senha atual" 73 | 74 | #: forms.py:142 forms.py:180 75 | msgid "New Password" 76 | msgstr "Nova senha" 77 | 78 | #: forms.py:146 forms.py:184 79 | msgid "New Password (again)" 80 | msgstr "Nova senha(novamente)" 81 | 82 | #: forms.py:156 83 | msgid "Please type your current password." 84 | msgstr "Por favor, entre com sua senha atual" 85 | 86 | #: forms.py:173 87 | msgid "Email address can not be found." 88 | msgstr "Endereço de email não pode ser encontrado." 89 | 90 | #: forms.py:199 91 | msgid "Timezone" 92 | msgstr "Fuso Horário" 93 | 94 | #: forms.py:205 95 | msgid "Language" 96 | msgstr "Idioma" 97 | 98 | #: models.py:34 99 | msgid "user" 100 | msgstr "usuário" 101 | 102 | #: models.py:35 103 | msgid "timezone" 104 | msgstr "fuso horário" 105 | 106 | #: models.py:37 107 | msgid "language" 108 | msgstr "idioma" 109 | 110 | #: models.py:250 111 | msgid "email address" 112 | msgstr "endereço de email" 113 | 114 | #: models.py:251 115 | msgid "email addresses" 116 | msgstr "endereços de email" 117 | 118 | #: models.py:300 119 | msgid "email confirmation" 120 | msgstr "email de confirmação" 121 | 122 | #: models.py:301 123 | msgid "email confirmations" 124 | msgstr "confirmações por e-mail" 125 | 126 | #: views.py:42 127 | #, python-brace-format 128 | msgid "Confirmation email sent to {email}." 129 | msgstr "E-mail de confirmação enviado para {email}." 130 | 131 | #: views.py:46 132 | #, python-brace-format 133 | msgid "The code {code} is invalid." 134 | msgstr "O código {code} é inválido." 135 | 136 | #: views.py:379 137 | #, python-brace-format 138 | msgid "You have confirmed {email}." 139 | msgstr "Você confirmou {email}." 140 | 141 | #: views.py:452 views.py:585 142 | msgid "Password successfully changed." 143 | msgstr "Senha alterada com sucesso." 144 | 145 | #: views.py:664 146 | msgid "Account settings updated." 147 | msgstr "Configurações da conta atualizada." 148 | 149 | #: views.py:748 150 | #, python-brace-format 151 | msgid "" 152 | "Your account is now inactive and your data will be expunged in the next " 153 | "{expunge_hours} hours." 154 | msgstr "Sua conta agora está inativo e seus dados serão apagados nas próximas {expunge_hours} horas." 155 | -------------------------------------------------------------------------------- /account/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Eugene MechanisM , 2012 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-user-accounts\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 12 | "PO-Revision-Date: 2014-08-12 01:34+0800\n" 13 | "Last-Translator: Vladislav 'SnoUweR' Kovalev \n" 14 | "Language-Team: Russian (http://www.transifex.com/projects/p/django-user-" 15 | "accounts/language/ru/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: ru\n" 20 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 21 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 22 | "X-Generator: Poedit 1.6.7\n" 23 | 24 | #: forms.py:27 forms.py:107 25 | msgid "Username" 26 | msgstr "Имя пользователя" 27 | 28 | #: forms.py:33 forms.py:79 29 | msgid "Password" 30 | msgstr "Пароль" 31 | 32 | #: forms.py:37 33 | msgid "Password (again)" 34 | msgstr "Пароль (еще раз)" 35 | 36 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 37 | msgid "Email" 38 | msgstr "Email" 39 | 40 | #: forms.py:52 41 | msgid "Usernames can only contain letters, numbers and underscores." 42 | msgstr "" 43 | "Имена пользователей могут состоять только из букв, цифер и подчеркиваний." 44 | 45 | #: forms.py:60 46 | msgid "This username is already taken. Please choose another." 47 | msgstr "Это имя пользователя уже занято. Пожалуйста, выберите другое." 48 | 49 | #: forms.py:67 forms.py:217 50 | msgid "A user is registered with this email address." 51 | msgstr "Данный электронный адрес уже используется в системе." 52 | 53 | #: forms.py:72 forms.py:162 forms.py:191 54 | msgid "You must type the same password each time." 55 | msgstr "Пароли не совпадают." 56 | 57 | #: forms.py:83 58 | msgid "Remember Me" 59 | msgstr "Запомнить меня" 60 | 61 | #: forms.py:96 62 | msgid "This account is inactive." 63 | msgstr "Этот аккаунт неактивен." 64 | 65 | #: forms.py:108 66 | msgid "The username and/or password you specified are not correct." 67 | msgstr "Имя пользователя и/или пароль введено некорректно." 68 | 69 | #: forms.py:123 70 | msgid "The email address and/or password you specified are not correct." 71 | msgstr "Email-адрес и/или пароль введено некорректно." 72 | 73 | #: forms.py:138 74 | msgid "Current Password" 75 | msgstr "Текущий пароль" 76 | 77 | #: forms.py:142 forms.py:180 78 | msgid "New Password" 79 | msgstr "Новый пароль" 80 | 81 | #: forms.py:146 forms.py:184 82 | msgid "New Password (again)" 83 | msgstr "Новый пароль (еще раз)" 84 | 85 | #: forms.py:156 86 | msgid "Please type your current password." 87 | msgstr "Пожалуйста, введите ваш текущий пароль." 88 | 89 | #: forms.py:173 90 | msgid "Email address can not be found." 91 | msgstr "Указанный адрес электронной почты не найден." 92 | 93 | #: forms.py:199 94 | msgid "Timezone" 95 | msgstr "Часовой пояс" 96 | 97 | #: forms.py:205 98 | msgid "Language" 99 | msgstr "Язык" 100 | 101 | #: models.py:34 102 | msgid "user" 103 | msgstr "пользователь" 104 | 105 | #: models.py:35 106 | msgid "timezone" 107 | msgstr "часовой пояс" 108 | 109 | #: models.py:37 110 | msgid "language" 111 | msgstr "язык" 112 | 113 | #: models.py:250 114 | msgid "email address" 115 | msgstr "email-адрес" 116 | 117 | #: models.py:251 118 | msgid "email addresses" 119 | msgstr "email-адреса" 120 | 121 | #: models.py:300 122 | msgid "email confirmation" 123 | msgstr "подтверждение email" 124 | 125 | #: models.py:301 126 | msgid "email confirmations" 127 | msgstr "подтверждения email" 128 | 129 | #: views.py:42 130 | #, python-brace-format 131 | msgid "Confirmation email sent to {email}." 132 | msgstr "Письмо с подтверждением было отправлено на {email}." 133 | 134 | #: views.py:46 135 | #, python-brace-format 136 | msgid "The code {code} is invalid." 137 | msgstr "Код {code} - неверный." 138 | 139 | #: views.py:379 140 | #, python-brace-format 141 | msgid "You have confirmed {email}." 142 | msgstr "Вы успешно подтвердили {email}." 143 | 144 | #: views.py:452 views.py:585 145 | msgid "Password successfully changed." 146 | msgstr "Пароль успешно изменен" 147 | 148 | #: views.py:664 149 | msgid "Account settings updated." 150 | msgstr "Настройки аккаунта изменены." 151 | 152 | #: views.py:748 153 | #, python-brace-format 154 | msgid "" 155 | "Your account is now inactive and your data will be expunged in the next " 156 | "{expunge_hours} hours." 157 | msgstr "" 158 | "Ваш аккаунт сейчас неактивен. Вся информация о нём будет удалена в течение " 159 | "{expunge_hours} часов." 160 | -------------------------------------------------------------------------------- /account/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # nolnol, 2014 7 | # nolnol, 2014 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: django-user-accounts\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2014-07-30 15:12-0600\n" 13 | "PO-Revision-Date: 2014-07-31 20:44+0000\n" 14 | "Last-Translator: Brian Rosner \n" 15 | "Language-Team: French (http://www.transifex.com/projects/p/django-user-accounts/language/fr/)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Language: fr\n" 20 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 21 | 22 | #: forms.py:27 forms.py:107 23 | msgid "Username" 24 | msgstr "Identifiant" 25 | 26 | #: forms.py:33 forms.py:79 27 | msgid "Password" 28 | msgstr "Mot de passe" 29 | 30 | #: forms.py:37 31 | msgid "Password (again)" 32 | msgstr "Mot de passe (à nouveau)" 33 | 34 | #: forms.py:41 forms.py:122 forms.py:168 forms.py:197 35 | msgid "Email" 36 | msgstr "Email" 37 | 38 | #: forms.py:52 39 | msgid "Usernames can only contain letters, numbers and underscores." 40 | msgstr "L'identifiant ne peut contenir que des lettres, chiffres et tiret-bas (_)" 41 | 42 | #: forms.py:60 43 | msgid "This username is already taken. Please choose another." 44 | msgstr "Cet identifiant est déjà utilisé. Merci d'en choisir un autre." 45 | 46 | #: forms.py:67 forms.py:217 47 | msgid "A user is registered with this email address." 48 | msgstr "Un utilisateur est déjà enregistré avec cette adresse email." 49 | 50 | #: forms.py:72 forms.py:162 forms.py:191 51 | msgid "You must type the same password each time." 52 | msgstr "Vous devez saisir le même mot de passe à chaque fois." 53 | 54 | #: forms.py:83 55 | msgid "Remember Me" 56 | msgstr "Se souvenir de moi" 57 | 58 | #: forms.py:96 59 | msgid "This account is inactive." 60 | msgstr "Ce compte est inactif." 61 | 62 | #: forms.py:108 63 | msgid "The username and/or password you specified are not correct." 64 | msgstr "L'identifiant et/ou le mot de passe saisis ne sont pas corrects." 65 | 66 | #: forms.py:123 67 | msgid "The email address and/or password you specified are not correct." 68 | msgstr "L'adresse email et/ou le mot de passe saisis ne sont pas corrects." 69 | 70 | #: forms.py:138 71 | msgid "Current Password" 72 | msgstr "Mot de passe actuel" 73 | 74 | #: forms.py:142 forms.py:180 75 | msgid "New Password" 76 | msgstr "Nouveau mot de passe" 77 | 78 | #: forms.py:146 forms.py:184 79 | msgid "New Password (again)" 80 | msgstr "Nouveau mot de passe (à nouveau)" 81 | 82 | #: forms.py:156 83 | msgid "Please type your current password." 84 | msgstr "Merci de saisir votre mot de passe actuel" 85 | 86 | #: forms.py:173 87 | msgid "Email address can not be found." 88 | msgstr "L'adresse email n'a pas pu être trouvée." 89 | 90 | #: forms.py:199 91 | msgid "Timezone" 92 | msgstr "Fuseau horaire" 93 | 94 | #: forms.py:205 95 | msgid "Language" 96 | msgstr "Langue" 97 | 98 | #: models.py:34 99 | msgid "user" 100 | msgstr "utilisateur" 101 | 102 | #: models.py:35 103 | msgid "timezone" 104 | msgstr "fuseau horaire" 105 | 106 | #: models.py:37 107 | msgid "language" 108 | msgstr "langue" 109 | 110 | #: models.py:250 111 | msgid "email address" 112 | msgstr "adresse email" 113 | 114 | #: models.py:251 115 | msgid "email addresses" 116 | msgstr "adresses email" 117 | 118 | #: models.py:300 119 | msgid "email confirmation" 120 | msgstr "email de confirmation" 121 | 122 | #: models.py:301 123 | msgid "email confirmations" 124 | msgstr "emails de confirmation" 125 | 126 | #: views.py:42 127 | #, python-brace-format 128 | msgid "Confirmation email sent to {email}." 129 | msgstr "L'email de confirmation a été envoyé à {email}." 130 | 131 | #: views.py:46 132 | #, python-brace-format 133 | msgid "The code {code} is invalid." 134 | msgstr "Le code {code} n'est pas valide." 135 | 136 | #: views.py:379 137 | #, python-brace-format 138 | msgid "You have confirmed {email}." 139 | msgstr "Vous avez validé l'adresse email {email}." 140 | 141 | #: views.py:452 views.py:585 142 | msgid "Password successfully changed." 143 | msgstr "Le mot de passe a été modifié avec succès." 144 | 145 | #: views.py:664 146 | msgid "Account settings updated." 147 | msgstr "Les paramètres de compte ont été mis à jour." 148 | 149 | #: views.py:748 150 | #, python-brace-format 151 | msgid "" 152 | "Your account is now inactive and your data will be expunged in the next " 153 | "{expunge_hours} hours." 154 | msgstr "Votre compte est maintenant inactif et vos données seront supprimées dans les {expunge_hours} prochaines heures." 155 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/account.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/account.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/account" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/account" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /account/locale/ja/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-user-account\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2015-07-20 11:21+0900\n" 11 | "PO-Revision-Date: 2015-07-20 11:35+0900\n" 12 | "Last-Translator: Hiroshi Miura \n" 13 | "Language-Team: ja \n" 14 | "Language: ja\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=1; plural=0;\n" 19 | "X-Generator: Poedit 1.5.4\n" 20 | 21 | #: forms.py:28 forms.py:108 22 | msgid "Username" 23 | msgstr "ユーザ名" 24 | 25 | #: forms.py:34 forms.py:80 26 | msgid "Password" 27 | msgstr "パスワード" 28 | 29 | #: forms.py:38 30 | msgid "Password (again)" 31 | msgstr "パスワード(確認)" 32 | 33 | #: forms.py:42 forms.py:123 forms.py:169 forms.py:198 34 | msgid "Email" 35 | msgstr "電子メールアドレス" 36 | 37 | #: forms.py:53 38 | msgid "Usernames can only contain letters, numbers and underscores." 39 | msgstr "" 40 | "ユーザ名には、半角英数字(A-Za-z0-9)とアンダースコア('_')のみが使用可能です。" 41 | 42 | #: forms.py:61 43 | msgid "This username is already taken. Please choose another." 44 | msgstr "ユーザ名がすでに使われています。他の名前をご利用ください。" 45 | 46 | #: forms.py:68 forms.py:218 47 | msgid "A user is registered with this email address." 48 | msgstr "このメールアドレスで登録されたユーザがいます。" 49 | 50 | #: forms.py:73 forms.py:163 forms.py:192 51 | msgid "You must type the same password each time." 52 | msgstr "2つのパスワードが異なります。" 53 | 54 | #: forms.py:84 55 | msgid "Remember Me" 56 | msgstr "ログインを覚えておく。" 57 | 58 | #: forms.py:97 59 | msgid "This account is inactive." 60 | msgstr "このアカウントは、無効にされています。" 61 | 62 | #: forms.py:109 63 | msgid "The username and/or password you specified are not correct." 64 | msgstr "入力されたユーザ名かパスワードが違うようです。" 65 | 66 | #: forms.py:124 67 | msgid "The email address and/or password you specified are not correct." 68 | msgstr "入力された電子メールかパスワードが違うようです。" 69 | 70 | #: forms.py:139 71 | msgid "Current Password" 72 | msgstr "現在のパスワード" 73 | 74 | #: forms.py:143 forms.py:181 75 | msgid "New Password" 76 | msgstr "新規パスワード" 77 | 78 | #: forms.py:147 forms.py:185 79 | msgid "New Password (again)" 80 | msgstr "新規パスワード(確認)" 81 | 82 | #: forms.py:157 83 | msgid "Please type your current password." 84 | msgstr "現在使用中のパスワードを入力してください。" 85 | 86 | #: forms.py:174 87 | msgid "Email address can not be found." 88 | msgstr "電子メールアドレスが見つかりません。" 89 | 90 | #: forms.py:200 91 | msgid "Timezone" 92 | msgstr "タイムゾーン" 93 | 94 | #: forms.py:206 95 | msgid "Language" 96 | msgstr "言語" 97 | 98 | #: models.py:34 99 | msgid "user" 100 | msgstr "ユーザ" 101 | 102 | #: models.py:35 103 | msgid "timezone" 104 | msgstr "タイムゾーン" 105 | 106 | #: models.py:37 107 | msgid "language" 108 | msgstr "言語" 109 | 110 | #: models.py:132 111 | msgid "code" 112 | msgstr "コード" 113 | 114 | #: models.py:133 115 | msgid "max uses" 116 | msgstr "最大使用可能数" 117 | 118 | #: models.py:134 119 | msgid "expiry" 120 | msgstr "有効期限" 121 | 122 | #: models.py:137 123 | msgid "notes" 124 | msgstr "備考" 125 | 126 | #: models.py:138 127 | msgid "sent" 128 | msgstr "送信日" 129 | 130 | #: models.py:139 131 | msgid "created" 132 | msgstr "作成日" 133 | 134 | #: models.py:140 135 | msgid "use count" 136 | msgstr "使用された回数" 137 | 138 | #: models.py:143 139 | msgid "signup code" 140 | msgstr "ユーザ登録コード" 141 | 142 | #: models.py:144 143 | msgid "signup codes" 144 | msgstr "ユーザ登録コード" 145 | 146 | #: models.py:250 147 | msgid "verified" 148 | msgstr "確認済み" 149 | 150 | #: models.py:251 151 | msgid "primary" 152 | msgstr "第一優先アドレス" 153 | 154 | #: models.py:256 155 | msgid "email address" 156 | msgstr "電子メールアドレス" 157 | 158 | #: models.py:257 159 | msgid "email addresses" 160 | msgstr "電子メールアドレス" 161 | 162 | #: models.py:306 163 | msgid "email confirmation" 164 | msgstr "メールアドレスの確認" 165 | 166 | #: models.py:307 167 | msgid "email confirmations" 168 | msgstr "メールアドレスの確認" 169 | 170 | #: models.py:356 171 | msgid "date requested" 172 | msgstr "削除要求日" 173 | 174 | #: models.py:357 175 | msgid "date expunged" 176 | msgstr "削除実行日" 177 | 178 | #: models.py:360 179 | msgid "account deletion" 180 | msgstr "アカウントの削除" 181 | 182 | #: models.py:361 183 | msgid "account deletions" 184 | msgstr "アカウントの削除" 185 | 186 | #: views.py:42 187 | #, python-brace-format 188 | msgid "Confirmation email sent to {email}." 189 | msgstr " {email}に確認メールが送信されました。" 190 | 191 | #: views.py:46 192 | #, python-brace-format 193 | msgid "The code {code} is invalid." 194 | msgstr "コード {code} が不正です。" 195 | 196 | #: views.py:381 197 | #, python-brace-format 198 | msgid "You have confirmed {email}." 199 | msgstr "{email} は確認されました。" 200 | 201 | #: views.py:454 views.py:590 202 | msgid "Password successfully changed." 203 | msgstr "パスワードを変更しました。" 204 | 205 | #: views.py:681 206 | msgid "Account settings updated." 207 | msgstr "アカウント設定が更新されました。" 208 | 209 | #: views.py:765 210 | #, python-brace-format 211 | msgid "" 212 | "Your account is now inactive and your data will be expunged in the next " 213 | "{expunge_hours} hours." 214 | msgstr "" 215 | "アカウントは無効になっています。あなたの登録データは、 {expunge_hours} 時間後" 216 | "に削除される予定です。" 217 | -------------------------------------------------------------------------------- /account/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import functools 3 | from urllib.parse import urlparse, urlunparse 4 | 5 | from django.contrib.auth import get_user_model 6 | from django.core.exceptions import SuspiciousOperation 7 | from django.http import HttpResponseRedirect, QueryDict 8 | from django.urls import NoReverseMatch, reverse 9 | from django.utils import timezone 10 | from django.utils.encoding import force_str 11 | 12 | from account.conf import settings 13 | 14 | from .models import PasswordHistory 15 | 16 | 17 | def get_user_lookup_kwargs(kwargs): 18 | result = {} 19 | username_field = getattr(get_user_model(), "USERNAME_FIELD", "username") 20 | for key, value in kwargs.items(): 21 | result[key.format(username=username_field)] = value 22 | return result 23 | 24 | 25 | def default_redirect(request, fallback_url, **kwargs): 26 | redirect_field_name = kwargs.get("redirect_field_name", "next") 27 | next_url = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name)) 28 | if not next_url: 29 | # try the session if available 30 | if hasattr(request, "session"): 31 | session_key_value = kwargs.get("session_key_value", "redirect_to") 32 | if session_key_value in request.session: 33 | next_url = request.session[session_key_value] 34 | del request.session[session_key_value] 35 | is_safe = functools.partial( 36 | ensure_safe_url, 37 | allowed_protocols=kwargs.get("allowed_protocols"), 38 | allowed_host=request.get_host() 39 | ) 40 | if next_url and is_safe(next_url): 41 | return next_url 42 | try: 43 | fallback_url = reverse(fallback_url) 44 | except NoReverseMatch: 45 | if callable(fallback_url): 46 | raise 47 | if "/" not in fallback_url and "." not in fallback_url: 48 | raise 49 | # assert the fallback URL is safe to return to caller. if it is 50 | # determined unsafe then raise an exception as the fallback value comes 51 | # from the a source the developer choose. 52 | is_safe(fallback_url, raise_on_fail=True) 53 | return fallback_url 54 | 55 | 56 | def user_display(user): 57 | return settings.ACCOUNT_USER_DISPLAY(user) 58 | 59 | 60 | def ensure_safe_url(url, allowed_protocols=None, allowed_host=None, raise_on_fail=False): 61 | if allowed_protocols is None: 62 | allowed_protocols = ["http", "https"] 63 | parsed = urlparse(url) 64 | # perform security checks to ensure no malicious intent 65 | # (i.e., an XSS attack with a data URL) 66 | safe = True 67 | if parsed.scheme and parsed.scheme not in allowed_protocols: 68 | if raise_on_fail: 69 | raise SuspiciousOperation("Unsafe redirect to URL with protocol '{0}'".format(parsed.scheme)) 70 | safe = False 71 | if allowed_host and parsed.netloc and parsed.netloc != allowed_host: 72 | if raise_on_fail: 73 | raise SuspiciousOperation("Unsafe redirect to URL not matching host '{0}'".format(allowed_host)) 74 | safe = False 75 | return safe 76 | 77 | 78 | def handle_redirect_to_login(request, **kwargs): 79 | login_url = kwargs.get("login_url") 80 | redirect_field_name = kwargs.get("redirect_field_name") 81 | next_url = kwargs.get("next_url") 82 | if login_url is None: 83 | login_url = settings.ACCOUNT_LOGIN_URL 84 | if next_url is None: 85 | next_url = request.get_full_path() 86 | try: 87 | login_url = reverse(login_url) 88 | except NoReverseMatch: 89 | if callable(login_url): 90 | raise 91 | if "/" not in login_url and "." not in login_url: 92 | raise 93 | url_bits = list(urlparse(force_str(login_url))) 94 | if redirect_field_name: 95 | querystring = QueryDict(url_bits[4], mutable=True) 96 | querystring[redirect_field_name] = next_url 97 | url_bits[4] = querystring.urlencode(safe="/") 98 | return HttpResponseRedirect(urlunparse(url_bits)) 99 | 100 | 101 | def get_form_data(form, field_name, default=None): 102 | if form.prefix: 103 | key = "-".join([form.prefix, field_name]) 104 | else: 105 | key = field_name 106 | return form.data.get(key, default) 107 | 108 | 109 | # https://stackoverflow.com/a/70419609/6461688 110 | def is_ajax(request): 111 | """ 112 | Return True if the request was sent with XMLHttpRequest, False otherwise. 113 | """ 114 | return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' 115 | 116 | 117 | def check_password_expired(user): 118 | """ 119 | Return True if password is expired and system is using 120 | password expiration, False otherwise. 121 | """ 122 | if not settings.ACCOUNT_PASSWORD_USE_HISTORY: 123 | return False 124 | 125 | if hasattr(user, "password_expiry"): 126 | # user-specific value 127 | expiry = user.password_expiry.expiry 128 | else: 129 | # use global value 130 | expiry = settings.ACCOUNT_PASSWORD_EXPIRY 131 | 132 | if expiry == 0: # zero indicates no expiration 133 | return False 134 | 135 | try: 136 | # get latest password info 137 | latest = user.password_history.latest("timestamp") 138 | except PasswordHistory.DoesNotExist: 139 | return False 140 | 141 | now = timezone.now() 142 | expiration = latest.timestamp + datetime.timedelta(seconds=expiry) 143 | 144 | return bool(expiration < now) 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://pinaxproject.com/pinax-design/social-banners/DUA.png) 2 | 3 | [![](https://img.shields.io/pypi/v/django-user-accounts.svg)](https://pypi.python.org/pypi/django-user-accounts/) 4 | 5 | [![Build](https://github.com/pinax/django-user-accounts/actions/workflows/ci.yaml/badge.svg)](https://github.com/pinax/django-user-accounts/actions) 6 | [![Codecov](https://img.shields.io/codecov/c/github/pinax/django-user-accounts.svg)](https://codecov.io/gh/pinax/django-user-accounts) 7 | [![](https://img.shields.io/github/contributors/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/graphs/contributors) 8 | [![](https://img.shields.io/github/issues-pr/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/pulls) 9 | [![](https://img.shields.io/github/issues-pr-closed/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/pulls?q=is%3Apr+is%3Aclosed) 10 | 11 | [![](http://slack.pinaxproject.com/badge.svg)](http://slack.pinaxproject.com/) 12 | [![](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 13 | 14 | 15 | # Table of Contents 16 | 17 | * [About Pinax](#about-pinax) 18 | * [Overview](#overview) 19 | * [Features](#features) 20 | * [Supported Django and Python versions](#supported-django-and-python-versions) 21 | * [Requirements](#requirements) 22 | * [Documentation](#documentation) 23 | * [Templates](#templates) 24 | * [Contribute](#contribute) 25 | * [Code of Conduct](#code-of-conduct) 26 | * [Connect with Pinax](#connect-with-pinax) 27 | * [License](#license) 28 | 29 | 30 | ## About Pinax 31 | 32 | Pinax is an open-source platform built on the Django Web Framework. It is an ecosystem of reusable Django apps, themes, and starter project templates. This collection can be found at http://pinaxproject.com. 33 | 34 | 35 | ## django-user-accounts 36 | 37 | ### Overview 38 | 39 | `django-user-accounts` provides a Django project with a very extensible infrastructure for dealing with user accounts. 40 | 41 | #### Features 42 | 43 | * Functionality for: 44 | * Log in (email or username authentication) 45 | * Sign up 46 | * Email confirmation 47 | * Signup tokens for private betas 48 | * Password reset 49 | * Password expiration 50 | * Account management (update account settings and change password) 51 | * Account deletion 52 | * Extensible class-based views and hooksets 53 | * Custom `User` model support 54 | 55 | #### Supported Django and Python versions 56 | 57 | Django / Python | 3.8 | 3.9 | 3.10 | 3.11 58 | --------------- | --- | --- | ---- | ---- 59 | 3.2 | * | * | * | 60 | 4.2 | * | * | * | * 61 | 62 | 63 | ## Requirements 64 | 65 | * Django 3.2 or 4.2 66 | * Python 3.8, 3.9, 3.10, 3.11 67 | * django-appconf (included in ``install_requires``) 68 | * pytz (included in ``install_requires``) 69 | 70 | 71 | ## Documentation 72 | 73 | See http://django-user-accounts.readthedocs.org/ for the `django-user-accounts` documentation. 74 | On September 17th, 2015, we did a Pinax Hangout on `django-user-accounts`. You can read the recap blog post and find the video here http://blog.pinaxproject.com/2015/10/12/recap-september-pinax-hangout/. 75 | 76 | The Pinax documentation is available at http://pinaxproject.com/pinax/. If you would like to help us improve our documentation or write more documentation, please join our Slack team and let us know! 77 | 78 | 79 | ### Templates 80 | 81 | Default templates are provided by the `pinax-templates` app in the 82 | [account](https://github.com/pinax/pinax-templates/tree/master/pinax/templates/templates/account) section of that project. 83 | 84 | Reference pinax-templates 85 | [installation instructions](https://github.com/pinax/pinax-templates/blob/master/README.md#installation) to include these templates in your project. 86 | 87 | View live `pinax-templates` examples and source at [Pinax Templates](https://templates.pinaxproject.com/)! 88 | 89 | See the `django-user-accounts` [templates](https://django-user-accounts.readthedocs.io/en/latest/templates.html) documentation for more information. 90 | 91 | 92 | ## Contribute 93 | 94 | For an overview on how contributing to Pinax works read this [blog post](http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/) 95 | and watch the included video, or read our [How to Contribute](http://pinaxproject.com/pinax/how_to_contribute/) section. For concrete contribution ideas, please see our 96 | [Ways to Contribute/What We Need Help With](http://pinaxproject.com/pinax/ways_to_contribute/) section. 97 | 98 | In case of any questions we recommend you join our [Pinax Slack team](http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. 99 | 100 | We also highly recommend reading our blog post on [Open Source and Self-Care](http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). 101 | 102 | 103 | ## Code of Conduct 104 | 105 | In order to foster a kind, inclusive, and harassment-free community, the Pinax Project 106 | has a [code of conduct](http://pinaxproject.com/pinax/code_of_conduct/). 107 | We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. 108 | 109 | 110 | ## Connect with Pinax 111 | 112 | For updates and news regarding the Pinax Project, please follow us on Twitter [@pinaxproject](https://twitter.com/pinaxproject) and check out our [Pinax Project blog](http://blog.pinaxproject.com). 113 | 114 | 115 | ## License 116 | 117 | Copyright (c) 2012-present James Tauber and contributors under the [MIT license](https://opensource.org/licenses/MIT). 118 | -------------------------------------------------------------------------------- /docs/migration.rst: -------------------------------------------------------------------------------- 1 | .. _migration: 2 | 3 | ==================== 4 | Migration from Pinax 5 | ==================== 6 | 7 | django-user-accounts is based on ``pinax.apps.account`` combining some of 8 | the supporting apps. django-email-confirmation, ``pinax.apps.signup_codes`` 9 | and bits of django-timezones have been merged to create django-user-accounts. 10 | 11 | This document will outline the changes needed to migrate from Pinax to using 12 | this app in your Django project. If you are new to django-user-accounts then 13 | this guide will not be useful to you. 14 | 15 | Database changes 16 | ================ 17 | 18 | Due to combining apps the table layout when converting from Pinax has changed. 19 | We've also taken the opportunity to update the schema to take advantage of 20 | much saner defaults. Here is SQL to convert from Pinax to django-user-accounts. 21 | 22 | PostgreSQL 23 | ---------- 24 | 25 | :: 26 | 27 | ALTER TABLE "signup_codes_signupcode" RENAME TO "account_signupcode"; 28 | ALTER TABLE "signup_codes_signupcoderesult" RENAME TO "account_signupcoderesult"; 29 | ALTER TABLE "emailconfirmation_emailaddress" RENAME TO "account_emailaddress"; 30 | ALTER TABLE "emailconfirmation_emailconfirmation" RENAME TO "account_emailconfirmation"; 31 | DROP TABLE "account_passwordreset"; 32 | ALTER TABLE "account_signupcode" ALTER COLUMN "code" TYPE varchar(64); 33 | ALTER TABLE "account_signupcode" ADD CONSTRAINT "account_signupcode_code_key" UNIQUE ("code"); 34 | ALTER TABLE "account_emailconfirmation" RENAME COLUMN "confirmation_key" TO "key"; 35 | ALTER TABLE "account_emailconfirmation" ALTER COLUMN "key" TYPE varchar(64); 36 | ALTER TABLE account_emailconfirmation ADD COLUMN created timestamp with time zone; 37 | UPDATE account_emailconfirmation SET created = sent; 38 | ALTER TABLE account_emailconfirmation ALTER COLUMN created SET NOT NULL; 39 | ALTER TABLE account_emailconfirmation ALTER COLUMN sent DROP NOT NULL; 40 | 41 | If ``ACCOUNT_EMAIL_UNIQUE`` is set to ``True`` (the default value) you need:: 42 | 43 | ALTER TABLE "account_emailaddress" ADD CONSTRAINT "account_emailaddress_email_key" UNIQUE ("email"); 44 | ALTER TABLE "account_emailaddress" DROP CONSTRAINT "emailconfirmation_emailaddress_user_id_email_key"; 45 | 46 | MySQL 47 | ----- 48 | 49 | :: 50 | 51 | RENAME TABLE `emailconfirmation_emailaddress` TO `account_emailaddress` ; 52 | RENAME TABLE `emailconfirmation_emailconfirmation` TO `account_emailconfirmation` ; 53 | DROP TABLE account_passwordreset; 54 | ALTER TABLE `account_emailconfirmation` CHANGE `confirmation_key` `key` VARCHAR(64) NOT NULL; 55 | ALTER TABLE `account_emailconfirmation` ADD UNIQUE (`key`); 56 | ALTER TABLE account_emailconfirmation ADD COLUMN created datetime NOT NULL; 57 | UPDATE account_emailconfirmation SET created = sent; 58 | ALTER TABLE `account_emailconfirmation` CHANGE `sent` `sent` DATETIME NULL; 59 | 60 | If ``ACCOUNT_EMAIL_UNIQUE`` is set to ``True`` (the default value) you need:: 61 | 62 | ALTER TABLE `account_emailaddress` ADD UNIQUE (`email`); 63 | ALTER TABLE account_emailaddress DROP INDEX user_id; 64 | 65 | If you have installed ``pinax.apps.signup_codes``:: 66 | 67 | RENAME TABLE `signup_codes_signupcode` TO `account_signupcode` ; 68 | RENAME TABLE `signup_codes_signupcoderesult` TO `account_signupcoderesult` ; 69 | 70 | 71 | URL changes 72 | =========== 73 | 74 | Here is a list of all URLs provided by django-user-accounts and how they map 75 | from Pinax. This assumes ``account.urls`` is mounted at ``/account/`` as it 76 | was in Pinax. 77 | 78 | ====================================== ==================================== 79 | Pinax django-user-accounts 80 | ====================================== ==================================== 81 | ``/account/login/`` ``/account/login/`` 82 | ``/account/signup/`` ``/account/signup/`` 83 | ``/account/confirm_email/`` ``/account/confirm_email/`` 84 | ``/account/password_change/`` ``/account/password/`` [1]_ 85 | ``/account/password_reset/`` ``/account/password/reset/`` 86 | ``/account/password_reset_done/`` *removed* 87 | ``/account/password_reset_key//`` ``/account/password/reset//`` 88 | ====================================== ==================================== 89 | 90 | .. [1] When user is anonymous and requests a GET the user is redirected to 91 | ``/account/password/reset/``. 92 | 93 | View changes 94 | ============ 95 | 96 | All views have been converted to class-based views. This is a big departure 97 | from the traditional function-based, but has the benefit of being much more 98 | flexible. 99 | 100 | @@@ todo: table of changes 101 | 102 | Settings changes 103 | ================ 104 | 105 | We have cleaned up settings and set saner defaults used by 106 | django-user-accounts. 107 | 108 | =========================================== =============================== 109 | Pinax django-user-accounts 110 | =========================================== =============================== 111 | ``ACCOUNT_OPEN_SIGNUP = True`` ``ACCOUNT_OPEN_SIGNUP = True`` 112 | ``ACCOUNT_UNIQUE_EMAIL = False`` ``ACCOUNT_EMAIL_UNIQUE = True`` 113 | ``EMAIL_CONFIRMATION_UNIQUE_EMAIL = False`` *removed* 114 | =========================================== =============================== 115 | 116 | General changes 117 | =============== 118 | 119 | django-user-accounts requires Django 1.4. This means we can take advantage of 120 | many of the new features offered by Django. This app implements all of the 121 | best practices of Django 1.4. If there is something missing you should let us 122 | know! 123 | -------------------------------------------------------------------------------- /account/locale/tr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-04-11 21:57+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: account/forms.py:28 account/forms.py:108 22 | msgid "Username" 23 | msgstr "Kullanıcı adı" 24 | 25 | #: account/forms.py:34 account/forms.py:80 26 | msgid "Password" 27 | msgstr "Şifre" 28 | 29 | #: account/forms.py:38 30 | msgid "Password (again)" 31 | msgstr "Şifre (tekrar)" 32 | 33 | #: account/forms.py:42 account/forms.py:123 account/forms.py:169 34 | #: account/forms.py:198 35 | msgid "Email" 36 | msgstr "Eposta" 37 | 38 | #: account/forms.py:53 39 | msgid "Usernames can only contain letters, numbers and underscores." 40 | msgstr "Kullanıcı adı sadece harfler, sayılar ve alt çizgiler içerebilir." 41 | 42 | #: account/forms.py:61 43 | msgid "This username is already taken. Please choose another." 44 | msgstr "Bu kullanıcı adı daha önce alınmış. Lütfen başka bir tane seçiniz." 45 | 46 | #: account/forms.py:68 account/forms.py:218 47 | msgid "A user is registered with this email address." 48 | msgstr "Bir kullanıcı bu eposta adresi ile kaydedildi." 49 | 50 | #: account/forms.py:73 account/forms.py:163 account/forms.py:192 51 | msgid "You must type the same password each time." 52 | msgstr "Şifrenizi her seferinde aynı girmelisiniz." 53 | 54 | #: account/forms.py:84 55 | msgid "Remember Me" 56 | msgstr "Beni Hatırla" 57 | 58 | #: account/forms.py:97 59 | msgid "This account is inactive." 60 | msgstr "Bu hesap inaktif." 61 | 62 | #: account/forms.py:109 63 | msgid "The username and/or password you specified are not correct." 64 | msgstr "Belirttiğiniz kullanıcı adı ve/veya şifre doğru değil." 65 | 66 | #: account/forms.py:124 67 | msgid "The email address and/or password you specified are not correct." 68 | msgstr "Belirttiğiniz eposta adresi ve/veya şifre doğru değil." 69 | 70 | #: account/forms.py:139 71 | msgid "Current Password" 72 | msgstr "Şuanki Şifre" 73 | 74 | #: account/forms.py:143 account/forms.py:181 75 | msgid "New Password" 76 | msgstr "Yeni Şifre" 77 | 78 | #: account/forms.py:147 account/forms.py:185 79 | msgid "New Password (again)" 80 | msgstr "Yeni Şifre (tekrar)" 81 | 82 | #: account/forms.py:157 83 | msgid "Please type your current password." 84 | msgstr "Lütfen şimdiki şifrenizi giriniz." 85 | 86 | #: account/forms.py:174 87 | msgid "Email address can not be found." 88 | msgstr "Eposta adresiniz bulunamıyor." 89 | 90 | #: account/forms.py:200 91 | msgid "Timezone" 92 | msgstr "Zaman Dilimi" 93 | 94 | #: account/forms.py:206 95 | msgid "Language" 96 | msgstr "Dil" 97 | 98 | #: account/models.py:36 99 | msgid "user" 100 | msgstr "kullanıcı" 101 | 102 | #: account/models.py:37 103 | msgid "timezone" 104 | msgstr "zaman dilimi" 105 | 106 | #: account/models.py:39 107 | msgid "language" 108 | msgstr "dil" 109 | 110 | #: account/models.py:135 111 | msgid "code" 112 | msgstr "kod" 113 | 114 | #: account/models.py:136 115 | msgid "max uses" 116 | msgstr "maximum kullanım" 117 | 118 | #: account/models.py:137 119 | msgid "expiry" 120 | msgstr "zaman aşımı" 121 | 122 | #: account/models.py:140 123 | msgid "notes" 124 | msgstr "notlar" 125 | 126 | #: account/models.py:141 127 | msgid "sent" 128 | msgstr "gönderildi" 129 | 130 | #: account/models.py:142 131 | msgid "created" 132 | msgstr "oluşturuldu" 133 | 134 | #: account/models.py:143 135 | msgid "use count" 136 | msgstr "kullanım sayısı" 137 | 138 | #: account/models.py:146 139 | msgid "signup code" 140 | msgstr "kayıt kodu" 141 | 142 | #: account/models.py:147 143 | msgid "signup codes" 144 | msgstr "kayıt kodları" 145 | 146 | #: account/models.py:254 147 | msgid "verified" 148 | msgstr "onaylandı" 149 | 150 | #: account/models.py:255 151 | msgid "primary" 152 | msgstr "birincil" 153 | 154 | #: account/models.py:260 155 | msgid "email address" 156 | msgstr "eposta adresi" 157 | 158 | #: account/models.py:261 159 | msgid "email addresses" 160 | msgstr "eposta adresleri" 161 | 162 | #: account/models.py:311 163 | msgid "email confirmation" 164 | msgstr "eposta onayı" 165 | 166 | #: account/models.py:312 167 | msgid "email confirmations" 168 | msgstr "eposta onayları" 169 | 170 | #: account/models.py:361 171 | msgid "date requested" 172 | msgstr "istenen tarih" 173 | 174 | #: account/models.py:362 175 | msgid "date expunged" 176 | msgstr "silinen tarih" 177 | 178 | #: account/models.py:365 179 | msgid "account deletion" 180 | msgstr "hesap silinmesi" 181 | 182 | #: account/models.py:366 183 | msgid "account deletions" 184 | msgstr "hespa silinmeleri" 185 | 186 | #: account/views.py:42 187 | #, python-brace-format 188 | msgid "Confirmation email sent to {email}." 189 | msgstr "Onay epostası {email} adresine yollandı." 190 | 191 | #: account/views.py:46 192 | #, python-brace-format 193 | msgid "The code {code} is invalid." 194 | msgstr "{code} kodu geçersiz." 195 | 196 | #: account/views.py:384 197 | #, python-brace-format 198 | msgid "You have confirmed {email}." 199 | msgstr "{email} adresini onayladınız." 200 | 201 | #: account/views.py:457 account/views.py:593 202 | msgid "Password successfully changed." 203 | msgstr "Şifre başarıyla değiştirildi." 204 | 205 | #: account/views.py:684 206 | msgid "Account settings updated." 207 | msgstr "Hesap ayarları güncelendi." 208 | 209 | #: account/views.py:768 210 | #, python-brace-format 211 | msgid "" 212 | "Your account is now inactive and your data will be expunged in the next " 213 | "{expunge_hours} hours." 214 | msgstr "Hesabınız inaktiftir ve verileriniz" 215 | "{expunge_hours} saat sonra silinecektir." 216 | 217 | -------------------------------------------------------------------------------- /account/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-user-accounts\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2017-10-17 18:31+0200\n" 10 | "PO-Revision-Date: 2023-11-03 11:29+0100\n" 11 | "Last-Translator: Guenther Meyer \n" 12 | "Language-Team: LANGUAGE \n" 13 | "accounts/language/de/)\n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: account/forms.py:45 account/forms.py:125 21 | msgid "Username" 22 | msgstr "Benutzername" 23 | 24 | #: account/forms.py:51 account/forms.py:140 account/forms.py:186 25 | #: account/forms.py:215 26 | msgid "Email" 27 | msgstr "E-Mail" 28 | 29 | #: account/forms.py:55 account/forms.py:97 30 | msgid "Password" 31 | msgstr "Passwort" 32 | 33 | #: account/forms.py:59 34 | msgid "Password (again)" 35 | msgstr "Passwort (wiederholen)" 36 | 37 | #: account/forms.py:70 38 | msgid "Usernames can only contain letters, numbers and underscores." 39 | msgstr "Der Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten." 40 | 41 | #: account/forms.py:78 42 | msgid "This username is already taken. Please choose another." 43 | msgstr "Dieser Benutzername ist bereits vergeben. Bitte wählen Sie einen anderen." 44 | 45 | #: account/forms.py:85 account/forms.py:235 46 | msgid "A user is registered with this email address." 47 | msgstr "Es ist bereits ein Benutzer mit dieser E-Mail-Adresse registriert." 48 | 49 | #: account/forms.py:90 account/forms.py:180 account/forms.py:209 50 | msgid "You must type the same password each time." 51 | msgstr "Die eingegebenen Passwörter stimmen nicht überein." 52 | 53 | #: account/forms.py:101 54 | msgid "Remember Me" 55 | msgstr "Auf diesem Computer merken" 56 | 57 | #: account/forms.py:114 58 | msgid "This account is inactive." 59 | msgstr "Dieses Konto ist nicht aktiv." 60 | 61 | #: account/forms.py:126 62 | msgid "The username and/or password you specified are not correct." 63 | msgstr "Der Benutzername und/oder das angegebene Passwort sind nicht korrekt." 64 | 65 | #: account/forms.py:141 66 | msgid "The email address and/or password you specified are not correct." 67 | msgstr "Die E-Mail-Adresse und/oder das eingegebene Passwort sind nicht korrekt." 68 | 69 | #: account/forms.py:156 70 | msgid "Current Password" 71 | msgstr "Derzeitiges Passwort" 72 | 73 | #: account/forms.py:160 account/forms.py:198 74 | msgid "New Password" 75 | msgstr "Neues Passwort" 76 | 77 | #: account/forms.py:164 account/forms.py:202 78 | msgid "New Password (again)" 79 | msgstr "Neues Passwort (wiederholen)" 80 | 81 | #: account/forms.py:174 82 | msgid "Please type your current password." 83 | msgstr "Bitte derzeitiges Passwort eingeben." 84 | 85 | #: account/forms.py:191 86 | msgid "Email address can not be found." 87 | msgstr "E-Mail-Adresse wurde nicht gefunden." 88 | 89 | #: account/forms.py:217 90 | msgid "Timezone" 91 | msgstr "Zeitzone" 92 | 93 | #: account/forms.py:223 94 | msgid "Language" 95 | msgstr "Sprache" 96 | 97 | #: account/middleware.py:92 98 | msgid "Your password has expired. Please save a new password." 99 | msgstr "Ihr Passwort ist abgelaufen. Bitte wählen Sie ein neues Passwort." 100 | 101 | #: account/models.py:36 account/models.py:412 102 | msgid "user" 103 | msgstr "user" 104 | 105 | #: account/models.py:37 106 | msgid "timezone" 107 | msgstr "timezone" 108 | 109 | #: account/models.py:39 110 | msgid "language" 111 | msgstr "language" 112 | 113 | #: account/models.py:140 114 | msgid "code" 115 | msgstr "code" 116 | 117 | #: account/models.py:141 118 | msgid "max uses" 119 | msgstr "max uses" 120 | 121 | #: account/models.py:142 122 | msgid "expiry" 123 | msgstr "expiry" 124 | 125 | #: account/models.py:145 126 | msgid "notes" 127 | msgstr "notes" 128 | 129 | #: account/models.py:146 130 | msgid "sent" 131 | msgstr "sent" 132 | 133 | #: account/models.py:147 134 | msgid "created" 135 | msgstr "created" 136 | 137 | #: account/models.py:148 138 | msgid "use count" 139 | msgstr "use count" 140 | 141 | #: account/models.py:151 142 | msgid "signup code" 143 | msgstr "signup code" 144 | 145 | #: account/models.py:152 146 | msgid "signup codes" 147 | msgstr "signup codes" 148 | 149 | #: account/models.py:259 150 | msgid "verified" 151 | msgstr "verified" 152 | 153 | #: account/models.py:260 154 | msgid "primary" 155 | msgstr "primary" 156 | 157 | #: account/models.py:265 158 | msgid "email address" 159 | msgstr "email address" 160 | 161 | #: account/models.py:266 162 | msgid "email addresses" 163 | msgstr "email addresses" 164 | 165 | #: account/models.py:316 166 | msgid "email confirmation" 167 | msgstr "email confirmation" 168 | 169 | #: account/models.py:317 170 | msgid "email confirmations" 171 | msgstr "email confirmations" 172 | 173 | #: account/models.py:366 174 | msgid "date requested" 175 | msgstr "date requested" 176 | 177 | #: account/models.py:367 178 | msgid "date expunged" 179 | msgstr "date expunged" 180 | 181 | #: account/models.py:370 182 | msgid "account deletion" 183 | msgstr "account deletion" 184 | 185 | #: account/models.py:371 186 | msgid "account deletions" 187 | msgstr "account deletions" 188 | 189 | #: account/models.py:400 190 | msgid "password history" 191 | msgstr "password history" 192 | 193 | #: account/models.py:401 194 | msgid "password histories" 195 | msgstr "password histories" 196 | 197 | #: account/views.py:54 account/views.py:554 198 | msgid "Password successfully changed." 199 | msgstr "Passwort wurde gändert." 200 | 201 | #: account/views.py:129 202 | #, python-brace-format 203 | msgid "Confirmation email sent to {email}." 204 | msgstr "Eine Bestätigungs-E-Mail wurde an {email} gesendet." 205 | 206 | #: account/views.py:133 207 | #, python-brace-format 208 | msgid "The code {code} is invalid." 209 | msgstr "Der Code {code} ist ungültig." 210 | 211 | #: account/views.py:459 212 | #, python-brace-format 213 | msgid "You have confirmed {email}." 214 | msgstr "Sie haben {email} bestätigt." 215 | 216 | #: account/views.py:463 217 | #, python-brace-format 218 | msgid "Email confirmation for {email} has expired." 219 | msgstr "Die E-Mail-Bestätigung für {email} ist abgelaufen." 220 | 221 | #: account/views.py:729 222 | msgid "Account settings updated." 223 | msgstr "Kontoeinstellungen aktualisiert." 224 | 225 | #: account/views.py:813 226 | #, python-brace-format 227 | msgid "" 228 | "Your account is now inactive and your data will be expunged in the next " 229 | "{expunge_hours} hours." 230 | msgstr "" 231 | "Ihr Account ist jetzt deaktiviert und Ihre Daten werden in den nächsten " 232 | "{expunge_hours} Stunden endgültig gelöscht." 233 | 234 | -------------------------------------------------------------------------------- /account/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Erik Rivera , 2012 7 | # Martin Gaitan , 2014 8 | # Martin Gaitan , 2014 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: django-user-accounts\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2017-03-30 18:22-0600\n" 14 | "PO-Revision-Date: 2017-03-30 18:22-0600\n" 15 | "Last-Translator: Pelana \n" 16 | "Language-Team: Spanish (http://www.transifex.com/projects/p/django-user-" 17 | "accounts/language/es/)\n" 18 | "Language: es\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | "X-Generator: Poedit 1.8.9\n" 24 | 25 | #: account/forms.py:45 account/forms.py:125 26 | msgid "Username" 27 | msgstr "Usuario" 28 | 29 | #: account/forms.py:51 account/forms.py:97 30 | msgid "Password" 31 | msgstr "Contraseña" 32 | 33 | #: account/forms.py:55 34 | msgid "Password (again)" 35 | msgstr "Contraseña (repetir)" 36 | 37 | #: account/forms.py:59 account/forms.py:140 account/forms.py:186 38 | #: account/forms.py:215 39 | msgid "Email" 40 | msgstr "Correo electrónico" 41 | 42 | #: account/forms.py:70 43 | msgid "Usernames can only contain letters, numbers and underscores." 44 | msgstr "" 45 | "Los nombres de usuario solo pueden contener letras, números y subguiones" 46 | 47 | #: account/forms.py:78 48 | msgid "This username is already taken. Please choose another." 49 | msgstr "Este nombre de usuario ya está en uso. Por favor elija otro." 50 | 51 | #: account/forms.py:85 account/forms.py:235 52 | msgid "A user is registered with this email address." 53 | msgstr "Un usuario se ha registrado con esta dirección de correo electrónico." 54 | 55 | #: account/forms.py:90 account/forms.py:180 account/forms.py:209 56 | msgid "You must type the same password each time." 57 | msgstr "Debe escribir la misma contraseña cada vez." 58 | 59 | #: account/forms.py:101 60 | msgid "Remember Me" 61 | msgstr "Recordarme" 62 | 63 | #: account/forms.py:114 64 | msgid "This account is inactive." 65 | msgstr "Esta cuenta está inactiva." 66 | 67 | #: account/forms.py:126 68 | msgid "The username and/or password you specified are not correct." 69 | msgstr "" 70 | "El nombre de usuario y/o la contraseña que ha especificado no son correctas." 71 | 72 | #: account/forms.py:141 73 | msgid "The email address and/or password you specified are not correct." 74 | msgstr "" 75 | "La dirección de correo electrónico y/o la contraseña que ha especificado no " 76 | "son correctas." 77 | 78 | #: account/forms.py:156 79 | msgid "Current Password" 80 | msgstr "Contraseña actual" 81 | 82 | #: account/forms.py:160 account/forms.py:198 83 | msgid "New Password" 84 | msgstr "Contraseña nueva" 85 | 86 | #: account/forms.py:164 account/forms.py:202 87 | msgid "New Password (again)" 88 | msgstr "Contraseña nueva (repetir)" 89 | 90 | #: account/forms.py:174 91 | msgid "Please type your current password." 92 | msgstr "Por favor escriba su contraseña actual." 93 | 94 | #: account/forms.py:191 95 | msgid "Email address can not be found." 96 | msgstr "El email no pudo ser encontrado." 97 | 98 | #: account/forms.py:217 99 | msgid "Timezone" 100 | msgstr "Zona horaria" 101 | 102 | #: account/forms.py:223 103 | msgid "Language" 104 | msgstr "Idioma" 105 | 106 | #: account/middleware.py:92 107 | msgid "Your password has expired. Please save a new password." 108 | msgstr "Tu contraseña expiró. Guarda tu Contraseña." 109 | 110 | #: account/models.py:36 account/models.py:412 111 | msgid "user" 112 | msgstr "usuario" 113 | 114 | #: account/models.py:37 115 | msgid "timezone" 116 | msgstr "zona horaria" 117 | 118 | #: account/models.py:39 119 | msgid "language" 120 | msgstr "idioma" 121 | 122 | #: account/models.py:140 123 | msgid "code" 124 | msgstr "código" 125 | 126 | #: account/models.py:141 127 | msgid "max uses" 128 | msgstr "usos maximos" 129 | 130 | #: account/models.py:142 131 | msgid "expiry" 132 | msgstr "expiró" 133 | 134 | #: account/models.py:145 135 | msgid "notes" 136 | msgstr "notas" 137 | 138 | #: account/models.py:146 139 | msgid "sent" 140 | msgstr "enviado" 141 | 142 | #: account/models.py:147 143 | msgid "created" 144 | msgstr "creado" 145 | 146 | #: account/models.py:148 147 | msgid "use count" 148 | msgstr "usos" 149 | 150 | #: account/models.py:151 151 | msgid "signup code" 152 | msgstr "código " 153 | 154 | #: account/models.py:152 155 | msgid "signup codes" 156 | msgstr "códigos de registro" 157 | 158 | #: account/models.py:259 159 | msgid "verified" 160 | msgstr "verificado" 161 | 162 | #: account/models.py:260 163 | msgid "primary" 164 | msgstr "primario" 165 | 166 | #: account/models.py:265 167 | msgid "email address" 168 | msgstr "correo electrónico" 169 | 170 | #: account/models.py:266 171 | msgid "email addresses" 172 | msgstr "correos electrónicos" 173 | 174 | #: account/models.py:316 175 | msgid "email confirmation" 176 | msgstr "confirmación de correo electrónico" 177 | 178 | #: account/models.py:317 179 | msgid "email confirmations" 180 | msgstr "confirmaciones de correos electrónicos" 181 | 182 | #: account/models.py:366 183 | msgid "date requested" 184 | msgstr "fecha solicitada" 185 | 186 | #: account/models.py:367 187 | msgid "date expunged" 188 | msgstr "fecha de expiración" 189 | 190 | #: account/models.py:370 191 | msgid "account deletion" 192 | msgstr "cuenta borrada" 193 | 194 | #: account/models.py:371 195 | msgid "account deletions" 196 | msgstr "cuentas borradas" 197 | 198 | #: account/models.py:400 199 | msgid "password history" 200 | msgstr "historial de contraseña" 201 | 202 | #: account/models.py:401 203 | msgid "password histories" 204 | msgstr "historico de contraseñas" 205 | 206 | #: account/views.py:50 account/views.py:524 207 | msgid "Password successfully changed." 208 | msgstr "La contraseña se ha cambiado con éxito." 209 | 210 | #: account/views.py:125 211 | #, python-brace-format 212 | msgid "Confirmation email sent to {email}." 213 | msgstr "Email de confirmación enviado a {email}." 214 | 215 | #: account/views.py:129 216 | #, python-brace-format 217 | msgid "The code {code} is invalid." 218 | msgstr "El código {code} es inválido." 219 | 220 | #: account/views.py:442 221 | #, python-brace-format 222 | msgid "You have confirmed {email}." 223 | msgstr "Has confirmado {email}." 224 | 225 | #: account/views.py:672 226 | msgid "Account settings updated." 227 | msgstr "Los ajustes de la cuenta actualizados." 228 | 229 | #: account/views.py:756 230 | #, python-brace-format 231 | msgid "" 232 | "Your account is now inactive and your data will be expunged in the next " 233 | "{expunge_hours} hours." 234 | msgstr "" 235 | "Tu cuenta está ahora inactiva y tus datos serán eliminados en las próximas " 236 | "{expunge_hours} horas." 237 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | .. _settings: 2 | 3 | ======== 4 | Settings 5 | ======== 6 | 7 | ``ACCOUNT_OPEN_SIGNUP`` 8 | ======================= 9 | 10 | Default: ``True`` 11 | 12 | If ``True``, creation of new accounts is allowed. When the signup view is 13 | called, the template ``account/signup.html`` will be displayed, usually 14 | showing a form to collect the new user data. 15 | 16 | If ``False``, creation of new accounts is disabled. When the signup view is 17 | called, the template ``account/signup_closed.html`` will be displayed. 18 | 19 | ``ACCOUNT_LOGIN_URL`` 20 | ===================== 21 | 22 | Default: ``"account_login"`` 23 | 24 | The name of the urlconf that calls the login view. 25 | 26 | ``ACCOUNT_SIGNUP_REDIRECT_URL`` 27 | =============================== 28 | 29 | Default: ``"/"`` 30 | 31 | The url where the user will be redirected after a successful signup. 32 | 33 | ``ACCOUNT_LOGIN_REDIRECT_URL`` 34 | ============================== 35 | 36 | Default: ``"/"`` 37 | 38 | The url where the user will be redirected after a successful authentication, 39 | unless the ``next`` parameter is defined in the request. 40 | 41 | ``ACCOUNT_LOGOUT_REDIRECT_URL`` 42 | =============================== 43 | 44 | Default: ``"/"`` 45 | 46 | The url where the user will be redirected after logging out. 47 | 48 | ``ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL`` 49 | ======================================== 50 | 51 | Default: ``"account_password"`` 52 | 53 | The url where the user will be redirected after changing his password. 54 | 55 | ``ACCOUNT_PASSWORD_RESET_REDIRECT_URL`` 56 | ======================================= 57 | 58 | Default: ``"account_login"`` 59 | 60 | The url where the user will be redirected after resetting his password. 61 | 62 | ``ACCOUNT_REMEMBER_ME_EXPIRY`` 63 | ============================== 64 | 65 | Default: ``60 * 60 * 24 * 365 * 10`` 66 | 67 | The number of seconds that the user will remain authenticated after he logs in 68 | the site. 69 | 70 | ``ACCOUNT_USER_DISPLAY`` 71 | ======================== 72 | 73 | Default: ``lambda user: user.username`` 74 | 75 | The function that will be called by the template tag user_display. 76 | 77 | ``ACCOUNT_CREATE_ON_SAVE`` 78 | ========================== 79 | 80 | Default: ``True`` 81 | 82 | If ``True``, an account instance will be created when a new user is created. 83 | 84 | ``ACCOUNT_EMAIL_UNIQUE`` 85 | ======================== 86 | 87 | Default: ``True`` 88 | 89 | If ``False``, more than one user can have the same email address. 90 | 91 | ``ACCOUNT_EMAIL_CONFIRMATION_REQUIRED`` 92 | ======================================= 93 | 94 | Default: ``False`` 95 | 96 | If ``True``, new user accounts will be created as inactive. The user must use 97 | the activation link to activate his account. 98 | 99 | ``ACCOUNT_EMAIL_CONFIRMATION_EMAIL`` 100 | ==================================== 101 | 102 | Default: ``True`` 103 | 104 | If ``True``, an email confirmation message will be sent to the user when they 105 | make a new account. 106 | 107 | ``ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS`` 108 | ========================================== 109 | 110 | Default: ``3`` 111 | 112 | After this time, the email confirmation link will not be longer valid. 113 | 114 | ``ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL`` 115 | ===================================================== 116 | 117 | Default: ``"account_login"`` 118 | 119 | A urlconf name where the user will be redirected after confirming an email 120 | address, if he is not authenticated. 121 | 122 | ``ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL`` 123 | ========================================================= 124 | 125 | Default: ``None`` 126 | 127 | A urlconf name where the user will be redirected after confirming an email 128 | address, if he is authenticated. If not set, this url will be the one defined 129 | in ``ACCOUNT_LOGIN_REDIRECT_URL``. 130 | 131 | ``ACCOUNT_EMAIL_CONFIRMATION_URL`` 132 | ================================== 133 | 134 | Default: ``"account_confirm_email"`` 135 | 136 | A urlconf name that will be used to confirm the user email (usually from the 137 | email message they received). 138 | 139 | ``ACCOUNT_SETTINGS_REDIRECT_URL`` 140 | ================================= 141 | 142 | Default: ``"account_settings"`` 143 | 144 | The url where the user will be redirected after updating their account settings. 145 | 146 | ``ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE`` 147 | ===================================== 148 | 149 | Default: ``True`` 150 | 151 | If ``True``, an notification email will be sent whenever a user changes their 152 | password. 153 | 154 | ``ACCOUNT_DELETION_MARK_CALLBACK`` 155 | ================================== 156 | 157 | Default: ``"account.callbacks.account_delete_mark"`` 158 | 159 | This function will be called just after a user asks for account deletion. 160 | 161 | ``ACCOUNT_DELETION_EXPUNGE_CALLBACK`` 162 | ===================================== 163 | 164 | Default: ``"account.callbacks.account_delete_expunge"`` 165 | 166 | The function that will be called to expunge accounts. 167 | 168 | ``ACCOUNT_DELETION_EXPUNGE_HOURS`` 169 | ================================== 170 | 171 | Default: ``48`` 172 | 173 | The minimum time in hours since a user asks for account deletion until their 174 | account is deleted. 175 | 176 | ``ACCOUNT_HOOKSET`` 177 | =================== 178 | 179 | Default: ``"account.hooks.AccountDefaultHookSet"`` 180 | 181 | This setting allows you define your own hooks for specific functionality that 182 | django-user-accounts exposes. Point this to a class using a string and you can 183 | override the following methods: 184 | 185 | * ``send_invitation_email(to, ctx)`` 186 | * ``send_confirmation_email(to, ctx)`` 187 | * ``send_password_change_email(to, ctx)`` 188 | * ``send_password_reset_email(to, ctx)`` 189 | 190 | ``ACCOUNT_TIMEZONES`` 191 | ===================== 192 | 193 | Default: ``list(zip(pytz.all_timezones, pytz.all_timezones))`` 194 | 195 | A list of time zones available for the user to set as their current time zone. 196 | 197 | ``ACCOUNT_LANGUAGES`` 198 | ===================== 199 | 200 | A tuple of languages available for the user to set as their preferred language. 201 | 202 | See full list in: https://github.com/pinax/django-user-accounts/blob/master/account/languages.py 203 | 204 | ``ACCOUNT_USE_AUTH_AUTHENTICATE`` 205 | ================================= 206 | 207 | Default: ``False`` 208 | 209 | If ``True``, ``django.contrib.auth.authenticate`` will be used to authenticate 210 | the user. 211 | 212 | .. note:: 213 | According to the comments in the code, this setting is deprecated and, 214 | in the future, ``django.contrib.auth.authenticate`` will be the preferred 215 | method. 216 | 217 | ``ACCOUNT_APPROVAL_REQUIRED`` 218 | ================================== 219 | 220 | Default: ``False`` 221 | 222 | This setting will make new registrations inactive, until staff will set ``is_active`` 223 | flag in admin panel. Additional integration (like sending notifications to staff) 224 | is possible with ``account.signals.user_signed_up`` signal. 225 | -------------------------------------------------------------------------------- /account/tests/test_password.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import django 4 | from django.contrib.auth.hashers import check_password, make_password 5 | from django.contrib.auth.models import User 6 | from django.test import TestCase, modify_settings, override_settings 7 | from django.urls import reverse 8 | 9 | import pytz 10 | 11 | from account.models import PasswordExpiry, PasswordHistory 12 | from account.utils import check_password_expired 13 | 14 | 15 | def middleware_kwarg(value): 16 | if django.VERSION >= (1, 10): 17 | kwarg = "MIDDLEWARE" 18 | else: 19 | kwarg = "MIDDLEWARE_CLASSES" 20 | return {kwarg: value} 21 | 22 | 23 | @override_settings( 24 | ACCOUNT_PASSWORD_USE_HISTORY=True 25 | ) 26 | @modify_settings( 27 | **middleware_kwarg({ 28 | "append": "account.middleware.ExpiredPasswordMiddleware" 29 | }) 30 | ) 31 | class PasswordExpirationTestCase(TestCase): 32 | 33 | def setUp(self): 34 | self.username = "user1" 35 | self.email = "user1@example.com" 36 | self.password = "changeme" 37 | self.user = User.objects.create_user( 38 | self.username, 39 | email=self.email, 40 | password=self.password, 41 | ) 42 | # create PasswordExpiry for user 43 | self.expiry = PasswordExpiry.objects.create( 44 | user=self.user, 45 | expiry=60, # password expires after sixty seconds 46 | ) 47 | # create PasswordHistory for user 48 | self.history = PasswordHistory.objects.create( 49 | user=self.user, 50 | password=make_password(self.password) 51 | ) 52 | 53 | def test_signup(self): 54 | """ 55 | Ensure new user has one PasswordHistory and no PasswordExpiry. 56 | """ 57 | email = "foobar@example.com" 58 | password = "bar" 59 | post_data = { 60 | "username": "foo", 61 | "password": password, 62 | "password_confirm": password, 63 | "email": email, 64 | } 65 | response = self.client.post(reverse("account_signup"), post_data) 66 | self.assertEqual(response.status_code, 302) 67 | user = User.objects.get(email=email) 68 | self.assertFalse(hasattr(user, "password_expiry")) 69 | latest_history = user.password_history.latest("timestamp") 70 | self.assertTrue(latest_history) 71 | 72 | # verify password is not expired 73 | self.assertFalse(check_password_expired(user)) 74 | # verify raw password matches encrypted password in history 75 | self.assertTrue(check_password(password, latest_history.password)) 76 | 77 | def test_get_not_expired(self): 78 | """ 79 | Ensure authenticated user can retrieve account settings page 80 | without "password change" redirect. 81 | """ 82 | self.client.login(username=self.username, password=self.password) 83 | 84 | # get account settings page (could be any application page) 85 | response = self.client.get(reverse("account_settings")) 86 | self.assertEqual(response.status_code, 200) 87 | 88 | def test_get_expired(self): 89 | """ 90 | Ensure authenticated user is redirected to change password 91 | when retrieving account settings page if password is expired. 92 | """ 93 | # set PasswordHistory timestamp in past so password is expired. 94 | self.history.timestamp = ( 95 | datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=1, seconds=self.expiry.expiry) 96 | ) 97 | self.history.save() 98 | 99 | self.client.login(username=self.username, password=self.password) 100 | 101 | # get account settings page (could be any application page) 102 | url_name = "account_settings" 103 | response = self.client.get(reverse(url_name)) 104 | 105 | # verify desired page is set as "?next=" in redirect URL 106 | redirect_url = "{}?next={}".format(reverse("account_password"), url_name) 107 | self.assertRedirects(response, redirect_url) 108 | 109 | def test_password_expiration_reset(self): 110 | """ 111 | Ensure changing password results in new PasswordHistory. 112 | """ 113 | history_count = self.user.password_history.count() 114 | 115 | # get login 116 | self.client.login(username=self.username, password=self.password) 117 | 118 | # post new password to reset PasswordHistory 119 | new_password = "lynyrdskynyrd" 120 | post_data = { 121 | "password_current": self.password, 122 | "password_new": new_password, 123 | "password_new_confirm": new_password, 124 | } 125 | self.client.post( 126 | reverse("account_password"), 127 | post_data 128 | ) 129 | # Should see one more history entry for this user 130 | self.assertEqual(self.user.password_history.count(), history_count + 1) 131 | 132 | latest = PasswordHistory.objects.latest("timestamp") 133 | self.assertTrue(latest != self.history) 134 | self.assertTrue(latest.timestamp > self.history.timestamp) 135 | 136 | 137 | @modify_settings( 138 | **middleware_kwarg({ 139 | "append": "account.middleware.ExpiredPasswordMiddleware" 140 | }) 141 | ) 142 | class ExistingUserNoHistoryTestCase(TestCase): 143 | """ 144 | Tests where user has no PasswordHistory. 145 | """ 146 | 147 | def setUp(self): 148 | self.username = "user1" 149 | self.email = "user1@example.com" 150 | self.password = "changeme" 151 | self.user = User.objects.create_user( 152 | self.username, 153 | email=self.email, 154 | password=self.password, 155 | ) 156 | 157 | def test_get_no_history(self): 158 | """ 159 | Ensure authenticated user without password history can retrieve 160 | account settings page without "password change" redirect. 161 | """ 162 | self.client.login(username=self.username, password=self.password) 163 | 164 | with override_settings( 165 | ACCOUNT_PASSWORD_USE_HISTORY=True 166 | ): 167 | # get account settings page (could be any application page) 168 | response = self.client.get(reverse("account_settings")) 169 | self.assertEqual(response.status_code, 200) 170 | 171 | def test_password_expiration_reset(self): 172 | """ 173 | Ensure changing password results in new PasswordHistory, 174 | even when no PasswordHistory exists. 175 | """ 176 | history_count = self.user.password_history.count() 177 | 178 | # get login 179 | self.client.login(username=self.username, password=self.password) 180 | 181 | # post new password to reset PasswordHistory 182 | new_password = "lynyrdskynyrd" 183 | post_data = { 184 | "password_current": self.password, 185 | "password_new": new_password, 186 | "password_new_confirm": new_password, 187 | } 188 | with override_settings( 189 | ACCOUNT_PASSWORD_USE_HISTORY=True 190 | ): 191 | self.client.post( 192 | reverse("account_password"), 193 | post_data 194 | ) 195 | # Should see one more history entry for this user 196 | self.assertEqual(self.user.password_history.count(), history_count + 1) 197 | 198 | def test_password_reset(self): 199 | """ 200 | Ensure changing password results in NO new PasswordHistory 201 | when ACCOUNT_PASSWORD_USE_HISTORY == False. 202 | """ 203 | # get login 204 | self.client.login(username=self.username, password=self.password) 205 | 206 | # post new password to reset PasswordHistory 207 | new_password = "lynyrdskynyrd" 208 | post_data = { 209 | "password_current": self.password, 210 | "password_new": new_password, 211 | "password_new_confirm": new_password, 212 | } 213 | with override_settings( 214 | ACCOUNT_PASSWORD_USE_HISTORY=False 215 | ): 216 | self.client.post( 217 | reverse("account_password"), 218 | post_data 219 | ) 220 | # history count should be zero 221 | self.assertEqual(self.user.password_history.count(), 0) 222 | -------------------------------------------------------------------------------- /account/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.core.management import call_command 5 | from django.test import TestCase, override_settings 6 | 7 | from account.conf import settings 8 | from account.models import PasswordExpiry, PasswordHistory 9 | 10 | 11 | @override_settings( 12 | ACCOUNT_PASSWORD_EXPIRY=500 13 | ) 14 | class UserPasswordExpiryTests(TestCase): 15 | 16 | def setUp(self): 17 | self.UserModel = get_user_model() 18 | self.user = self.UserModel.objects.create_user(username="patrick") 19 | 20 | def test_set_explicit_password_expiry(self): 21 | """ 22 | Ensure specific password expiry is set. 23 | """ 24 | self.assertFalse(hasattr(self.user, "password_expiry")) 25 | expiration_period = 60 26 | out = StringIO() 27 | call_command( 28 | "user_password_expiry", 29 | "patrick", 30 | "--expire={}".format(expiration_period), 31 | stdout=out 32 | ) 33 | 34 | user = self.UserModel.objects.get(username="patrick") 35 | user_expiry = user.password_expiry 36 | self.assertEqual(user_expiry.expiry, expiration_period) 37 | self.assertIn('User "{}" password expiration set to {} seconds'.format( 38 | self.user.username, expiration_period), out.getvalue(), 39 | ) 40 | 41 | def test_set_default_password_expiry(self): 42 | """ 43 | Ensure default password expiry (from settings) is set. 44 | """ 45 | self.assertFalse(hasattr(self.user, "password_expiry")) 46 | out = StringIO() 47 | call_command( 48 | "user_password_expiry", 49 | "patrick", 50 | stdout=out 51 | ) 52 | 53 | user = self.UserModel.objects.get(username="patrick") 54 | user_expiry = user.password_expiry 55 | default_expiration = settings.ACCOUNT_PASSWORD_EXPIRY 56 | self.assertEqual(user_expiry.expiry, default_expiration) 57 | self.assertIn('User "{}" password expiration set to {} seconds'.format( 58 | self.user.username, default_expiration), out.getvalue(), 59 | ) 60 | 61 | def test_reset_existing_password_expiry(self): 62 | """ 63 | Ensure existing password expiry is reset. 64 | """ 65 | previous_expiry = 123 66 | existing_expiry = PasswordExpiry.objects.create(user=self.user, expiry=previous_expiry) 67 | out = StringIO() 68 | call_command( 69 | "user_password_expiry", 70 | "patrick", 71 | stdout=out 72 | ) 73 | 74 | user = self.UserModel.objects.get(username="patrick") 75 | user_expiry = user.password_expiry 76 | self.assertEqual(user_expiry, existing_expiry) 77 | default_expiration = settings.ACCOUNT_PASSWORD_EXPIRY 78 | self.assertEqual(user_expiry.expiry, default_expiration) 79 | self.assertNotEqual(user_expiry.expiry, previous_expiry) 80 | 81 | def test_bad_username(self): 82 | """ 83 | Ensure proper operation when username is not found. 84 | """ 85 | bad_username = "asldkfj" 86 | out = StringIO() 87 | call_command( 88 | "user_password_expiry", 89 | bad_username, 90 | stdout=out 91 | ) 92 | self.assertIn('User "{}" not found'.format(bad_username), out.getvalue()) 93 | 94 | 95 | class UserPasswordHistoryTests(TestCase): 96 | 97 | def setUp(self): 98 | self.UserModel = get_user_model() 99 | self.user = self.UserModel.objects.create_user(username="patrick") 100 | 101 | def test_set_history(self): 102 | """ 103 | Ensure password history is created. 104 | """ 105 | self.assertFalse(self.user.password_history.all()) 106 | password_age = 5 # days 107 | out = StringIO() 108 | call_command( 109 | "user_password_history", 110 | "--days={}".format(password_age), 111 | stdout=out 112 | ) 113 | 114 | user = self.UserModel.objects.get(username="patrick") 115 | password_history = user.password_history.all() 116 | self.assertEqual(password_history.count(), 1) 117 | self.assertIn("Password history set to ", out.getvalue()) 118 | self.assertIn("for {} users".format(1), out.getvalue()) 119 | 120 | def test_set_history_exists(self): 121 | """ 122 | Ensure password history is NOT created. 123 | """ 124 | PasswordHistory.objects.create(user=self.user) 125 | password_age = 5 # days 126 | out = StringIO() 127 | call_command( 128 | "user_password_history", 129 | "--days={}".format(password_age), 130 | stdout=out 131 | ) 132 | 133 | user = self.UserModel.objects.get(username="patrick") 134 | password_history = user.password_history.all() 135 | self.assertEqual(password_history.count(), 1) 136 | self.assertIn("No users found without password history", out.getvalue()) 137 | 138 | def test_set_history_one_exists(self): 139 | """ 140 | Ensure password history is created for users without existing history. 141 | """ 142 | another_user = self.UserModel.objects.create_user(username="james") 143 | PasswordHistory.objects.create(user=another_user) 144 | 145 | password_age = 5 # days 146 | out = StringIO() 147 | call_command( 148 | "user_password_history", 149 | "--days={}".format(password_age), 150 | stdout=out 151 | ) 152 | 153 | user = self.UserModel.objects.get(username="patrick") 154 | password_history = user.password_history.all() 155 | self.assertEqual(password_history.count(), 1) 156 | 157 | # verify user with existing history did not get another entry 158 | user = self.UserModel.objects.get(username="james") 159 | password_history = user.password_history.all() 160 | self.assertEqual(password_history.count(), 1) 161 | 162 | self.assertIn("Password history set to ", out.getvalue()) 163 | self.assertIn("for {} users".format(1), out.getvalue()) 164 | 165 | def test_set_history_force(self): 166 | """ 167 | Ensure specific password history is created for all users. 168 | """ 169 | another_user = self.UserModel.objects.create_user(username="james") 170 | PasswordHistory.objects.create(user=another_user) 171 | 172 | password_age = 5 # days 173 | out = StringIO() 174 | call_command( 175 | "user_password_history", 176 | "--days={}".format(password_age), 177 | "--force", 178 | stdout=out 179 | ) 180 | 181 | user = self.UserModel.objects.get(username="patrick") 182 | password_history = user.password_history.all() 183 | self.assertEqual(password_history.count(), 1) 184 | 185 | # verify user with existing history DID get another entry 186 | user = self.UserModel.objects.get(username="james") 187 | password_history = user.password_history.all() 188 | self.assertEqual(password_history.count(), 2) 189 | 190 | self.assertIn("Password history set to ", out.getvalue()) 191 | self.assertIn("for {} users".format(2), out.getvalue()) 192 | 193 | def test_set_history_multiple(self): 194 | """ 195 | Ensure password history is created for all users without existing history. 196 | """ 197 | self.UserModel.objects.create_user(username="second") 198 | self.UserModel.objects.create_user(username="third") 199 | 200 | password_age = 5 # days 201 | out = StringIO() 202 | call_command( 203 | "user_password_history", 204 | "--days={}".format(password_age), 205 | stdout=out 206 | ) 207 | 208 | user = self.UserModel.objects.get(username="patrick") 209 | password_history = user.password_history.all() 210 | self.assertEqual(password_history.count(), 1) 211 | first_timestamp = password_history[0].timestamp 212 | 213 | user = self.UserModel.objects.get(username="second") 214 | password_history = user.password_history.all() 215 | self.assertEqual(password_history.count(), 1) 216 | second_timestamp = password_history[0].timestamp 217 | self.assertEqual(first_timestamp, second_timestamp) 218 | 219 | user = self.UserModel.objects.get(username="third") 220 | password_history = user.password_history.all() 221 | self.assertEqual(password_history.count(), 1) 222 | third_timestamp = password_history[0].timestamp 223 | self.assertEqual(first_timestamp, third_timestamp) 224 | 225 | self.assertIn("Password history set to ", out.getvalue()) 226 | self.assertIn("for {} users".format(3), out.getvalue()) 227 | -------------------------------------------------------------------------------- /account/forms.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | 4 | from django import forms 5 | from django.contrib import auth 6 | from django.contrib.auth import get_user_model 7 | from django.utils.encoding import force_str 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from account.conf import settings 11 | from account.hooks import hookset 12 | from account.models import EmailAddress 13 | from account.utils import get_user_lookup_kwargs 14 | 15 | alnum_re = re.compile(r"^[\w\-\.\+]+$") 16 | 17 | User = get_user_model() 18 | USER_FIELD_MAX_LENGTH = getattr(User, User.USERNAME_FIELD).field.max_length 19 | 20 | 21 | class PasswordField(forms.CharField): 22 | 23 | def __init__(self, *args, **kwargs): 24 | kwargs.setdefault("widget", forms.PasswordInput(render_value=False)) 25 | self.strip = kwargs.pop("strip", True) 26 | super(PasswordField, self).__init__(*args, **kwargs) 27 | 28 | def to_python(self, value): 29 | if value in self.empty_values: 30 | return "" 31 | value = force_str(value) 32 | if self.strip: 33 | value = value.strip() 34 | return value 35 | 36 | 37 | class SignupForm(forms.Form): 38 | 39 | username = forms.CharField( 40 | label=_("Username"), 41 | max_length=USER_FIELD_MAX_LENGTH, 42 | widget=forms.TextInput(), 43 | required=True 44 | ) 45 | email = forms.EmailField( 46 | label=_("Email"), 47 | widget=forms.TextInput(), required=True 48 | ) 49 | password = PasswordField( 50 | label=_("Password"), 51 | strip=settings.ACCOUNT_PASSWORD_STRIP, 52 | ) 53 | password_confirm = PasswordField( 54 | label=_("Password (again)"), 55 | strip=settings.ACCOUNT_PASSWORD_STRIP, 56 | ) 57 | code = forms.CharField( 58 | max_length=64, 59 | required=False, 60 | widget=forms.HiddenInput() 61 | ) 62 | 63 | def clean_username(self): 64 | if not alnum_re.search(self.cleaned_data["username"]): 65 | raise forms.ValidationError( 66 | _("Usernames can only contain letters, numbers and the following special characters ./+/-/_") 67 | ) 68 | lookup_kwargs = get_user_lookup_kwargs({ 69 | "{username}__iexact": self.cleaned_data["username"] 70 | }) 71 | qs = User.objects.filter(**lookup_kwargs) 72 | if not qs.exists(): 73 | return self.cleaned_data["username"] 74 | raise forms.ValidationError(_("This username is already taken. Please choose another.")) 75 | 76 | def clean_email(self): 77 | value = self.cleaned_data["email"] 78 | qs = EmailAddress.objects.filter(email__iexact=value) 79 | if not qs.exists() or not settings.ACCOUNT_EMAIL_UNIQUE: 80 | return value 81 | raise forms.ValidationError(_("A user is registered with this email address.")) 82 | 83 | def clean(self): 84 | if ( 85 | "password" in self.cleaned_data and 86 | "password_confirm" in self.cleaned_data and 87 | self.cleaned_data["password"] != self.cleaned_data["password_confirm"] 88 | ): 89 | raise forms.ValidationError(_("You must type the same password each time.")) 90 | return self.cleaned_data 91 | 92 | 93 | class LoginForm(forms.Form): 94 | 95 | password = PasswordField( 96 | label=_("Password"), 97 | strip=settings.ACCOUNT_PASSWORD_STRIP, 98 | ) 99 | remember = forms.BooleanField( 100 | label=_("Remember Me"), 101 | required=False 102 | ) 103 | user = None 104 | 105 | def clean(self): 106 | if self._errors: 107 | return 108 | user = auth.authenticate(**self.user_credentials()) 109 | if user: 110 | if user.is_active: 111 | self.user = user 112 | else: 113 | raise forms.ValidationError(_("This account is inactive.")) 114 | else: 115 | raise forms.ValidationError(self.authentication_fail_message) 116 | return self.cleaned_data 117 | 118 | def user_credentials(self): 119 | return hookset.get_user_credentials(self, self.identifier_field) 120 | 121 | 122 | class LoginUsernameForm(LoginForm): 123 | 124 | username = forms.CharField(label=_("Username"), max_length=USER_FIELD_MAX_LENGTH) 125 | authentication_fail_message = _("The username and/or password you specified are not correct.") 126 | identifier_field = "username" 127 | 128 | def __init__(self, *args, **kwargs): 129 | super(LoginUsernameForm, self).__init__(*args, **kwargs) 130 | field_order = ["username", "password", "remember"] 131 | if hasattr(self.fields, "keyOrder"): 132 | self.fields.keyOrder = field_order 133 | else: 134 | self.fields = OrderedDict((k, self.fields[k]) for k in field_order) 135 | 136 | 137 | class LoginEmailForm(LoginForm): 138 | 139 | email = forms.EmailField(label=_("Email")) 140 | authentication_fail_message = _("The email address and/or password you specified are not correct.") 141 | identifier_field = "email" 142 | 143 | def __init__(self, *args, **kwargs): 144 | super(LoginEmailForm, self).__init__(*args, **kwargs) 145 | field_order = ["email", "password", "remember"] 146 | if hasattr(self.fields, "keyOrder"): 147 | self.fields.keyOrder = field_order 148 | else: 149 | self.fields = OrderedDict((k, self.fields[k]) for k in field_order) 150 | 151 | 152 | class ChangePasswordForm(forms.Form): 153 | 154 | password_current = forms.CharField( 155 | label=_("Current Password"), 156 | widget=forms.PasswordInput(render_value=False) 157 | ) 158 | password_new = forms.CharField( 159 | label=_("New Password"), 160 | widget=forms.PasswordInput(render_value=False) 161 | ) 162 | password_new_confirm = forms.CharField( 163 | label=_("New Password (again)"), 164 | widget=forms.PasswordInput(render_value=False) 165 | ) 166 | 167 | def __init__(self, *args, **kwargs): 168 | self.user = kwargs.pop("user") 169 | super(ChangePasswordForm, self).__init__(*args, **kwargs) 170 | 171 | def clean_password_current(self): 172 | if not self.user.check_password(self.cleaned_data.get("password_current")): 173 | raise forms.ValidationError(_("Please type your current password.")) 174 | return self.cleaned_data["password_current"] 175 | 176 | def clean_password_new_confirm(self): 177 | if "password_new" in self.cleaned_data and "password_new_confirm" in self.cleaned_data: 178 | password_new = self.cleaned_data["password_new"] 179 | password_new_confirm = self.cleaned_data["password_new_confirm"] 180 | return hookset.clean_password(password_new, password_new_confirm) 181 | return self.cleaned_data["password_new_confirm"] 182 | 183 | 184 | class PasswordResetForm(forms.Form): 185 | 186 | email = forms.EmailField(label=_("Email"), required=True) 187 | 188 | def clean_email(self): 189 | value = self.cleaned_data["email"] 190 | if not EmailAddress.objects.filter(email__iexact=value).exists(): 191 | raise forms.ValidationError(_("Email address can not be found.")) 192 | return value 193 | 194 | 195 | class PasswordResetTokenForm(forms.Form): 196 | 197 | password = forms.CharField( 198 | label=_("New Password"), 199 | widget=forms.PasswordInput(render_value=False) 200 | ) 201 | password_confirm = forms.CharField( 202 | label=_("New Password (again)"), 203 | widget=forms.PasswordInput(render_value=False) 204 | ) 205 | 206 | def clean_password_confirm(self): 207 | if "password" in self.cleaned_data and "password_confirm" in self.cleaned_data: 208 | password = self.cleaned_data["password"] 209 | password_confirm = self.cleaned_data["password_confirm"] 210 | return hookset.clean_password(password, password_confirm) 211 | return self.cleaned_data["password_confirm"] 212 | 213 | 214 | class SettingsForm(forms.Form): 215 | 216 | email = forms.EmailField(label=_("Email"), required=True) 217 | timezone = forms.ChoiceField( 218 | label=_("Timezone"), 219 | choices=[("", "---------")] + settings.ACCOUNT_TIMEZONES, 220 | required=False 221 | ) 222 | if settings.USE_I18N: 223 | language = forms.ChoiceField( 224 | label=_("Language"), 225 | choices=settings.ACCOUNT_LANGUAGES, 226 | required=False 227 | ) 228 | 229 | def clean_email(self): 230 | value = self.cleaned_data["email"] 231 | if self.initial.get("email") == value: 232 | return value 233 | qs = EmailAddress.objects.filter(email__iexact=value) 234 | if not qs.exists() or not settings.ACCOUNT_EMAIL_UNIQUE: 235 | return value 236 | raise forms.ValidationError(_("A user is registered with this email address.")) 237 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | .. _templates: 2 | 3 | ========= 4 | Templates 5 | ========= 6 | 7 | This document covers the implementation of django-user-accounts within Django 8 | templates. The `pinax-theme-bootstrap`_ package provides a good `starting point`_ 9 | to build from. Note, this document assumes you have read the installation docs. 10 | 11 | .. _pinax-theme-bootstrap: https://github.com/pinax/pinax-theme-bootstrap 12 | .. _starting point: https://github.com/pinax/pinax-theme-bootstrap/tree/master/pinax_theme_bootstrap/templates/account 13 | 14 | 15 | Template Files 16 | =============== 17 | 18 | By default, django-user-accounts expects the following templates. If you 19 | don't use ``pinax-theme-bootstrap``, then you will have to create these 20 | templates yourself. 21 | 22 | 23 | Login/Registration/Signup Templates 24 | ----------------------------------- 25 | 26 | ``account/login.html`` 27 | ~~~~~~~~~~~~~~~~~~~~~~ 28 | 29 | The template with the form to authenticate the user. The template has the 30 | following context: 31 | 32 | ``form`` 33 | The login form. 34 | 35 | ``redirect_field_name`` 36 | The name of the hidden field that will hold the url where to redirect the 37 | user after login. 38 | 39 | ``redirect_field_value`` 40 | The actual url where the user will be redirected after login. 41 | 42 | ``account/logout.html`` 43 | ~~~~~~~~~~~~~~~~~~~~~~~ 44 | 45 | The default template shown after the user has been logged out. 46 | 47 | ``account/signup.html`` 48 | ~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | The template with the form to registrate a new user. The template has the 51 | following context: 52 | 53 | ``form`` 54 | The form used to create the new user. 55 | 56 | ``redirect_field_name`` 57 | The name of the hidden field that will hold the url where to redirect the 58 | user after signing up. 59 | 60 | ``redirect_field_value`` 61 | The actual url where the user will be redirected after signing up. 62 | 63 | ``account/signup_closed.html`` 64 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | 66 | A template to inform the user that creating new users is not allowed (mainly 67 | because ``settings.ACCOUNT_OPEN_SIGNUP`` is ``False``). 68 | 69 | 70 | Registration Approval Templates 71 | ------------------------------- 72 | 73 | These templates are only used when ``settings.ACCOUNT_APPROVAL_REQUIRED`` is 74 | ``True``. 75 | 76 | ``account/admin_approval_sent.html`` 77 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 78 | 79 | The template shown after a new user has been created (with ``is_active`` set to 80 | ``False``). It should explain that an administrator will need to approve their 81 | registration before they can use it. The template has the following context: 82 | 83 | ``email`` 84 | The email address for the newly created user. 85 | 86 | ``success_url`` 87 | The URL where the user will be directed to. 88 | 89 | ``account/ajax/admin_approval_sent.html`` 90 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 91 | 92 | The same template as ``account/admin_approval_sent.html`` but for AJAX 93 | responses; is rendered with the same context. 94 | 95 | 96 | Email Confirmation Templates 97 | ---------------------------- 98 | 99 | ``account/email_confirm.html`` 100 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | 102 | A template to confirm an email address. The template has the following context: 103 | 104 | ``email`` 105 | The email address where the activation link has been sent. 106 | 107 | ``confirmation`` 108 | The EmailConfirmation instance to be confirmed. 109 | 110 | ``account/email_confirmation_sent.html`` 111 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 112 | 113 | The template shown after a new user has been created. It should tell the user 114 | that an activation link has been sent to his email address. The template has 115 | the following context: 116 | 117 | ``email`` 118 | The email address where the activation link has been sent. 119 | 120 | ``success_url`` 121 | A url where the user can be redirected from this page. For example to 122 | show a link to go back. 123 | 124 | ``account/email_confirmed.html`` 125 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 126 | 127 | A template shown after an email address has been confirmed. The template 128 | context is the same as in email_confirm.html. 129 | 130 | ``email`` 131 | The email address that has been confirmed. 132 | 133 | Password Management Templates 134 | ----------------------------- 135 | 136 | ``account/password_change.html`` 137 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 138 | 139 | The template that shows the form to change the user's password, when the user 140 | is authenticated. The template has the following context: 141 | 142 | ``form`` 143 | The form to change the password. 144 | 145 | ``account/password_reset.html`` 146 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 147 | 148 | A template with a form to type an email address to reset a user's password. 149 | The template has the following context: 150 | 151 | ``form`` 152 | The form to reset the password. 153 | 154 | ``account/password_reset_sent.html`` 155 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 156 | 157 | A template to inform the user that his password has been reset and that he 158 | should receive an email with a link to create a new password. The template has 159 | the following context: 160 | 161 | ``form`` 162 | An instance of ``PasswordResetForm``. Usually the fields of this form 163 | must be hidden. 164 | 165 | ``resend`` 166 | If ``True`` it means that the reset link has been resent to the user. 167 | 168 | ``account/password_reset_token.html`` 169 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 170 | 171 | The template that shows the form to change the user's password. The user should 172 | have come here following the link received to reset his password. The template 173 | has the following context: 174 | 175 | ``form`` 176 | The form to set the new password. 177 | 178 | ``account/password_reset_token_fail.html`` 179 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | A template to inform the user that he is not allowed to change the password, 182 | because the authentication token is wrong. The template has the following 183 | context: 184 | 185 | ``url`` 186 | The url to request a new reset token. 187 | 188 | 189 | Account Settings 190 | ---------------- 191 | 192 | ``account/settings.html`` 193 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 194 | 195 | A template with a form where the user may change his email address, time zone 196 | and preferred language. The template has the following context: 197 | 198 | ``form`` 199 | The form to change the settings. 200 | 201 | 202 | Emails (actual emails themselves) 203 | --------------------------------- 204 | 205 | ``account/email/email_confirmation_subject.txt`` 206 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 207 | 208 | The subject line of the email that will be sent to the new user to validate the 209 | email address. It will be rendered as a single line. The template has the 210 | following context: 211 | 212 | ``email_address`` 213 | The actual email address where the activation message will be sent. 214 | 215 | ``user`` 216 | The new user object. 217 | 218 | ``activate_url`` 219 | The complete url for account activation, including protocol and domain. 220 | 221 | ``current_site`` 222 | The domain name of the site. 223 | 224 | ``key`` 225 | The confirmation key. 226 | 227 | ``account/email/email_confirmation_message.txt`` 228 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 229 | 230 | The body of the activation email. It has the same context as the subject 231 | template (see above). 232 | 233 | ``account/email/invite_user.txt`` 234 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 235 | 236 | The body of the invitation sent to somebody to join the site. The template has 237 | the following context: 238 | 239 | ``signup_code`` 240 | An instance of account.models.SignupCode. 241 | 242 | ``current_site`` 243 | The instance of django.contrib.sites.models.Site that identifies the site. 244 | 245 | ``signup_url`` 246 | The link used to use the invitation and create a new account. 247 | 248 | ``account/email/invite_user_subject.txt`` 249 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 250 | 251 | The subject line of the invitation sent to somebody to join the site. The 252 | template has the same context as in invite_user.txt. 253 | 254 | ``account/email/password_change.txt`` 255 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 256 | 257 | The body of the email used to inform the user that his password has been 258 | changed. The template has the following context: 259 | 260 | ``user`` 261 | The user whom the password belongs to. 262 | 263 | ``protocol`` 264 | The application protocol (usually http or https) being used in the site. 265 | 266 | ``current_site`` 267 | The instance of django.contrib.sites.models.Site that identifies the site. 268 | 269 | ``account/email/password_change_subject.txt`` 270 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 271 | 272 | The subject line of the email used to inform the user that his password has 273 | been changed. The context is the same as in password_change.txt. 274 | 275 | ``account/email/password_reset.txt`` 276 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 277 | 278 | The body of the email with a link to reset a user's password. The template has 279 | the following context: 280 | 281 | ``user`` 282 | The user whom the password belongs to. 283 | 284 | ``current_site`` 285 | The instance of django.contrib.sites.models.Site that identifies the site. 286 | 287 | ``password_reset_url`` 288 | The link that the user needs to follow to set a new password. 289 | 290 | ``account/email/password_reset_subject.txt`` 291 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 292 | 293 | The subject line of the email with a link to reset a user's password. The 294 | context is the same as in password_reset.txt. 295 | 296 | 297 | Template Tags 298 | ============= 299 | 300 | To use the built in template tags you must first load them within the templates: 301 | 302 | .. code-block:: jinja 303 | 304 | {% load account_tags %} 305 | 306 | To display the current logged-in user: 307 | 308 | .. code-block:: jinja 309 | 310 | {% user_display request.user %} 311 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | ===== 4 | Usage 5 | ===== 6 | 7 | This document covers the usage of django-user-accounts. It assumes you've 8 | read :ref:`installation`. 9 | 10 | django-user-accounts has very good default behavior when handling user 11 | accounts. It has been designed to be customizable in many aspects. By default 12 | this app will: 13 | 14 | * enable username authentication 15 | * provide default views and forms for sign up, log in, password reset and 16 | account management 17 | * handle log out with POST 18 | * require unique email addresses globally 19 | * require email verification for performing password resets 20 | 21 | The rest of this document will cover how you can tweak the default behavior 22 | of django-user-accounts. 23 | 24 | 25 | Customizing the sign up process 26 | =============================== 27 | 28 | In many cases you need to tweak the sign up process to do some domain specific 29 | tasks. Perhaps you need to update a profile for the new user or something else. 30 | The built-in ``SignupView`` has hooks to enable just about any sort of 31 | customization during sign up. Here's an example of a custom ``SignupView`` 32 | defined in your project:: 33 | 34 | import account.views 35 | 36 | 37 | class SignupView(account.views.SignupView): 38 | 39 | def after_signup(self, form): 40 | self.update_profile(form) 41 | super(SignupView, self).after_signup(form) 42 | 43 | def update_profile(self, form): 44 | profile = self.created_user.profile # replace with your reverse one-to-one profile attribute only if you've defined a `related_name`. 45 | profile.some_attr = "some value" 46 | profile.save() 47 | 48 | 49 | This example assumes you had a receiver hooked up to the `post_save` signal for 50 | the sender, `User` like so:: 51 | 52 | from django.dispatch import receiver 53 | from django.db.models.signals import post_save 54 | 55 | from django.contrib.auth.models import User 56 | 57 | from mysite.profiles.models import UserProfile 58 | 59 | 60 | @receiver(post_save, sender=User) 61 | def handle_user_save(sender, instance, created, **kwargs): 62 | if created: 63 | UserProfile.objects.create(user=instance) 64 | 65 | 66 | You can define your own form class to add fields to the sign up process:: 67 | 68 | # forms.py 69 | 70 | from django import forms 71 | from django.forms.extras.widgets import SelectDateWidget 72 | 73 | import account.forms 74 | 75 | 76 | class SignupForm(account.forms.SignupForm): 77 | 78 | birthdate = forms.DateField(widget=SelectDateWidget(years=range(1910, 1991))) 79 | 80 | # views.py 81 | 82 | import account.views 83 | 84 | import myproject.forms 85 | 86 | 87 | class SignupView(account.views.SignupView): 88 | 89 | form_class = myproject.forms.SignupForm 90 | 91 | def after_signup(self, form): 92 | self.create_profile(form) 93 | super(SignupView, self).after_signup(form) 94 | 95 | def create_profile(self, form): 96 | profile = self.created_user.profile # replace with your reverse one-to-one profile attribute 97 | profile.birthdate = form.cleaned_data["birthdate"] 98 | profile.save() 99 | 100 | To hook this up for your project you need to override the URL for sign up:: 101 | 102 | from django.conf.urls import patterns, include, url 103 | 104 | import myproject.views 105 | 106 | 107 | urlpatterns = patterns("", 108 | url(r"^account/signup/$", myproject.views.SignupView.as_view(), name="account_signup"), 109 | url(r"^account/", include("account.urls")), 110 | ) 111 | 112 | .. note:: 113 | 114 | Make sure your ``url`` for ``/account/signup/`` comes *before* the 115 | ``include`` of ``account.urls``. Django will short-circuit on yours. 116 | 117 | Using email address for authentication 118 | ====================================== 119 | 120 | django-user-accounts allows you to use email addresses for authentication 121 | instead of usernames. You still have the option to continue using usernames 122 | or get rid of them entirely. 123 | 124 | To enable email authentication do the following: 125 | 126 | 1. check your settings for the following values:: 127 | 128 | ACCOUNT_EMAIL_UNIQUE = True 129 | ACCOUNT_EMAIL_CONFIRMATION_REQUIRED = True 130 | 131 | .. note:: 132 | 133 | If you need to change the value of ``ACCOUNT_EMAIL_UNIQUE`` make sure your 134 | database schema is modified to support a unique email column in 135 | ``account_emailaddress``. 136 | 137 | ``ACCOUNT_EMAIL_CONFIRMATION_REQUIRED`` is optional, but highly 138 | recommended to be ``True``. 139 | 140 | 2. define your own ``LoginView`` in your project:: 141 | 142 | import account.forms 143 | import account.views 144 | 145 | 146 | class LoginView(account.views.LoginView): 147 | 148 | form_class = account.forms.LoginEmailForm 149 | 150 | 3. ensure ``"account.auth_backends.EmailAuthenticationBackend"`` is in ``AUTHENTICATION_BACKENDS`` 151 | 152 | If you want to get rid of username you'll need to do some extra work: 153 | 154 | 1. define your own ``SignupForm`` and ``SignupView`` in your project:: 155 | 156 | # forms.py 157 | 158 | import account.forms 159 | 160 | 161 | class SignupForm(account.forms.SignupForm): 162 | 163 | def __init__(self, *args, **kwargs): 164 | super(SignupForm, self).__init__(*args, **kwargs) 165 | del self.fields["username"] 166 | 167 | # views.py 168 | 169 | import account.views 170 | 171 | import myproject.forms 172 | 173 | 174 | class SignupView(account.views.SignupView): 175 | 176 | form_class = myproject.forms.SignupForm 177 | identifier_field = 'email' 178 | 179 | def generate_username(self, form): 180 | # do something to generate a unique username (required by the 181 | # Django User model, unfortunately) 182 | username = "" 183 | return username 184 | 185 | 2. many places will rely on a username for a User instance. 186 | django-user-accounts provides a mechanism to add a level of indirection 187 | when representing the user in the user interface. Keep in mind not 188 | everything you include in your project will do what you expect when 189 | removing usernames entirely. 190 | 191 | Set ``ACCOUNT_USER_DISPLAY`` in settings to a callable suitable for your 192 | site:: 193 | 194 | ACCOUNT_USER_DISPLAY = lambda user: user.email 195 | 196 | Your Python code can use ``user_display`` to handle user representation:: 197 | 198 | from account.utils import user_display 199 | user_display(user) 200 | 201 | Your templates can use ``{% user_display request.user %}``:: 202 | 203 | {% load account_tags %} 204 | {% user_display request.user %} 205 | 206 | 207 | Allow non-unique email addresses 208 | ================================ 209 | 210 | If your site requires that you support non-unique email addresses globally 211 | you can tweak the behavior to allow this. 212 | 213 | Set ``ACCOUNT_EMAIL_UNIQUE`` to ``False``. If you have already setup the 214 | tables for django-user-accounts you will need to migrate the 215 | ``account_emailaddress`` table:: 216 | 217 | ALTER TABLE "account_emailaddress" ADD CONSTRAINT "account_emailaddress_user_id_email_key" UNIQUE ("user_id", "email"); 218 | ALTER TABLE "account_emailaddress" DROP CONSTRAINT "account_emailaddress_email_key"; 219 | 220 | ``ACCOUNT_EMAIL_UNIQUE = False`` will allow duplicate email addresses per 221 | user, but not across users. 222 | 223 | 224 | Including accounts in fixtures 225 | ============================== 226 | 227 | If you want to include account_account in your fixture, you may notice 228 | that when you load that fixture there is a conflict because 229 | django-user-accounts defaults to creating a new account for each new 230 | user. 231 | 232 | Example:: 233 | 234 | IntegrityError: Problem installing fixture \ 235 | ...'/app/fixtures/some_users_and_accounts.json': \ 236 | Could not load account.Account(pk=1): duplicate key value violates unique constraint \ 237 | "account_account_user_id_key" 238 | DETAIL: Key (user_id)=(1) already exists. 239 | 240 | To prevent this from happening, subclass DiscoverRunner and in 241 | setup_test_environment set CREATE_ON_SAVE to False. For example in a 242 | file called lib/tests.py:: 243 | 244 | from django.test.runner import DiscoverRunner 245 | from account.conf import AccountAppConf 246 | 247 | class MyTestDiscoverRunner(DiscoverRunner): 248 | 249 | def setup_test_environment(self, **kwargs): 250 | super(MyTestDiscoverRunner, self).setup_test_environment(**kwargs) 251 | aac = AccountAppConf() 252 | aac.CREATE_ON_SAVE = False 253 | 254 | 255 | And in your settings:: 256 | 257 | TEST_RUNNER = "lib.tests.MyTestDiscoverRunner" 258 | 259 | Restricting views to authenticated users 260 | ======================================== 261 | 262 | ``django.contrib.auth`` includes a convenient decorator and a mixin to restrict 263 | views to authenticated users. ``django-user-accounts`` includes a modified 264 | version of these decorator and mixin that should be used instead of the 265 | usual ones. 266 | 267 | If you want to restrict a function based view, use the decorator:: 268 | 269 | from account.decorators import login_required 270 | 271 | @login_required 272 | def restricted_view(request): 273 | pass 274 | 275 | To do the same with class based views, use the mixin:: 276 | 277 | from account.mixins import LoginRequiredMixin 278 | 279 | class RestrictedView(LoginRequiredMixin, View): 280 | pass 281 | 282 | 283 | Defining a custom password checker 284 | ================================== 285 | 286 | First add the path to the module which contains the 287 | `AccountDefaultHookSet` subclass to your settings:: 288 | 289 | ACCOUNT_HOOKSET = "scenemachine.hooks.AccountHookSet" 290 | 291 | Then define a custom `clean_password` method on the `AccountHookSet` 292 | class. 293 | 294 | Here is an example that harnesses the `VeryFacistCheck` dictionary 295 | checker from `cracklib`_.:: 296 | 297 | import cracklib 298 | 299 | from django import forms from django.conf import settings from 300 | django.template.defaultfilters import mark_safe from 301 | django.utils.translation import gettext_lazy as _ 302 | 303 | from account.hooks import AccountDefaultHookSet 304 | 305 | 306 | class AccountHookSet(AccountDefaultHookSet): 307 | 308 | def clean_password(self, password_new, password_new_confirm): 309 | password_new = super(AccountHookSet, self).clean_password(password_new, password_new_confirm) 310 | try: 311 | dictpath = "/usr/share/cracklib/pw_dict" 312 | if dictpath: 313 | cracklib.VeryFascistCheck(password_new, dictpath=dictpath) 314 | else: 315 | cracklib.VeryFascistCheck(password_new) 316 | return password_new 317 | except ValueError as e: 318 | message = _(unicode(e)) 319 | raise forms.ValidationError, mark_safe(message) 320 | return password_new 321 | 322 | 323 | .. _cracklib: https://pypi.python.org/pypi/cracklib/2.8.19 324 | --------------------------------------------------------------------------------