├── .bumpversion.cfg ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── django_password_validators ├── __init__.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fi │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── models.py ├── password_character_requirements │ ├── __init__.py │ └── password_validation.py ├── password_history │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── hashers.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20180424_1422.py │ │ ├── 0003_auto_20201206_1357.py │ │ └── __init__.py │ ├── models.py │ └── password_validation.py └── settings.py ├── setup.py ├── tests ├── __init__.py ├── manage.py └── test_project │ ├── __init__.py │ ├── settings.py │ ├── tests │ ├── __init__.py │ ├── base.py │ ├── test_character_requirements.py │ └── test_password_history.py │ ├── urls.py │ └── wsgi.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.7.3 3 | commit = True 4 | tag = True 5 | tag_name = v{new_version} 6 | message = New version: {current_version} → {new_version} [{now:%Y-%m-%d}] 7 | 8 | [bumpversion:file:django_password_validators/__init__.py] 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: 19 | - "3.7" 20 | - "3.8" 21 | - "3.9" 22 | - "3.10" 23 | os: 24 | - ubuntu-latest 25 | - macOS-latest 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Install tox 34 | run: pip install tox 35 | shell: bash 36 | - name: Run tests 37 | run: tox -f py$(echo ${{ matrix.python-version }} | tr -d .) 38 | shell: bash 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | eggs/ 13 | sdist/ 14 | tmp/ 15 | *.egg-info/ 16 | .installed.cfg 17 | *.egg 18 | 19 | # Installer logs 20 | pip-log.txt 21 | pip-delete-this-directory.txt 22 | 23 | # Unit test / coverage reports 24 | .tox/ 25 | .tmp/ 26 | ghostdriver.log 27 | 28 | # Editor configs 29 | .project 30 | .pydevproject 31 | .python-version 32 | .ruby-version 33 | .settings/ 34 | .idea/ 35 | 36 | ## generic files to ignore 37 | *~ 38 | *.lock 39 | 40 | *.sqlite3 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Wojciech Banaś and individual contributors. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of django-password-validators nor the names of its contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE.txt 2 | recursive-include django_password_validators/locale * 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test translatte new_version_major new_version_minor new_version_patch release 2 | 3 | test: 4 | tox 5 | 6 | translatte: 7 | cd ./django_password_validators; django-admin.py makemessages -a 8 | cd ./django_password_validators; django-admin.py compilemessages -f 9 | 10 | new_version_major: 11 | bump-my-version major 12 | 13 | new_version_minor: 14 | bump-my-version minor 15 | 16 | new_version_patch: 17 | bump-my-version patch 18 | 19 | release: 20 | rm -r dist/ 21 | python -m build 22 | twine check dist/* 23 | twine upload dist/* 24 | git push --tags 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Django Password Validators 3 | ========================== 4 | 5 | .. image:: https://github.com/fizista/django-password-validators/actions/workflows/ci.yml/badge.svg?branch=master 6 | :target: https://github.com/fizista/django-password-validators/actions/workflows/ci.yml?query=branch%3Amaster 7 | :alt: CI status 8 | 9 | Additional libraries for validating passwords in Django 3.2 or later. 10 | 11 | The application works well under python 4.x and 3.x versions. 12 | 13 | Django version after the number 1.9, allows you to configure password validation. 14 | Configuration validation is placed under the variable AUTH_PASSWORD_VALIDATORS_. 15 | 16 | 17 | Installation 18 | ============ 19 | 20 | Just install ``django-password-validators`` via ``pip``:: 21 | 22 | $ pip install django-password-validators 23 | 24 | 25 | Validators 26 | ========== 27 | 28 | ------------------------ 29 | UniquePasswordsValidator 30 | ------------------------ 31 | Validator checks if the password was once used by a particular user. 32 | If the password is used, then an exception is thrown, of course. 33 | 34 | For each user, all the passwords are stored in a database. 35 | All passwords are strongly encrypted. 36 | 37 | Configuration... 38 | 39 | In the file settings.py we add :: 40 | 41 | INSTALLED_APPS = [ 42 | ... 43 | 'django_password_validators', 44 | 'django_password_validators.password_history', 45 | ... 46 | ] 47 | 48 | AUTH_PASSWORD_VALIDATORS = [ 49 | ... 50 | { 51 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 52 | 'OPTIONS': { 53 | # How many recently entered passwords matter. 54 | # Passwords out of range are deleted. 55 | # Default: 0 - All passwords entered by the user. All password hashes are stored. 56 | 'last_passwords': 5 # Only the last 5 passwords entered by the user 57 | } 58 | }, 59 | ... 60 | ] 61 | 62 | # If you want, you can change the default hasher for the password history. 63 | # DPV_DEFAULT_HISTORY_HASHER = 'django_password_validators.password_history.hashers.HistoryHasher' 64 | 65 | And run :: 66 | 67 | python manage.py migrate 68 | 69 | -------------------------- 70 | PasswordCharacterValidator 71 | -------------------------- 72 | 73 | The validator checks for the minimum number of characters of a given type. 74 | 75 | In the file settings.py we add :: 76 | 77 | INSTALLED_APPS = [ 78 | ... 79 | 'django_password_validators', 80 | ... 81 | ] 82 | 83 | AUTH_PASSWORD_VALIDATORS = [ 84 | ... 85 | { 86 | 'NAME': 'django_password_validators.password_character_requirements.password_validation.PasswordCharacterValidator', 87 | 'OPTIONS': { 88 | 'min_length_digit': 1, 89 | 'min_length_alpha': 2, 90 | 'min_length_special': 3, 91 | 'min_length_lower': 4, 92 | 'min_length_upper': 5, 93 | 'special_characters': "~!@#$%^&*()_+{}\":;'[]" 94 | } 95 | }, 96 | ... 97 | ] 98 | 99 | 100 | .. _AUTH_PASSWORD_VALIDATORS: https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-AUTH_PASSWORD_VALIDATORS 101 | -------------------------------------------------------------------------------- /django_password_validators/__init__.py: -------------------------------------------------------------------------------- 1 | # Versions: 2 | # 3 | # X.Y.ZAB 4 | # X - Stable major version of the application, 5 | # Y - Minor release with additional features 6 | # Z - Version with bug fixes 7 | # A - Pre-release types: 8 | # "a" - alpha 9 | # "b" - betha 10 | # "c" - release candidate 11 | # B - Pre-release number 12 | # 13 | # X,Y,Z - range <0, inf) 14 | # B - range <1, inf) 15 | # 16 | # Examples: 17 | # 1.1 18 | # 0.3.5b1 19 | # 0.6c3 20 | __version__ = '1.7.3' 21 | -------------------------------------------------------------------------------- /django_password_validators/locale/ar/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/ar/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/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 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # 15/4/2021 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2023-12-10 18:48+0100\n" 15 | "PO-Revision-Date: 2021-04-07 09:11+0000\n" 16 | "Language: ar\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " 21 | "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" 22 | 23 | #: password_character_requirements/password_validation.py:31 24 | #, python-format 25 | msgid "This password must contain at least %(min_length)d digit." 26 | msgid_plural "This password must contain at least %(min_length)d digits." 27 | msgstr[0] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصرًا على الأقل." 28 | msgstr[1] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصرًا على الأقل." 29 | msgstr[2] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصرًا على الأقل." 30 | msgstr[3] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصرًا على الأقل." 31 | msgstr[4] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصرًا على الأقل." 32 | msgstr[5] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصرًا على الأقل." 33 | 34 | #: password_character_requirements/password_validation.py:41 35 | #, python-format 36 | msgid "This password must contain at least %(min_length)d letter." 37 | msgid_plural "This password must contain at least %(min_length)d letters." 38 | msgstr[0] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل." 39 | msgstr[1] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل." 40 | msgstr[2] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل." 41 | msgstr[3] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل." 42 | msgstr[4] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل." 43 | msgstr[5] "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل." 44 | 45 | #: password_character_requirements/password_validation.py:51 46 | #, python-format 47 | msgid "This password must contain at least %(min_length)d upper case letter." 48 | msgid_plural "" 49 | "This password must contain at least %(min_length)d upper case letters." 50 | msgstr[0] "" 51 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 52 | "الأحرف الكبيرة." 53 | msgstr[1] "" 54 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 55 | "الأحرف الكبيرة." 56 | msgstr[2] "" 57 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 58 | "الأحرف الكبيرة." 59 | msgstr[3] "" 60 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 61 | "الأحرف الكبيرة." 62 | msgstr[4] "" 63 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 64 | "الأحرف الكبيرة." 65 | msgstr[5] "" 66 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 67 | "الأحرف الكبيرة." 68 | 69 | #: password_character_requirements/password_validation.py:61 70 | #, python-format 71 | msgid "This password must contain at least %(min_length)d lower case letter." 72 | msgid_plural "" 73 | "This password must contain at least %(min_length)d lower case letters." 74 | msgstr[0] "" 75 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 76 | "الأحرف الصغيرة." 77 | msgstr[1] "" 78 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 79 | "الأحرف الصغيرة." 80 | msgstr[2] "" 81 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 82 | "الأحرف الصغيرة." 83 | msgstr[3] "" 84 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 85 | "الأحرف الصغيرة." 86 | msgstr[4] "" 87 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 88 | "الأحرف الصغيرة." 89 | msgstr[5] "" 90 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d حرفًا على الأقل في حالة " 91 | "الأحرف الصغيرة." 92 | 93 | #: password_character_requirements/password_validation.py:71 94 | #, python-format 95 | msgid "This password must contain at least %(min_length)d special character." 96 | msgid_plural "" 97 | "This password must contain at least %(min_length)d special characters." 98 | msgstr[0] "" 99 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصر على الأقل في الحالة " 100 | "الخاصة." 101 | msgstr[1] "" 102 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصر على الأقل في الحالة " 103 | "الخاصة." 104 | msgstr[2] "" 105 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصر على الأقل في الحالة " 106 | "الخاصة." 107 | msgstr[3] "" 108 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصر على الأقل في الحالة " 109 | "الخاصة." 110 | msgstr[4] "" 111 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصر على الأقل في الحالة " 112 | "الخاصة." 113 | msgstr[5] "" 114 | "يجب أن تحتوي كلمة المرور هذه على %(min_length)d عنصر على الأقل في الحالة " 115 | "الخاصة." 116 | 117 | #: password_character_requirements/password_validation.py:86 118 | #, python-format 119 | msgid "%(min_length)s letter" 120 | msgid_plural "%(min_length)s letters" 121 | msgstr[0] "%(min_length)s حرفاً" 122 | msgstr[1] "%(min_length)s حرفاً" 123 | msgstr[2] "%(min_length)s حرفاً" 124 | msgstr[3] "%(min_length)s حرفاً" 125 | msgstr[4] "%(min_length)s حرفاً" 126 | msgstr[5] "%(min_length)s حرفاً" 127 | 128 | #: password_character_requirements/password_validation.py:94 129 | #, python-format 130 | msgid "%(min_length)s digit" 131 | msgid_plural "%(min_length)s digits" 132 | msgstr[0] "%(min_length)s عنصرًا" 133 | msgstr[1] "%(min_length)s عنصرًا" 134 | msgstr[2] "%(min_length)s عنصرًا" 135 | msgstr[3] "%(min_length)s عنصرًا" 136 | msgstr[4] "%(min_length)s عنصرًا" 137 | msgstr[5] "%(min_length)s عنصرًا" 138 | 139 | #: password_character_requirements/password_validation.py:102 140 | #, python-format 141 | msgid "%(min_length)s lower case letter" 142 | msgid_plural "%(min_length)s lower case letters" 143 | msgstr[0] "%(min_length)s حرفًا في حالة الأحرف الصغيرة" 144 | msgstr[1] "%(min_length)s حرفًا في حالة الأحرف الصغيرة" 145 | msgstr[2] "%(min_length)s حرفًا في حالة الأحرف الصغيرة" 146 | msgstr[3] "%(min_length)s حرفًا في حالة الأحرف الصغيرة" 147 | msgstr[4] "%(min_length)s حرفًا في حالة الأحرف الصغيرة" 148 | msgstr[5] "%(min_length)s حرفًا في حالة الأحرف الصغيرة" 149 | 150 | #: password_character_requirements/password_validation.py:110 151 | #, python-format 152 | msgid "%(min_length)s upper case letter" 153 | msgid_plural "%(min_length)s upper case letters" 154 | msgstr[0] "%(min_length)s حرفًا في حالة الأحرف الكبيرة" 155 | msgstr[1] "%(min_length)s حرفًا في حالة الأحرف الكبيرة" 156 | msgstr[2] "%(min_length)s حرفًا في حالة الأحرف الكبيرة " 157 | msgstr[3] "%(min_length)s حرفًا في حالة الأحرف الكبيرة" 158 | msgstr[4] "%(min_length)s حرفًا في حالة الأحرف الكبيرة" 159 | msgstr[5] "%(min_length)s حرفًا في حالة الأحرف الكبيرة" 160 | 161 | #: password_character_requirements/password_validation.py:118 162 | #, python-format 163 | msgid "" 164 | "%(min_length_special)s special character, such as %(special_characters)s" 165 | msgid_plural "" 166 | "%(min_length_special)s special characters, such as %(special_characters)s" 167 | msgstr[0] "" 168 | "%(min_length_special)s عنصرًا في الحالة الخاصة مثل: %(special_characters)s" 169 | msgstr[1] "" 170 | "%(min_length_special)s عنصرًا في الحالة الخاصة مثل: %(special_characters)s" 171 | msgstr[2] "" 172 | "%(min_length_special)s عنصرًا في الحالة الخاصة مثل: %(special_characters)s" 173 | msgstr[3] "" 174 | "%(min_length_special)s عنصرًا في الحالة الخاصة مثل: %(special_characters)s" 175 | msgstr[4] "" 176 | "%(min_length_special)s عنصرًا في الحالة الخاصة مثل: %(special_characters)s" 177 | msgstr[5] "" 178 | "%(min_length_special)s عنصرًا في الحالة الخاصة مثل: %(special_characters)s" 179 | 180 | #: password_character_requirements/password_validation.py:123 181 | msgid "This password must contain at least" 182 | msgstr "يجب أن تحتوي كلمة المرور على" 183 | 184 | #: password_history/models.py:20 185 | msgid "When created salt" 186 | msgstr "عند تقديم مقترحات" 187 | 188 | #: password_history/models.py:25 189 | msgid "Salt for the user" 190 | msgstr "مقترح للمستخدم" 191 | 192 | #: password_history/models.py:30 193 | msgid "The number of iterations for hasher" 194 | msgstr "عدد تعديلات المشفرين" 195 | 196 | #: password_history/models.py:38 197 | msgid "Configuration" 198 | msgstr "تكوين" 199 | 200 | #: password_history/models.py:39 201 | msgid "Configurations" 202 | msgstr "تكوينات" 203 | 204 | #: password_history/models.py:78 205 | msgid "Password hash" 206 | msgstr "شفرة كلمة المرور" 207 | 208 | #: password_history/models.py:83 209 | msgid "Date" 210 | msgstr "التاريخ" 211 | 212 | #: password_history/password_validation.py:82 213 | msgid "" 214 | "You can not use a password that was already used in this application in the " 215 | "past." 216 | msgstr "لا يمكنك استخدام كلمة مرور قد تم استخدامها بالفعل مسبقا" 217 | 218 | #: password_history/password_validation.py:117 219 | #, python-format 220 | msgid "" 221 | "Your new password can not be identical to any of the %(old_pass)d previously " 222 | "entered passwords." 223 | msgid_plural "" 224 | "Your new password can not be identical to any of the %(old_pass)d previously " 225 | "entered passwords." 226 | msgstr[0] "" 227 | "لا يمكن أن تكون كلمة المرور الجديدة مطابقة لأية %(old_pass)d كلمة مرور سابقة." 228 | msgstr[1] "" 229 | "لا يمكن أن تكون كلمة المرور الجديدة مطابقة لأية %(old_pass)d كلمة مرور سابقة." 230 | msgstr[2] "" 231 | "لا يمكن أن تكون كلمة المرور الجديدة مطابقة لأية %(old_pass)d كلمة مرور سابقة." 232 | msgstr[3] "" 233 | "لا يمكن أن تكون كلمة المرور الجديدة مطابقة لأية %(old_pass)d كلمة مرور سابقة." 234 | msgstr[4] "" 235 | "لا يمكن أن تكون كلمة المرور الجديدة مطابقة لأية %(old_pass)d كلمة مرور سابقة." 236 | msgstr[5] "" 237 | "لا يمكن أن تكون كلمة المرور الجديدة مطابقة لأية %(old_pass)d كلمة مرور سابقة." 238 | 239 | #: password_history/password_validation.py:123 240 | msgid "" 241 | "Your new password can not be identical to any of the previously entered." 242 | msgstr "لايمكن أن تكون كلمة المرور الجديدة مطابقة لأية كلمة مرور سابقة." 243 | -------------------------------------------------------------------------------- /django_password_validators/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/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 | # 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: 2023-12-10 18:48+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Christian Schweinhardt \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 | #: password_character_requirements/password_validation.py:31 22 | #, python-format 23 | msgid "This password must contain at least %(min_length)d digit." 24 | msgid_plural "This password must contain at least %(min_length)d digits." 25 | msgstr[0] "Dieses Passwort muss mindestens eine Zahl enthalten." 26 | msgstr[1] "Dieses Passwort muss mindestens %(min_length)d Zahlen enthalten." 27 | 28 | #: password_character_requirements/password_validation.py:41 29 | #, python-format 30 | msgid "This password must contain at least %(min_length)d letter." 31 | msgid_plural "This password must contain at least %(min_length)d letters." 32 | msgstr[0] "Dieses Passwort muss mindestens einen Buchstaben enthalten." 33 | msgstr[1] "" 34 | "Dieses Passwort muss mindestens %(min_length)d Buchstaben enthalten." 35 | 36 | #: password_character_requirements/password_validation.py:51 37 | #, python-format 38 | msgid "This password must contain at least %(min_length)d upper case letter." 39 | msgid_plural "" 40 | "This password must contain at least %(min_length)d upper case letters." 41 | msgstr[0] "Dieses Passwort muss mindestens einen Großbuchstaben enthalten." 42 | msgstr[1] "" 43 | "Dieses Passwort muss mindestens %(min_length)d Großbuchstaben enthalten." 44 | 45 | #: password_character_requirements/password_validation.py:61 46 | #, python-format 47 | msgid "This password must contain at least %(min_length)d lower case letter." 48 | msgid_plural "" 49 | "This password must contain at least %(min_length)d lower case letters." 50 | msgstr[0] "Dieses Passwort muss mindestens einen Kleinbuchstaben enthalten." 51 | msgstr[1] "" 52 | "Dieses Passwort muss mindestens %(min_length)d Kleinbuchstaben enthalten." 53 | 54 | #: password_character_requirements/password_validation.py:71 55 | #, python-format 56 | msgid "This password must contain at least %(min_length)d special character." 57 | msgid_plural "" 58 | "This password must contain at least %(min_length)d special characters." 59 | msgstr[0] "" 60 | "Dieses Passwort muss mindestens %(min_length)d Sonderzeichen enthalten." 61 | msgstr[1] "" 62 | "Dieses Passwort muss mindestens %(min_length)d Sonderzeichen enthalten." 63 | 64 | #: password_character_requirements/password_validation.py:86 65 | #, python-format 66 | msgid "%(min_length)s letter" 67 | msgid_plural "%(min_length)s letters" 68 | msgstr[0] "%(min_length)s Buchstabe" 69 | msgstr[1] "%(min_length)s Buchstaben" 70 | 71 | #: password_character_requirements/password_validation.py:94 72 | #, python-format 73 | msgid "%(min_length)s digit" 74 | msgid_plural "%(min_length)s digits" 75 | msgstr[0] "%(min_length)s Zahl" 76 | msgstr[1] "%(min_length)s Zahlen" 77 | 78 | #: password_character_requirements/password_validation.py:102 79 | #, python-format 80 | msgid "%(min_length)s lower case letter" 81 | msgid_plural "%(min_length)s lower case letters" 82 | msgstr[0] "%(min_length)s Kleinbuchstabe" 83 | msgstr[1] "%(min_length)s Kleinbuchstaben" 84 | 85 | #: password_character_requirements/password_validation.py:110 86 | #, python-format 87 | msgid "%(min_length)s upper case letter" 88 | msgid_plural "%(min_length)s upper case letters" 89 | msgstr[0] "%(min_length)s Großbuchstabe" 90 | msgstr[1] "%(min_length)s Großbuchstaben" 91 | 92 | #: password_character_requirements/password_validation.py:118 93 | #, python-format 94 | msgid "" 95 | "%(min_length_special)s special character, such as %(special_characters)s" 96 | msgid_plural "" 97 | "%(min_length_special)s special characters, such as %(special_characters)s" 98 | msgstr[0] "" 99 | "%(min_length_special)s Sonderzeichen, wie zum Beispiel %(special_characters)s" 100 | msgstr[1] "" 101 | "%(min_length_special)s Sonderzeichen, wie zum Beispiel %(special_characters)s" 102 | 103 | #: password_character_requirements/password_validation.py:123 104 | msgid "This password must contain at least" 105 | msgstr "Dieses Passwort muss mindestens enthalten" 106 | 107 | #: password_history/models.py:20 108 | msgid "When created salt" 109 | msgstr "" 110 | 111 | #: password_history/models.py:25 112 | msgid "Salt for the user" 113 | msgstr "" 114 | 115 | #: password_history/models.py:30 116 | msgid "The number of iterations for hasher" 117 | msgstr "Die Anzahl der Iterationen für Hasher" 118 | 119 | #: password_history/models.py:38 120 | msgid "Configuration" 121 | msgstr "Konfiguration" 122 | 123 | #: password_history/models.py:39 124 | msgid "Configurations" 125 | msgstr "Konfigurationen" 126 | 127 | #: password_history/models.py:78 128 | msgid "Password hash" 129 | msgstr "Passwort-Hash" 130 | 131 | #: password_history/models.py:83 132 | msgid "Date" 133 | msgstr "Datum" 134 | 135 | #: password_history/password_validation.py:82 136 | msgid "" 137 | "You can not use a password that was already used in this application in the " 138 | "past." 139 | msgstr "" 140 | "Sie können kein Passwort verwenden, das bereits in dieser Anwendung " 141 | "verwendet wird." 142 | 143 | #: password_history/password_validation.py:117 144 | #, python-format 145 | msgid "" 146 | "Your new password can not be identical to any of the %(old_pass)d previously " 147 | "entered passwords." 148 | msgid_plural "" 149 | "Your new password can not be identical to any of the %(old_pass)d previously " 150 | "entered passwords." 151 | msgstr[0] "" 152 | "Ihr neues Passwort darf nicht mit dem zuvor eingegebenen Passwort identisch " 153 | "sein." 154 | msgstr[1] "" 155 | "Ihr neues Passwort darf mit keinem der %(old_pass)d zuvor eingegebenen " 156 | "Passwörter identisch sein." 157 | 158 | #: password_history/password_validation.py:123 159 | msgid "" 160 | "Your new password can not be identical to any of the previously entered." 161 | msgstr "" 162 | "Ihr neues Passwort darf mit keinem der zuvor eingegebenen Passwörter " 163 | "identisch sein." 164 | -------------------------------------------------------------------------------- /django_password_validators/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/locale/en/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: 2023-12-10 18:48+0100\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 | 20 | #: password_character_requirements/password_validation.py:31 21 | #, python-format 22 | msgid "This password must contain at least %(min_length)d digit." 23 | msgid_plural "This password must contain at least %(min_length)d digits." 24 | msgstr[0] "" 25 | msgstr[1] "" 26 | 27 | #: password_character_requirements/password_validation.py:41 28 | #, python-format 29 | msgid "This password must contain at least %(min_length)d letter." 30 | msgid_plural "This password must contain at least %(min_length)d letters." 31 | msgstr[0] "" 32 | msgstr[1] "" 33 | 34 | #: password_character_requirements/password_validation.py:51 35 | #, python-format 36 | msgid "This password must contain at least %(min_length)d upper case letter." 37 | msgid_plural "" 38 | "This password must contain at least %(min_length)d upper case letters." 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: password_character_requirements/password_validation.py:61 43 | #, python-format 44 | msgid "This password must contain at least %(min_length)d lower case letter." 45 | msgid_plural "" 46 | "This password must contain at least %(min_length)d lower case letters." 47 | msgstr[0] "" 48 | msgstr[1] "" 49 | 50 | #: password_character_requirements/password_validation.py:71 51 | #, python-format 52 | msgid "This password must contain at least %(min_length)d special character." 53 | msgid_plural "" 54 | "This password must contain at least %(min_length)d special characters." 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | #: password_character_requirements/password_validation.py:86 59 | #, python-format 60 | msgid "%(min_length)s letter" 61 | msgid_plural "%(min_length)s letters" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | #: password_character_requirements/password_validation.py:94 66 | #, python-format 67 | msgid "%(min_length)s digit" 68 | msgid_plural "%(min_length)s digits" 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | #: password_character_requirements/password_validation.py:102 73 | #, python-format 74 | msgid "%(min_length)s lower case letter" 75 | msgid_plural "%(min_length)s lower case letters" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | #: password_character_requirements/password_validation.py:110 80 | #, python-format 81 | msgid "%(min_length)s upper case letter" 82 | msgid_plural "%(min_length)s upper case letters" 83 | msgstr[0] "" 84 | msgstr[1] "" 85 | 86 | #: password_character_requirements/password_validation.py:118 87 | #, python-format 88 | msgid "" 89 | "%(min_length_special)s special character, such as %(special_characters)s" 90 | msgid_plural "" 91 | "%(min_length_special)s special characters, such as %(special_characters)s" 92 | msgstr[0] "" 93 | msgstr[1] "" 94 | 95 | #: password_character_requirements/password_validation.py:123 96 | msgid "This password must contain at least" 97 | msgstr "Your password must contain at least" 98 | 99 | #: password_history/models.py:20 100 | msgid "When created salt" 101 | msgstr "" 102 | 103 | #: password_history/models.py:25 104 | msgid "Salt for the user" 105 | msgstr "" 106 | 107 | #: password_history/models.py:30 108 | msgid "The number of iterations for hasher" 109 | msgstr "" 110 | 111 | #: password_history/models.py:38 112 | msgid "Configuration" 113 | msgstr "" 114 | 115 | #: password_history/models.py:39 116 | msgid "Configurations" 117 | msgstr "" 118 | 119 | #: password_history/models.py:78 120 | msgid "Password hash" 121 | msgstr "" 122 | 123 | #: password_history/models.py:83 124 | msgid "Date" 125 | msgstr "" 126 | 127 | #: password_history/password_validation.py:82 128 | msgid "" 129 | "You can not use a password that was already used in this application in the " 130 | "past." 131 | msgstr "" 132 | 133 | #: password_history/password_validation.py:117 134 | #, python-format 135 | msgid "" 136 | "Your new password can not be identical to any of the %(old_pass)d previously " 137 | "entered passwords." 138 | msgid_plural "" 139 | "Your new password can not be identical to any of the %(old_pass)d previously " 140 | "entered passwords." 141 | msgstr[0] "Your password can't be the same as the last password you used." 142 | msgstr[1] "" 143 | "Your password can't be identical to any of the %(old_pass)d previously used " 144 | "passwords." 145 | 146 | #: password_history/password_validation.py:123 147 | msgid "" 148 | "Your new password can not be identical to any of the previously entered." 149 | msgstr "" 150 | -------------------------------------------------------------------------------- /django_password_validators/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/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 | # 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: 2023-12-10 18:48+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Yamil Jaskolowski \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 | #: password_character_requirements/password_validation.py:31 22 | #, python-format 23 | msgid "This password must contain at least %(min_length)d digit." 24 | msgid_plural "This password must contain at least %(min_length)d digits." 25 | msgstr[0] "La contraseña debe tener al menos %(min_length)d dígito." 26 | msgstr[1] "La contraseña debe tener al menos %(min_length)d dígitos." 27 | 28 | #: password_character_requirements/password_validation.py:41 29 | #, python-format 30 | msgid "This password must contain at least %(min_length)d letter." 31 | msgid_plural "This password must contain at least %(min_length)d letters." 32 | msgstr[0] "La contraseña debe tener al menos %(min_length)d letra." 33 | msgstr[1] "La contraseña debe tener al menos %(min_length)d letras." 34 | 35 | #: password_character_requirements/password_validation.py:51 36 | #, python-format 37 | msgid "This password must contain at least %(min_length)d upper case letter." 38 | msgid_plural "" 39 | "This password must contain at least %(min_length)d upper case letters." 40 | msgstr[0] "La contraseña debe tener al menos %(min_length)d mayúscula." 41 | msgstr[1] "La contraseña debe tener al menos %(min_length)d mayúsculas." 42 | 43 | #: password_character_requirements/password_validation.py:61 44 | #, python-format 45 | msgid "This password must contain at least %(min_length)d lower case letter." 46 | msgid_plural "" 47 | "This password must contain at least %(min_length)d lower case letters." 48 | msgstr[0] "La contraseña debe tener al menos %(min_length)d minúscula." 49 | msgstr[1] "La contraseña debe tener al menos %(min_length)d minúsculas." 50 | 51 | #: password_character_requirements/password_validation.py:71 52 | #, python-format 53 | msgid "This password must contain at least %(min_length)d special character." 54 | msgid_plural "" 55 | "This password must contain at least %(min_length)d special characters." 56 | msgstr[0] "La contraseña debe tener al menos %(min_length)d caracter especial." 57 | msgstr[1] "" 58 | "La contraseña debe tener al menos %(min_length)d caracteres especiales." 59 | 60 | #: password_character_requirements/password_validation.py:86 61 | #, python-format 62 | msgid "%(min_length)s letter" 63 | msgid_plural "%(min_length)s letters" 64 | msgstr[0] "%(min_length)s letra" 65 | msgstr[1] "%(min_length)s letras" 66 | 67 | #: password_character_requirements/password_validation.py:94 68 | #, python-format 69 | msgid "%(min_length)s digit" 70 | msgid_plural "%(min_length)s digits" 71 | msgstr[0] "%(min_length)s dígito" 72 | msgstr[1] "%(min_length)s dígitos" 73 | 74 | #: password_character_requirements/password_validation.py:102 75 | #, python-format 76 | msgid "%(min_length)s lower case letter" 77 | msgid_plural "%(min_length)s lower case letters" 78 | msgstr[0] "%(min_length)s letra minúscula" 79 | msgstr[1] "%(min_length)s letras minúsculas" 80 | 81 | #: password_character_requirements/password_validation.py:110 82 | #, python-format 83 | msgid "%(min_length)s upper case letter" 84 | msgid_plural "%(min_length)s upper case letters" 85 | msgstr[0] "%(min_length)s letra mayúscula" 86 | msgstr[1] "%(min_length)s letras mayúsculas" 87 | 88 | #: password_character_requirements/password_validation.py:118 89 | #, python-format 90 | msgid "" 91 | "%(min_length_special)s special character, such as %(special_characters)s" 92 | msgid_plural "" 93 | "%(min_length_special)s special characters, such as %(special_characters)s" 94 | msgstr[0] "" 95 | "%(min_length_special)s caracter especial, como %(special_characters)s" 96 | msgstr[1] "" 97 | "%(min_length_special)s caracteres especiales, como %(special_characters)s" 98 | 99 | #: password_character_requirements/password_validation.py:123 100 | msgid "This password must contain at least" 101 | msgstr "La contraseña debe tener al menos" 102 | 103 | #: password_history/models.py:20 104 | msgid "When created salt" 105 | msgstr "Cuándo se creó el salt" 106 | 107 | #: password_history/models.py:25 108 | msgid "Salt for the user" 109 | msgstr "Salt del usuario" 110 | 111 | #: password_history/models.py:30 112 | msgid "The number of iterations for hasher" 113 | msgstr "Número de iteraciones para el hasher" 114 | 115 | #: password_history/models.py:38 116 | msgid "Configuration" 117 | msgstr "Configuración" 118 | 119 | #: password_history/models.py:39 120 | msgid "Configurations" 121 | msgstr "Configuraciones" 122 | 123 | #: password_history/models.py:78 124 | msgid "Password hash" 125 | msgstr "Hash de contraseña" 126 | 127 | #: password_history/models.py:83 128 | msgid "Date" 129 | msgstr "Fecha" 130 | 131 | #: password_history/password_validation.py:82 132 | msgid "" 133 | "You can not use a password that was already used in this application in the " 134 | "past." 135 | msgstr "No puedes usar una contraseña que fue utilizada en esta aplicación." 136 | 137 | #: password_history/password_validation.py:117 138 | #, fuzzy, python-format 139 | #| msgid "" 140 | #| "Your new password can not be identical to any of the previously entered." 141 | msgid "" 142 | "Your new password can not be identical to any of the %(old_pass)d previously " 143 | "entered passwords." 144 | msgid_plural "" 145 | "Your new password can not be identical to any of the %(old_pass)d previously " 146 | "entered passwords." 147 | msgstr[0] "" 148 | "Su nueva contraseña no puede ser la misma que la última contraseña ingresada." 149 | msgstr[1] "" 150 | "Su nueva contraseña no puede ser idéntica a ninguna de las %(old_pass)d " 151 | "contraseñas ingresadas anteriormente." 152 | 153 | #: password_history/password_validation.py:123 154 | msgid "" 155 | "Your new password can not be identical to any of the previously entered." 156 | msgstr "" 157 | "La nueva contraseña no puede ser identica a alguna utilizada anteriormente." 158 | -------------------------------------------------------------------------------- /django_password_validators/locale/fi/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/fi/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/locale/fi/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: 2023-12-10 18:48+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Ville Skyttä \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: fi\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 | #: password_character_requirements/password_validation.py:31 22 | #, python-format 23 | msgid "This password must contain at least %(min_length)d digit." 24 | msgid_plural "This password must contain at least %(min_length)d digits." 25 | msgstr[0] "Tämän salasanan tulee sisältää ainakin %(min_length)d numero." 26 | msgstr[1] "Tämän salasanan tulee sisältää ainakin %(min_length)d numeroa." 27 | 28 | #: password_character_requirements/password_validation.py:41 29 | #, python-format 30 | msgid "This password must contain at least %(min_length)d letter." 31 | msgid_plural "This password must contain at least %(min_length)d letters." 32 | msgstr[0] "Tämän salasanan tulee sisältää ainakin %(min_length)d kirjain." 33 | msgstr[1] "Tämän salasanan tulee sisältää ainakin %(min_length)d kirjainta." 34 | 35 | #: password_character_requirements/password_validation.py:51 36 | #, python-format 37 | msgid "This password must contain at least %(min_length)d upper case letter." 38 | msgid_plural "" 39 | "This password must contain at least %(min_length)d upper case letters." 40 | msgstr[0] "Tämän salasanan tulee sisältää ainakin %(min_length)d iso kirjain." 41 | msgstr[1] "" 42 | "Tämän salasanan tulee sisältää ainakin %(min_length)d isoa kirjainta." 43 | 44 | #: password_character_requirements/password_validation.py:61 45 | #, python-format 46 | msgid "This password must contain at least %(min_length)d lower case letter." 47 | msgid_plural "" 48 | "This password must contain at least %(min_length)d lower case letters." 49 | msgstr[0] "" 50 | "Tämän salasanan tulee sisältää ainakin %(min_length)d pieni kirjain." 51 | msgstr[1] "" 52 | "Tämän salasanan tulee sisältää ainakin %(min_length)d pientä kirjainta." 53 | 54 | #: password_character_requirements/password_validation.py:71 55 | #, python-format 56 | msgid "This password must contain at least %(min_length)d special character." 57 | msgid_plural "" 58 | "This password must contain at least %(min_length)d special characters." 59 | msgstr[0] "" 60 | "Tämän salasanan tulee sisältää ainakin %(min_length)d erikoismerkki." 61 | msgstr[1] "" 62 | "Tämän salasanan tulee sisältää ainakin %(min_length)d erikoismerkkiä." 63 | 64 | #: password_character_requirements/password_validation.py:86 65 | #, python-format 66 | msgid "%(min_length)s letter" 67 | msgid_plural "%(min_length)s letters" 68 | msgstr[0] "%(min_length)s kirjain" 69 | msgstr[1] "%(min_length)s kirjainta" 70 | 71 | #: password_character_requirements/password_validation.py:94 72 | #, python-format 73 | msgid "%(min_length)s digit" 74 | msgid_plural "%(min_length)s digits" 75 | msgstr[0] "%(min_length)s numero" 76 | msgstr[1] "%(min_length)s numeroa" 77 | 78 | #: password_character_requirements/password_validation.py:102 79 | #, python-format 80 | msgid "%(min_length)s lower case letter" 81 | msgid_plural "%(min_length)s lower case letters" 82 | msgstr[0] "%(min_length)s pieni kirjain" 83 | msgstr[1] "%(min_length)s pientä kirjainta" 84 | 85 | #: password_character_requirements/password_validation.py:110 86 | #, python-format 87 | msgid "%(min_length)s upper case letter" 88 | msgid_plural "%(min_length)s upper case letters" 89 | msgstr[0] "%(min_length)s iso kirjain" 90 | msgstr[1] "%(min_length)s isoa kirjainta" 91 | 92 | #: password_character_requirements/password_validation.py:118 93 | #, python-format 94 | msgid "" 95 | "%(min_length_special)s special character, such as %(special_characters)s" 96 | msgid_plural "" 97 | "%(min_length_special)s special characters, such as %(special_characters)s" 98 | msgstr[0] "%(min_length_special)s erikoismerkki, kuten %(special_characters)s" 99 | msgstr[1] "%(min_length_special)s erikoismerkkiä, kuten %(special_characters)s" 100 | 101 | #: password_character_requirements/password_validation.py:123 102 | msgid "This password must contain at least" 103 | msgstr "Tämän salasanan tulee sisältää ainakin" 104 | 105 | #: password_history/models.py:20 106 | msgid "When created salt" 107 | msgstr "Suolan päiväys" 108 | 109 | #: password_history/models.py:25 110 | msgid "Salt for the user" 111 | msgstr "Käyttäjän suola" 112 | 113 | #: password_history/models.py:30 114 | msgid "The number of iterations for hasher" 115 | msgstr "Tiivisteen iteraatioiden määrä" 116 | 117 | #: password_history/models.py:38 118 | msgid "Configuration" 119 | msgstr "Asetus" 120 | 121 | #: password_history/models.py:39 122 | msgid "Configurations" 123 | msgstr "Asetukset" 124 | 125 | #: password_history/models.py:78 126 | msgid "Password hash" 127 | msgstr "Salasanan tiiviste" 128 | 129 | #: password_history/models.py:83 130 | msgid "Date" 131 | msgstr "Päiväys" 132 | 133 | #: password_history/password_validation.py:82 134 | msgid "" 135 | "You can not use a password that was already used in this application in the " 136 | "past." 137 | msgstr "Et voi käyttää aiemmin käyttämääsi salasanaa tässä sovelluksessa." 138 | 139 | #: password_history/password_validation.py:117 140 | #, fuzzy, python-format 141 | #| msgid "" 142 | #| "Your new password can not be identical to any of the previously entered." 143 | msgid "" 144 | "Your new password can not be identical to any of the %(old_pass)d previously " 145 | "entered passwords." 146 | msgid_plural "" 147 | "Your new password can not be identical to any of the %(old_pass)d previously " 148 | "entered passwords." 149 | msgstr[0] "Uusi salasanasi ei voi olla sama kuin viimeksi syötetty salasana." 150 | msgstr[1] "" 151 | "Uusi salasanasi ei voi olla identtinen minkään %(old_pass)d aiemmin syötetyn " 152 | "salasanan kanssa." 153 | 154 | #: password_history/password_validation.py:123 155 | msgid "" 156 | "Your new password can not be identical to any of the previously entered." 157 | msgstr "Uusi salasanasi ei voi olla sama kuin mikään aiemmin käyttämäsi." 158 | -------------------------------------------------------------------------------- /django_password_validators/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/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 | # 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: 2023-12-10 18:48+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Aurélien WERNER \n" 14 | "Language-Team: LANGUAGE \n" 15 | "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 | 20 | #: password_character_requirements/password_validation.py:31 21 | #, python-format 22 | msgid "This password must contain at least %(min_length)d digit." 23 | msgid_plural "This password must contain at least %(min_length)d digits." 24 | msgstr[0] "Le mot de passe doit contenir au moins %(min_length)d chiffre." 25 | msgstr[1] "Le mot de passe doit contenir au moins %(min_length)d chiffres." 26 | 27 | #: password_character_requirements/password_validation.py:41 28 | #, python-format 29 | msgid "This password must contain at least %(min_length)d letter." 30 | msgid_plural "This password must contain at least %(min_length)d letters." 31 | msgstr[0] "Le mot de passe doit contenir au moins %(min_length)d lettre." 32 | msgstr[1] "Le mot de passe doit contenir au moins %(min_length)d lettres." 33 | 34 | #: password_character_requirements/password_validation.py:51 35 | #, python-format 36 | msgid "This password must contain at least %(min_length)d upper case letter." 37 | msgid_plural "" 38 | "This password must contain at least %(min_length)d upper case letters." 39 | msgstr[0] "Le mot de passe doit contenir au moins %(min_length)d majuscule." 40 | msgstr[1] "Le mot de passe doit contenir au moins %(min_length)d majuscules." 41 | 42 | #: password_character_requirements/password_validation.py:61 43 | #, python-format 44 | msgid "This password must contain at least %(min_length)d lower case letter." 45 | msgid_plural "" 46 | "This password must contain at least %(min_length)d lower case letters." 47 | msgstr[0] "Le mot de passe doit contenir au moins %(min_length)d minuscule." 48 | msgstr[1] "Le mot de passe doit contenir au moins %(min_length)d minuscules." 49 | 50 | #: password_character_requirements/password_validation.py:71 51 | #, python-format 52 | msgid "This password must contain at least %(min_length)d special character." 53 | msgid_plural "" 54 | "This password must contain at least %(min_length)d special characters." 55 | msgstr[0] "" 56 | "Le mot de passe doit contenir au moins %(min_length)d caractère spécial." 57 | msgstr[1] "" 58 | "Le mot de passe doit contenir au moins %(min_length)d caractères spéciaux." 59 | 60 | #: password_character_requirements/password_validation.py:86 61 | #, python-format 62 | msgid "%(min_length)s letter" 63 | msgid_plural "%(min_length)s letters" 64 | msgstr[0] "%(min_length)s lettre" 65 | msgstr[1] "%(min_length)s lettres" 66 | 67 | #: password_character_requirements/password_validation.py:94 68 | #, python-format 69 | msgid "%(min_length)s digit" 70 | msgid_plural "%(min_length)s digits" 71 | msgstr[0] "%(min_length)s chiffre" 72 | msgstr[1] "%(min_length)s chiffres" 73 | 74 | #: password_character_requirements/password_validation.py:102 75 | #, python-format 76 | msgid "%(min_length)s lower case letter" 77 | msgid_plural "%(min_length)s lower case letters" 78 | msgstr[0] "%(min_length)s minuscule" 79 | msgstr[1] "%(min_length)s minuscules" 80 | 81 | #: password_character_requirements/password_validation.py:110 82 | #, python-format 83 | msgid "%(min_length)s upper case letter" 84 | msgid_plural "%(min_length)s upper case letters" 85 | msgstr[0] "%(min_length)s majuscule" 86 | msgstr[1] "%(min_length)s majuscules" 87 | 88 | #: password_character_requirements/password_validation.py:118 89 | #, python-format 90 | msgid "" 91 | "%(min_length_special)s special character, such as %(special_characters)s" 92 | msgid_plural "" 93 | "%(min_length_special)s special characters, such as %(special_characters)s" 94 | msgstr[0] "" 95 | "%(min_length_special)s caractère spécial, comme %(special_characters)s" 96 | msgstr[1] "" 97 | "%(min_length_special)s caractères spéciaux, comme %(special_characters)s" 98 | 99 | #: password_character_requirements/password_validation.py:123 100 | msgid "This password must contain at least" 101 | msgstr "Le mot de passe doit contenir" 102 | 103 | #: password_history/models.py:20 104 | msgid "When created salt" 105 | msgstr "Lors de la création du sel" 106 | 107 | #: password_history/models.py:25 108 | msgid "Salt for the user" 109 | msgstr "Sel pour l'utilisateur" 110 | 111 | #: password_history/models.py:30 112 | msgid "The number of iterations for hasher" 113 | msgstr "Le nombre d'itérations pour le hash" 114 | 115 | #: password_history/models.py:38 116 | msgid "Configuration" 117 | msgstr "Configuration" 118 | 119 | #: password_history/models.py:39 120 | msgid "Configurations" 121 | msgstr "Configurations" 122 | 123 | #: password_history/models.py:78 124 | msgid "Password hash" 125 | msgstr "Hash de mot de passe" 126 | 127 | #: password_history/models.py:83 128 | msgid "Date" 129 | msgstr "Date" 130 | 131 | #: password_history/password_validation.py:82 132 | msgid "" 133 | "You can not use a password that was already used in this application in the " 134 | "past." 135 | msgstr "" 136 | "Vous ne pouvez pas utiliser un mot de passe déjà utilisé par l'application." 137 | 138 | #: password_history/password_validation.py:117 139 | #, fuzzy, python-format 140 | #| msgid "" 141 | #| "Your new password can not be identical to any of the previously entered." 142 | msgid "" 143 | "Your new password can not be identical to any of the %(old_pass)d previously " 144 | "entered passwords." 145 | msgid_plural "" 146 | "Your new password can not be identical to any of the %(old_pass)d previously " 147 | "entered passwords." 148 | msgstr[0] "" 149 | "Votre nouveau mot de passe ne peut pas être le même que le dernier mot de " 150 | "passe entré." 151 | msgstr[1] "" 152 | "Votre nouveau mot de passe ne peut être identique à aucun des %(old_pass)d " 153 | "saisis précédemment." 154 | 155 | #: password_history/password_validation.py:123 156 | msgid "" 157 | "Your new password can not be identical to any of the previously entered." 158 | msgstr "Votre nouveau mot de passe ne peut pas être identique au précédent." 159 | -------------------------------------------------------------------------------- /django_password_validators/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/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 | # 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: 2023-12-10 18:48+0100\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 | 20 | #: password_character_requirements/password_validation.py:31 21 | #, python-format 22 | msgid "This password must contain at least %(min_length)d digit." 23 | msgid_plural "This password must contain at least %(min_length)d digits." 24 | msgstr[0] "La tua password deve contenere almeno %(min_length)d cifra" 25 | msgstr[1] "La tua password deve contenere almeno %(min_length)d cifre" 26 | 27 | #: password_character_requirements/password_validation.py:41 28 | #, python-format 29 | msgid "This password must contain at least %(min_length)d letter." 30 | msgid_plural "This password must contain at least %(min_length)d letters." 31 | msgstr[0] "La tua password deve contenere almeno %(min_length)d lettera" 32 | msgstr[1] "La tua password deve contenere almeno %(min_length)d lettere" 33 | 34 | #: password_character_requirements/password_validation.py:51 35 | #, python-format 36 | msgid "This password must contain at least %(min_length)d upper case letter." 37 | msgid_plural "" 38 | "This password must contain at least %(min_length)d upper case letters." 39 | msgstr[0] "" 40 | "La tua password deve contenere almeno %(min_length)d lettera maiuscola" 41 | msgstr[1] "" 42 | "La tua password deve contenere almeno %(min_length)d lettere maiuscole" 43 | 44 | #: password_character_requirements/password_validation.py:61 45 | #, python-format 46 | msgid "This password must contain at least %(min_length)d lower case letter." 47 | msgid_plural "" 48 | "This password must contain at least %(min_length)d lower case letters." 49 | msgstr[0] "" 50 | "La tua password deve contenere almeno %(min_length)d lettera minuscola" 51 | msgstr[1] "" 52 | "La tua password deve contenere almeno %(min_length)d lettere minuscole" 53 | 54 | #: password_character_requirements/password_validation.py:71 55 | #, python-format 56 | msgid "This password must contain at least %(min_length)d special character." 57 | msgid_plural "" 58 | "This password must contain at least %(min_length)d special characters." 59 | msgstr[0] "La tua password deve contenere almeno %(min_length)d simbolo" 60 | msgstr[1] "La tua password deve contenere almeno %(min_length)d simboli" 61 | 62 | #: password_character_requirements/password_validation.py:86 63 | #, python-format 64 | msgid "%(min_length)s letter" 65 | msgid_plural "%(min_length)s letters" 66 | msgstr[0] "%(min_length)s lettera" 67 | msgstr[1] "%(min_length)s lettere" 68 | 69 | #: password_character_requirements/password_validation.py:94 70 | #, python-format 71 | msgid "%(min_length)s digit" 72 | msgid_plural "%(min_length)s digits" 73 | msgstr[0] "%(min_length)s cifra" 74 | msgstr[1] "%(min_length)s cifre" 75 | 76 | #: password_character_requirements/password_validation.py:102 77 | #, python-format 78 | msgid "%(min_length)s lower case letter" 79 | msgid_plural "%(min_length)s lower case letters" 80 | msgstr[0] "%(min_length)s lettera minuscola" 81 | msgstr[1] "%(min_length)s lettere minuscole" 82 | 83 | #: password_character_requirements/password_validation.py:110 84 | #, python-format 85 | msgid "%(min_length)s upper case letter" 86 | msgid_plural "%(min_length)s upper case letters" 87 | msgstr[0] "%(min_length)s lettera maiuscola" 88 | msgstr[1] "%(min_length)s lettere maiuscole" 89 | 90 | #: password_character_requirements/password_validation.py:118 91 | #, python-format 92 | msgid "" 93 | "%(min_length_special)s special character, such as %(special_characters)s" 94 | msgid_plural "" 95 | "%(min_length_special)s special characters, such as %(special_characters)s" 96 | msgstr[0] "%(min_length_special)s simbolo, come %(special_characters)s" 97 | msgstr[1] "%(min_length_special)s simboli, come %(special_characters)s" 98 | 99 | #: password_character_requirements/password_validation.py:123 100 | msgid "This password must contain at least" 101 | msgstr "La tua password deve contenere almeno" 102 | 103 | #: password_history/models.py:20 104 | msgid "When created salt" 105 | msgstr "" 106 | 107 | #: password_history/models.py:25 108 | msgid "Salt for the user" 109 | msgstr "Salt per l'utente" 110 | 111 | #: password_history/models.py:30 112 | msgid "The number of iterations for hasher" 113 | msgstr "Numero di iterazioni per l'hasher" 114 | 115 | #: password_history/models.py:38 116 | msgid "Configuration" 117 | msgstr "Configurazione" 118 | 119 | #: password_history/models.py:39 120 | msgid "Configurations" 121 | msgstr "Configurazioni" 122 | 123 | #: password_history/models.py:78 124 | msgid "Password hash" 125 | msgstr "" 126 | 127 | #: password_history/models.py:83 128 | msgid "Date" 129 | msgstr "Data" 130 | 131 | #: password_history/password_validation.py:82 132 | msgid "" 133 | "You can not use a password that was already used in this application in the " 134 | "past." 135 | msgstr "" 136 | "Non puoi riutilizzare una password usata precedentemente in questa " 137 | "applicazione." 138 | 139 | #: password_history/password_validation.py:117 140 | #, python-format 141 | msgid "" 142 | "Your new password can not be identical to any of the %(old_pass)d previously " 143 | "entered passwords." 144 | msgid_plural "" 145 | "Your new password can not be identical to any of the %(old_pass)d previously " 146 | "entered passwords." 147 | msgstr[0] "La tua nuova password non può essere uguale alla password attuale." 148 | msgstr[1] "" 149 | "La tua nuova password non può essere uguale a nessuna delle ultime " 150 | "%(old_pass)d password usate." 151 | 152 | #: password_history/password_validation.py:123 153 | msgid "" 154 | "Your new password can not be identical to any of the previously entered." 155 | msgstr "" 156 | "La tua nuova password non può essere identica a nessuna delle password usate " 157 | "in precedenza." 158 | -------------------------------------------------------------------------------- /django_password_validators/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/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 | # 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: 2023-12-10 18:48+0100\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: PL\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=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " 20 | "|| n%100>=20) ? 1 : 2);\n" 21 | 22 | #: password_character_requirements/password_validation.py:31 23 | #, python-format 24 | msgid "This password must contain at least %(min_length)d digit." 25 | msgid_plural "This password must contain at least %(min_length)d digits." 26 | msgstr[0] "To hasło musi zawierać co najmniej %(min_length)d znak." 27 | msgstr[1] "To hasło musi zawierać co najmniej %(min_length)d znaki." 28 | msgstr[2] "To hasło musi zawierać co najmniej %(min_length)d znaków." 29 | msgstr[3] "To hasło musi zawierać co najmniej %(min_length)d znaków." 30 | 31 | #: password_character_requirements/password_validation.py:41 32 | #, python-format 33 | msgid "This password must contain at least %(min_length)d letter." 34 | msgid_plural "This password must contain at least %(min_length)d letters." 35 | msgstr[0] "To hasło musi zawierać co najmniej %(min_length)d literę." 36 | msgstr[1] "To hasło musi zawierać co najmniej %(min_length)d litery." 37 | msgstr[2] "To hasło musi zawierać co najmniej %(min_length)d liter." 38 | msgstr[3] "To hasło musi zawierać co najmniej %(min_length)d liter." 39 | 40 | #: password_character_requirements/password_validation.py:51 41 | #, python-format 42 | msgid "This password must contain at least %(min_length)d upper case letter." 43 | msgid_plural "" 44 | "This password must contain at least %(min_length)d upper case letters." 45 | msgstr[0] "To hasło musi zawierać co najmniej %(min_length)d dużą literę." 46 | msgstr[1] "To hasło musi zawierać co najmniej %(min_length)d duże litery." 47 | msgstr[2] "To hasło musi zawierać co najmniej %(min_length)d dużych liter." 48 | msgstr[3] "To hasło musi zawierać co najmniej %(min_length)d dużych liter." 49 | 50 | #: password_character_requirements/password_validation.py:61 51 | #, python-format 52 | msgid "This password must contain at least %(min_length)d lower case letter." 53 | msgid_plural "" 54 | "This password must contain at least %(min_length)d lower case letters." 55 | msgstr[0] "To hasło musi zawierać co najmniej %(min_length)d małą literę." 56 | msgstr[1] "To hasło musi zawierać co najmniej %(min_length)d małe litery." 57 | msgstr[2] "To hasło musi zawierać co najmniej %(min_length)d małych liter." 58 | msgstr[3] "To hasło musi zawierać co najmniej %(min_length)d małych liter." 59 | 60 | #: password_character_requirements/password_validation.py:71 61 | #, python-format 62 | msgid "This password must contain at least %(min_length)d special character." 63 | msgid_plural "" 64 | "This password must contain at least %(min_length)d special characters." 65 | msgstr[0] "To hasło musi zawierać co najmniej %(min_length)d specjalny znak." 66 | msgstr[1] "To hasło musi zawierać co najmniej %(min_length)d specjalne znaki." 67 | msgstr[2] "" 68 | "To hasło musi zawierać co najmniej %(min_length)d specjalnych znaków." 69 | msgstr[3] "" 70 | "To hasło musi zawierać co najmniej %(min_length)d specjalnych znaków." 71 | 72 | #: password_character_requirements/password_validation.py:86 73 | #, python-format 74 | msgid "%(min_length)s letter" 75 | msgid_plural "%(min_length)s letters" 76 | msgstr[0] "%(min_length)s literę" 77 | msgstr[1] "%(min_length)s litery" 78 | msgstr[2] "%(min_length)s liter" 79 | 80 | #: password_character_requirements/password_validation.py:94 81 | #, python-format 82 | msgid "%(min_length)s digit" 83 | msgid_plural "%(min_length)s digits" 84 | msgstr[0] "%(min_length)s liczbę" 85 | msgstr[1] "%(min_length)s liczby" 86 | msgstr[2] "%(min_length)s liczb" 87 | 88 | #: password_character_requirements/password_validation.py:102 89 | #, python-format 90 | msgid "%(min_length)s lower case letter" 91 | msgid_plural "%(min_length)s lower case letters" 92 | msgstr[0] "%(min_length)s mały znak" 93 | msgstr[1] "%(min_length)s małe znaki" 94 | msgstr[2] "%(min_length)s małych znaków" 95 | 96 | #: password_character_requirements/password_validation.py:110 97 | #, python-format 98 | msgid "%(min_length)s upper case letter" 99 | msgid_plural "%(min_length)s upper case letters" 100 | msgstr[0] "%(min_length)s duży znak" 101 | msgstr[1] "%(min_length)s duże znaki" 102 | msgstr[2] "%(min_length)s dużych znaków" 103 | 104 | #: password_character_requirements/password_validation.py:118 105 | #, python-format 106 | msgid "" 107 | "%(min_length_special)s special character, such as %(special_characters)s" 108 | msgid_plural "" 109 | "%(min_length_special)s special characters, such as %(special_characters)s" 110 | msgstr[0] "" 111 | "%(min_length_special)s specjalny znak, taki jak %(special_characters)s" 112 | msgstr[1] "" 113 | "%(min_length_special)s specjalne znaki, takie jak %(special_characters)s" 114 | msgstr[2] "" 115 | "%(min_length_special)s specjalnych znaków, takich jak %(special_characters)s" 116 | 117 | #: password_character_requirements/password_validation.py:123 118 | msgid "This password must contain at least" 119 | msgstr "To hasło musi zawierać przynajmniej" 120 | 121 | #: password_history/models.py:20 122 | msgid "When created salt" 123 | msgstr "Kiedy utworzono sól" 124 | 125 | #: password_history/models.py:25 126 | msgid "Salt for the user" 127 | msgstr "Sól dla użytkownika" 128 | 129 | #: password_history/models.py:30 130 | msgid "The number of iterations for hasher" 131 | msgstr "Ilość iteracji dla hasza" 132 | 133 | #: password_history/models.py:38 134 | msgid "Configuration" 135 | msgstr "Konfiguracja" 136 | 137 | #: password_history/models.py:39 138 | msgid "Configurations" 139 | msgstr "Konfiguracje" 140 | 141 | #: password_history/models.py:78 142 | msgid "Password hash" 143 | msgstr "Hasz hasła" 144 | 145 | #: password_history/models.py:83 146 | msgid "Date" 147 | msgstr "Data" 148 | 149 | #: password_history/password_validation.py:82 150 | msgid "" 151 | "You can not use a password that was already used in this application in the " 152 | "past." 153 | msgstr "Nie możesz użyć hasła, które już kiedyś używałeś w tej aplikacji." 154 | 155 | #: password_history/password_validation.py:117 156 | #, python-format 157 | msgid "" 158 | "Your new password can not be identical to any of the %(old_pass)d previously " 159 | "entered passwords." 160 | msgid_plural "" 161 | "Your new password can not be identical to any of the %(old_pass)d previously " 162 | "entered passwords." 163 | msgstr[0] "" 164 | "Twoje nowe hasło nie może być takie samo jak ostatnio wprowadzone hasło." 165 | msgstr[1] "" 166 | "Twoje nowe hasło nie może być identyczne jak któreś z %(old_pass)d " 167 | "poprzednio wprowadzonych haseł." 168 | msgstr[2] "" 169 | "Twoje nowe hasło nie może być identyczne jak któreś z %(old_pass)d " 170 | "poprzednio wprowadzonych haseł." 171 | 172 | #: password_history/password_validation.py:123 173 | msgid "" 174 | "Your new password can not be identical to any of the previously entered." 175 | msgstr "" 176 | "Twoje nowe hasło nie może być identyczne jak któreś z poprzednio " 177 | "wprowadzonych." 178 | -------------------------------------------------------------------------------- /django_password_validators/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_password_validators/locale/zh_Hans/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: 2023-12-10 18:48+0100\n" 12 | "PO-Revision-Date: 2023-06-15 14:00+0800\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=1; plural=0;\n" 20 | 21 | #: password_character_requirements/password_validation.py:31 22 | #, python-format 23 | msgid "This password must contain at least %(min_length)d digit." 24 | msgid_plural "This password must contain at least %(min_length)d digits." 25 | msgstr[0] "你的密码必须包含至少 %(min_length)d 个数字。" 26 | 27 | #: password_character_requirements/password_validation.py:41 28 | #, python-format 29 | msgid "This password must contain at least %(min_length)d letter." 30 | msgid_plural "This password must contain at least %(min_length)d letters." 31 | msgstr[0] "你的密码必须包含至少 %(min_length)d 个字母。" 32 | 33 | #: password_character_requirements/password_validation.py:51 34 | #, python-format 35 | msgid "This password must contain at least %(min_length)d upper case letter." 36 | msgid_plural "" 37 | "This password must contain at least %(min_length)d upper case letters." 38 | msgstr[0] "你的密码必须包含至少 %(min_length)d 个大写字母。" 39 | 40 | #: password_character_requirements/password_validation.py:61 41 | #, python-format 42 | msgid "This password must contain at least %(min_length)d lower case letter." 43 | msgid_plural "" 44 | "This password must contain at least %(min_length)d lower case letters." 45 | msgstr[0] "你的密码必须包含至少 %(min_length)d 个小写字母。" 46 | 47 | #: password_character_requirements/password_validation.py:71 48 | #, python-format 49 | msgid "This password must contain at least %(min_length)d special character." 50 | msgid_plural "" 51 | "This password must contain at least %(min_length)d special characters." 52 | msgstr[0] "你的密码必须包含至少 %(min_length)d 个特殊符号。" 53 | 54 | #: password_character_requirements/password_validation.py:86 55 | #, python-format 56 | msgid "%(min_length)s letter" 57 | msgid_plural "%(min_length)s letters" 58 | msgstr[0] "%(min_length)s 个字母" 59 | 60 | #: password_character_requirements/password_validation.py:94 61 | #, python-format 62 | msgid "%(min_length)s digit" 63 | msgid_plural "%(min_length)s digits" 64 | msgstr[0] "%(min_length)s 个数字" 65 | 66 | #: password_character_requirements/password_validation.py:102 67 | #, python-format 68 | msgid "%(min_length)s lower case letter" 69 | msgid_plural "%(min_length)s lower case letters" 70 | msgstr[0] "%(min_length)s 个小写字母" 71 | 72 | #: password_character_requirements/password_validation.py:110 73 | #, python-format 74 | msgid "%(min_length)s upper case letter" 75 | msgid_plural "%(min_length)s upper case letters" 76 | msgstr[0] "%(min_length)s 个大写字母" 77 | 78 | #: password_character_requirements/password_validation.py:118 79 | #, python-format 80 | msgid "" 81 | "%(min_length_special)s special character, such as %(special_characters)s" 82 | msgid_plural "" 83 | "%(min_length_special)s special characters, such as %(special_characters)s" 84 | msgstr[0] "%(min_length_special)s 个特殊符号,例如:%(special_characters)s" 85 | 86 | #: password_character_requirements/password_validation.py:123 87 | msgid "This password must contain at least" 88 | msgstr "你的密码必须包含至少" 89 | 90 | #: password_history/models.py:20 91 | msgid "When created salt" 92 | msgstr "创建 salt 的时间" 93 | 94 | #: password_history/models.py:25 95 | msgid "Salt for the user" 96 | msgstr "用户的 salt" 97 | 98 | #: password_history/models.py:30 99 | msgid "The number of iterations for hasher" 100 | msgstr "密码哈希的迭代次数" 101 | 102 | #: password_history/models.py:38 103 | msgid "Configuration" 104 | msgstr "配置" 105 | 106 | #: password_history/models.py:39 107 | msgid "Configurations" 108 | msgstr "配置" 109 | 110 | #: password_history/models.py:78 111 | msgid "Password hash" 112 | msgstr "密码哈希" 113 | 114 | #: password_history/models.py:83 115 | msgid "Date" 116 | msgstr "日期" 117 | 118 | #: password_history/password_validation.py:82 119 | msgid "" 120 | "You can not use a password that was already used in this application in the " 121 | "past." 122 | msgstr "你不能重复使用这个密码。" 123 | 124 | #: password_history/password_validation.py:117 125 | #, python-format 126 | msgid "" 127 | "Your new password can not be identical to any of the %(old_pass)d previously " 128 | "entered passwords." 129 | msgid_plural "" 130 | "Your new password can not be identical to any of the %(old_pass)d previously " 131 | "entered passwords." 132 | msgstr[0] "你的密码不能与之前输入的 %(old_pass)d 个密码相同。" 133 | 134 | #: password_history/password_validation.py:123 135 | msgid "" 136 | "Your new password can not be identical to any of the previously entered." 137 | msgstr "你的密码应该和以前使用过的密码不同。" 138 | -------------------------------------------------------------------------------- /django_password_validators/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | -------------------------------------------------------------------------------- /django_password_validators/password_character_requirements/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/password_character_requirements/__init__.py -------------------------------------------------------------------------------- /django_password_validators/password_character_requirements/password_validation.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | try: 3 | from django.utils.translation import gettext as _, ngettext 4 | except ImportError: 5 | from django.utils.translation import ugettext as _, ungettext as ngettext 6 | 7 | 8 | class PasswordCharacterValidator(): 9 | 10 | def __init__( 11 | self, 12 | min_length_digit=1, 13 | min_length_alpha=1, 14 | min_length_special=1, 15 | min_length_lower=1, 16 | min_length_upper=1, 17 | special_characters="~!@#$%^&*()_+{}\":;'[]" 18 | ): 19 | self.min_length_digit = min_length_digit 20 | self.min_length_alpha = min_length_alpha 21 | self.min_length_special = min_length_special 22 | self.min_length_lower = min_length_lower 23 | self.min_length_upper = min_length_upper 24 | self.special_characters = special_characters 25 | 26 | def validate(self, password, user=None): 27 | validation_errors = [] 28 | if len([char for char in password if char.isdigit()]) < self.min_length_digit: 29 | validation_errors.append(ValidationError( 30 | ngettext( 31 | 'This password must contain at least %(min_length)d digit.', 32 | 'This password must contain at least %(min_length)d digits.', 33 | self.min_length_digit 34 | ), 35 | params={'min_length': self.min_length_digit}, 36 | code='min_length_digit', 37 | )) 38 | if len([char for char in password if char.isalpha()]) < self.min_length_alpha: 39 | validation_errors.append(ValidationError( 40 | ngettext( 41 | 'This password must contain at least %(min_length)d letter.', 42 | 'This password must contain at least %(min_length)d letters.', 43 | self.min_length_alpha 44 | ), 45 | params={'min_length': self.min_length_alpha}, 46 | code='min_length_alpha', 47 | )) 48 | if len([char for char in password if char.isupper()]) < self.min_length_upper: 49 | validation_errors.append(ValidationError( 50 | ngettext( 51 | 'This password must contain at least %(min_length)d upper case letter.', 52 | 'This password must contain at least %(min_length)d upper case letters.', 53 | self.min_length_upper 54 | ), 55 | params={'min_length': self.min_length_upper}, 56 | code='min_length_upper_characters', 57 | )) 58 | if len([char for char in password if char.islower()]) < self.min_length_lower: 59 | validation_errors.append(ValidationError( 60 | ngettext( 61 | 'This password must contain at least %(min_length)d lower case letter.', 62 | 'This password must contain at least %(min_length)d lower case letters.', 63 | self.min_length_lower 64 | ), 65 | params={'min_length': self.min_length_lower}, 66 | code='min_length_lower_characters', 67 | )) 68 | if len([char for char in password if char in self.special_characters]) < self.min_length_special: 69 | validation_errors.append(ValidationError( 70 | ngettext( 71 | 'This password must contain at least %(min_length)d special character.', 72 | 'This password must contain at least %(min_length)d special characters.', 73 | self.min_length_special 74 | ), 75 | params={'min_length': self.min_length_special}, 76 | code='min_length_special_characters', 77 | )) 78 | if validation_errors: 79 | raise ValidationError(validation_errors) 80 | 81 | def get_help_text(self): 82 | validation_req = [] 83 | if self.min_length_alpha: 84 | validation_req.append( 85 | ngettext( 86 | "%(min_length)s letter", 87 | "%(min_length)s letters", 88 | self.min_length_alpha 89 | ) % {'min_length': self.min_length_alpha} 90 | ) 91 | if self.min_length_digit: 92 | validation_req.append( 93 | ngettext( 94 | "%(min_length)s digit", 95 | "%(min_length)s digits", 96 | self.min_length_digit 97 | ) % {'min_length': self.min_length_digit} 98 | ) 99 | if self.min_length_lower: 100 | validation_req.append( 101 | ngettext( 102 | "%(min_length)s lower case letter", 103 | "%(min_length)s lower case letters", 104 | self.min_length_lower 105 | ) % {'min_length': self.min_length_lower} 106 | ) 107 | if self.min_length_upper: 108 | validation_req.append( 109 | ngettext( 110 | "%(min_length)s upper case letter", 111 | "%(min_length)s upper case letters", 112 | self.min_length_upper 113 | ) % {'min_length': self.min_length_upper} 114 | ) 115 | if self.min_length_special and self.special_characters: 116 | validation_req.append( 117 | ngettext( 118 | "%(min_length_special)s special character, such as %(special_characters)s", 119 | "%(min_length_special)s special characters, such as %(special_characters)s", 120 | self.min_length_special 121 | ) % {'min_length_special': str(self.min_length_special), 'special_characters': self.special_characters} 122 | ) 123 | return _("This password must contain at least") + ' ' + ', '.join(validation_req) + '.' 124 | -------------------------------------------------------------------------------- /django_password_validators/password_history/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/password_history/__init__.py -------------------------------------------------------------------------------- /django_password_validators/password_history/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import PasswordHistory, UserPasswordHistoryConfig 4 | 5 | 6 | class UserPasswordHistoryConfigAdmin(admin.ModelAdmin): 7 | 8 | list_display = ('user', 'date', 'iterations') 9 | list_filter = ('date', ) 10 | ordering = ('date', ) 11 | 12 | 13 | class PasswordHistoryAdmin(admin.ModelAdmin): 14 | 15 | list_display = ('user_config', 'date', ) 16 | list_filter = ('date', ) 17 | ordering = ('date', ) 18 | 19 | admin.site.register(UserPasswordHistoryConfig, UserPasswordHistoryConfigAdmin) 20 | admin.site.register(PasswordHistory, PasswordHistoryAdmin) 21 | -------------------------------------------------------------------------------- /django_password_validators/password_history/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PasswordHistoryConfig(AppConfig): 5 | name = 'django_password_validators.password_history' 6 | default_auto_field = "django.db.models.AutoField" 7 | -------------------------------------------------------------------------------- /django_password_validators/password_history/hashers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.hashers import PBKDF2PasswordHasher 2 | 3 | 4 | class HistoryHasher(PBKDF2PasswordHasher): 5 | """ 6 | We need to keep the old password so that when you update django 7 | (or configuration change) hashes have not changed. 8 | Therefore, special hasher. 9 | 10 | """ 11 | # Experimental value of the of iterations so that the calculation on the 12 | # average server configuration lasted around one second. 13 | iterations = 20000 * 10 14 | 15 | 16 | class HistoryVeryStrongHasher(PBKDF2PasswordHasher): 17 | """ 18 | We need to keep the old password so that when you update django 19 | (or configuration change) hashes have not changed. 20 | Therefore, special hasher. 21 | 22 | """ 23 | # Experimental value of the of iterations so that the calculation on the 24 | # average server configuration lasted around 10 second. 25 | iterations = 20000 * 101 -------------------------------------------------------------------------------- /django_password_validators/password_history/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-03-01 11:07 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 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='PasswordHistory', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('password', models.CharField(editable=False, max_length=255, verbose_name='Password hash')), 24 | ('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')), 25 | ], 26 | options={ 27 | 'ordering': ['-user_config', 'password'], 28 | 'verbose_name': 'Old password', 29 | 'verbose_name_plural': 'Password history', 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name='UserPasswordHistoryConfig', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('date', models.DateTimeField(auto_now_add=True, verbose_name='When created salt')), 37 | ('salt', models.CharField(editable=False, max_length=120, verbose_name='Salt for the user')), 38 | ('iterations', models.IntegerField(blank=True, default=None, editable=False, null=True, verbose_name='The number of of iterations for Hasher')), 39 | ('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 40 | ], 41 | options={ 42 | 'ordering': ['-user', 'iterations'], 43 | 'verbose_name': 'Configuration', 44 | 'verbose_name_plural': 'Configurations', 45 | }, 46 | ), 47 | migrations.AddField( 48 | model_name='passwordhistory', 49 | name='user_config', 50 | field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='password_history.UserPasswordHistoryConfig'), 51 | ), 52 | migrations.AlterUniqueTogether( 53 | name='userpasswordhistoryconfig', 54 | unique_together=set([('user', 'iterations')]), 55 | ), 56 | migrations.AlterUniqueTogether( 57 | name='passwordhistory', 58 | unique_together=set([('user_config', 'password')]), 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /django_password_validators/password_history/migrations/0002_auto_20180424_1422.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.12 on 2018-04-24 14:22 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 | ('password_history', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='passwordhistory', 17 | name='password', 18 | field=models.CharField(editable=False, max_length=255, verbose_name='Password hash'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_password_validators/password_history/migrations/0003_auto_20201206_1357.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-12-06 13:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('password_history', '0002_auto_20180424_1422'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='userpasswordhistoryconfig', 15 | name='iterations', 16 | field=models.IntegerField(blank=True, default=None, editable=False, null=True, verbose_name='The number of iterations for hasher'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_password_validators/password_history/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/django_password_validators/password_history/migrations/__init__.py -------------------------------------------------------------------------------- /django_password_validators/password_history/models.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf import settings 3 | from django.db import models 4 | from django.utils.crypto import get_random_string 5 | try: 6 | from django.utils.translation import gettext_lazy as _ 7 | except ImportError: 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | from django_password_validators.settings import get_password_hasher 11 | 12 | 13 | class UserPasswordHistoryConfig(models.Model): 14 | user = models.ForeignKey( 15 | settings.AUTH_USER_MODEL, 16 | on_delete=models.CASCADE, 17 | editable=False 18 | ) 19 | date = models.DateTimeField( 20 | _('When created salt'), 21 | auto_now_add=True, 22 | editable=False 23 | ) 24 | salt = models.CharField( 25 | verbose_name=_('Salt for the user'), 26 | max_length=120, 27 | editable=False, 28 | ) 29 | iterations = models.IntegerField( 30 | _('The number of iterations for hasher'), 31 | default=None, 32 | editable=False, 33 | blank=True, 34 | null=True 35 | ) 36 | 37 | class Meta: 38 | verbose_name = _('Configuration') 39 | verbose_name_plural = _('Configurations') 40 | unique_together = (("user", "iterations",),) 41 | ordering = ['-user', 'iterations', ] 42 | 43 | def make_password_hash(self, password): 44 | """ 45 | Generates a password hash for the given password. 46 | 47 | Args: 48 | passaword - the password is not encrypted form 49 | """ 50 | hasher = get_password_hasher()() 51 | return hasher.encode(password, self.salt, self.iterations) 52 | 53 | def _gen_password_history_salt(self): 54 | salt_max_length = self._meta.get_field('salt').max_length 55 | self.salt = get_random_string(length=salt_max_length) 56 | 57 | def save(self, *args, **kwargs): 58 | # When there is no salt as defined for a given user, 59 | # then we create the salt. 60 | if not self.salt: 61 | self._gen_password_history_salt() 62 | # We take iterations from the default Hasher 63 | if not self.iterations: 64 | self.iterations = get_password_hasher().iterations 65 | return super(UserPasswordHistoryConfig, self).save(*args, **kwargs) 66 | 67 | def __str__(self): 68 | return '%s [%d]' % (self.user, self.iterations) 69 | 70 | 71 | class PasswordHistory(models.Model): 72 | user_config = models.ForeignKey( 73 | UserPasswordHistoryConfig, 74 | on_delete=models.CASCADE, 75 | editable=False 76 | ) 77 | password = models.CharField( 78 | _('Password hash'), 79 | max_length=255, 80 | editable=False 81 | ) 82 | date = models.DateTimeField( 83 | _('Date'), 84 | auto_now_add=True, 85 | editable=False 86 | ) 87 | 88 | class Meta: 89 | verbose_name = 'Old password' 90 | verbose_name_plural = 'Password history' 91 | unique_together = (("user_config", "password",),) 92 | ordering = ['-user_config', 'password', ] 93 | 94 | def __str__(self): 95 | return '%s [%s]' % (self.user_config.user, self.date) 96 | -------------------------------------------------------------------------------- /django_password_validators/password_history/password_validation.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import warnings 3 | 4 | from django.core.exceptions import ValidationError 5 | 6 | try: 7 | from django.utils.translation import gettext as _, ngettext 8 | except ImportError: 9 | from django.utils.translation import ugettext as _, ngettext 10 | from django_password_validators.settings import get_password_hasher 11 | from django_password_validators.password_history.models import ( 12 | PasswordHistory, 13 | UserPasswordHistoryConfig, 14 | ) 15 | 16 | 17 | class UniquePasswordsValidator(object): 18 | """ 19 | Validate whether the password was once used by the user. 20 | 21 | The password is only checked for an existing user. 22 | """ 23 | 24 | def __init__(self, last_passwords=0): 25 | """ 26 | 27 | :param last_passwords: 28 | * lookup_range > 0 - We check only the XXX latest passwords 29 | * lookup_range <= 0 - Check all passwords that have been used so far 30 | """ 31 | self.last_passwords = int(last_passwords) 32 | 33 | def _user_ok(self, user): 34 | if not user: 35 | return 36 | 37 | user_pk = getattr(user, 'pk', None) 38 | if user_pk is None: 39 | warnings.warn( 40 | 'An unsaved user model!s %r' 41 | % user 42 | ) 43 | return 44 | 45 | if isinstance(user_pk, property): 46 | warnings.warn( 47 | 'Not initialized user model! %r' 48 | % user 49 | ) 50 | return 51 | 52 | return True 53 | 54 | def delete_old_passwords(self, user): 55 | if self.last_passwords > 0: 56 | # Delete old passwords that are outside the lookup_range 57 | password_ids = list( 58 | PasswordHistory.objects. \ 59 | filter(user_config__user=user). \ 60 | order_by('-date')[self.last_passwords:]. \ 61 | values_list('pk', flat=True) 62 | ) 63 | if password_ids: 64 | PasswordHistory.objects.filter(pk__in=password_ids).delete() 65 | 66 | def validate(self, password, user=None): 67 | 68 | if not self._user_ok(user): 69 | return 70 | 71 | # We make sure there are no old passwords in the database. 72 | self.delete_old_passwords(user) 73 | 74 | for user_config in UserPasswordHistoryConfig.objects.filter(user=user): 75 | password_hash = user_config.make_password_hash(password) 76 | try: 77 | PasswordHistory.objects.get( 78 | user_config=user_config, 79 | password=password_hash 80 | ) 81 | raise ValidationError( 82 | _("You can not use a password that was already used in this application in the past."), 83 | code='password_used' 84 | ) 85 | except PasswordHistory.DoesNotExist: 86 | pass 87 | 88 | def password_changed(self, password, user=None): 89 | 90 | if not self._user_ok(user): 91 | return 92 | 93 | user_config = UserPasswordHistoryConfig.objects.filter( 94 | user=user, 95 | iterations=get_password_hasher().iterations 96 | ).first() 97 | 98 | if not user_config: 99 | user_config = UserPasswordHistoryConfig() 100 | user_config.user = user 101 | user_config.save() 102 | 103 | password_hash = user_config.make_password_hash(password) 104 | 105 | # We are looking hash password in the database 106 | old_password, old_password__created = PasswordHistory.objects.get_or_create( 107 | user_config=user_config, 108 | password=password_hash 109 | ) 110 | 111 | # We make sure there are no old passwords in the database. 112 | self.delete_old_passwords(user) 113 | 114 | def get_help_text(self): 115 | if self.last_passwords > 0: 116 | return ngettext( 117 | 'Your new password can not be identical to any of the %(old_pass)d previously entered passwords.', 118 | 'Your new password can not be identical to any of the %(old_pass)d previously entered passwords.', 119 | self.last_passwords 120 | ) % {'old_pass': self.last_passwords} 121 | 122 | else: 123 | return _('Your new password can not be identical to any of the previously entered.') 124 | -------------------------------------------------------------------------------- /django_password_validators/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.module_loading import import_string 3 | 4 | 5 | def get_password_hasher(): 6 | history_hasher = getattr( 7 | settings, 8 | 'DPV_DEFAULT_HISTORY_HASHER', 9 | 'django_password_validators.password_history.hashers.HistoryHasher' 10 | ) 11 | return import_string(history_hasher) 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import dirname, realpath 3 | import sys 4 | 5 | from setuptools import setup, find_packages 6 | from setuptools.command.test import test as TestCommand 7 | 8 | from django_password_validators import __version__ as VERSION 9 | 10 | 11 | class Tox(TestCommand): 12 | 13 | def initialize_options(self): 14 | TestCommand.initialize_options(self) 15 | self.tox_args = None 16 | 17 | def finalize_options(self): 18 | TestCommand.finalize_options(self) 19 | self.test_args = [] 20 | self.test_suite = True 21 | 22 | def run_tests(self): 23 | # import here, cause outside the eggs aren't loaded 24 | import tox 25 | import shlex 26 | args = self.tox_args 27 | if args: 28 | args = shlex.split(self.tox_args) 29 | errno = tox.cmdline(args=args) 30 | sys.exit(errno) 31 | 32 | 33 | __dir__ = realpath(dirname(__file__)) 34 | 35 | TESTS_REQUIRE = ['tox >= 4.11', 'bump-my-version >= 0.12'] 36 | 37 | DESCRIPTION = open( 38 | os.path.join(os.path.dirname(__file__), 'README.rst')).read() 39 | 40 | setup( 41 | name='django-password-validators', 42 | version=VERSION, 43 | description="Additional libraries for validating passwords in Django.", 44 | long_description=DESCRIPTION, 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Environment :: Web Environment', 48 | 'Framework :: Django', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: BSD License', 51 | 'Programming Language :: Python :: 3.7', 52 | 'Programming Language :: Python :: 3.8', 53 | 'Programming Language :: Python :: 3.9', 54 | 'Programming Language :: Python :: 3.10', 55 | 'Programming Language :: Python :: 3.11', 56 | 'Programming Language :: Python :: 3.12', 57 | 'Programming Language :: Python', 58 | 'Operating System :: OS Independent', 59 | 'Topic :: Security', 60 | ], # strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 61 | keywords='django password validator', 62 | author='Wojciech Banas', 63 | author_email='fizista@gmail.com', 64 | url='https://github.com/fizista/django-password-validators', 65 | license='BSD', 66 | packages=find_packages(exclude=['tests*', ]), 67 | include_package_data=True, 68 | zip_safe=False, 69 | install_requires=[ 70 | 'django >= 3.0', 71 | ], 72 | tests_require=TESTS_REQUIRE, 73 | extras_require={ 74 | 'test': TESTS_REQUIRE, 75 | }, 76 | cmdclass={'test': Tox}, 77 | ) 78 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/tests/__init__.py -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from os.path import join, realpath, dirname 4 | import sys 5 | 6 | 7 | __dir__ = realpath(dirname(__file__)) 8 | sys.path.insert(0, realpath(join(__dir__, '..'))) 9 | 10 | 11 | if __name__ == "__main__": 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 13 | 14 | from django.core.management import execute_from_command_line 15 | 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /tests/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/tests/test_project/__init__.py -------------------------------------------------------------------------------- /tests/test_project/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = '!p!7q_yipf=rivrb#9m-yc&%lnlq9*qvnt72+(#(@qsscbi3@6' 6 | 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS = [] 10 | 11 | # Application definition 12 | 13 | INSTALLED_APPS = [ 14 | 'django.contrib.admin', 15 | 'django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.messages', 19 | 'django.contrib.staticfiles', 20 | ] 21 | 22 | MIDDLEWARE_CLASSES = [ 23 | 'django.middleware.security.SecurityMiddleware', 24 | 'django.contrib.sessions.middleware.SessionMiddleware', 25 | 'django.middleware.common.CommonMiddleware', 26 | 'django.middleware.csrf.CsrfViewMiddleware', 27 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 28 | 'django.contrib.messages.middleware.MessageMiddleware', 29 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 30 | ] 31 | 32 | MIDDLEWARE = MIDDLEWARE_CLASSES 33 | 34 | ROOT_URLCONF = 'test_project.urls' 35 | 36 | TEMPLATES = [ 37 | { 38 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 39 | 'DIRS': [], 40 | 'APP_DIRS': True, 41 | 'OPTIONS': { 42 | 'context_processors': [ 43 | 'django.template.context_processors.debug', 44 | 'django.template.context_processors.request', 45 | 'django.contrib.auth.context_processors.auth', 46 | 'django.contrib.messages.context_processors.messages', 47 | ], 48 | }, 49 | }, 50 | ] 51 | 52 | WSGI_APPLICATION = 'test_project.wsgi.application' 53 | 54 | DATABASES = { 55 | 'default': { 56 | 'ENGINE': 'django.db.backends.sqlite3', 57 | 'NAME': os.path.join(current_dir, 'db.sqlite3'), 58 | } 59 | } 60 | 61 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 62 | 63 | # Internationalization 64 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 65 | 66 | LANGUAGE_CODE = 'en-us' 67 | 68 | TIME_ZONE = 'UTC' 69 | 70 | USE_I18N = True 71 | 72 | USE_L10N = True 73 | 74 | USE_TZ = True 75 | 76 | # Static files (CSS, JavaScript, Images) 77 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 78 | 79 | STATIC_URL = '/static/' 80 | 81 | ## ####################################################################### 82 | # Specific configurations for the library django_password_validators 83 | ## ####################################################################### 84 | AUTH_PASSWORD_VALIDATORS = [ 85 | { 86 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 87 | }, 88 | ] 89 | 90 | INSTALLED_APPS += [ 91 | # Required by: UniquePasswordsValidator 92 | 'django_password_validators.password_history', 93 | ] 94 | -------------------------------------------------------------------------------- /tests/test_project/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fizista/django-password-validators/b9c0384c21cb3f7ccfb256cd0e0b6bfb500abf83/tests/test_project/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_project/tests/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.password_validation import validate_password 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | 6 | 7 | class PasswordsTestCase(TestCase): 8 | """ 9 | Local varibles: 10 | self.UserModel - user model class 11 | """ 12 | 13 | PASSWORD_TEMPLATE = 'ABCDEFGHIJKLMNOPRSTUWXYZ_%d' 14 | 15 | def create_user(self, number=1): 16 | user = self.UserModel.objects.create_user( 17 | 'test%d' % number, 18 | email='test%d@example.com' % number, 19 | ) 20 | user.set_password(self.PASSWORD_TEMPLATE % 1) 21 | user.save() 22 | return user 23 | 24 | def user_change_password(self, user_number, password_number): 25 | user = self.UserModel.objects.get(username='test%d' % user_number) 26 | user.set_password(self.PASSWORD_TEMPLATE % password_number) 27 | user.save() 28 | 29 | def assert_password_validation_True(self, user_number, password_number): 30 | user = self.UserModel.objects.get(username='test%d' % user_number) 31 | validate_password( 32 | self.PASSWORD_TEMPLATE % password_number, 33 | user 34 | ) 35 | 36 | def assert_password_validation_False(self, user_number, password_number): 37 | user = self.UserModel.objects.get(username='test%d' % user_number) 38 | 39 | try: 40 | validate_password( 41 | self.PASSWORD_TEMPLATE % password_number, 42 | user 43 | ) 44 | except ValidationError as e: 45 | for error in e.error_list: 46 | if e.error_list[0].code == 'password_used': 47 | return 48 | else: 49 | raise e 50 | 51 | def setUp(self): 52 | self.UserModel = get_user_model() 53 | super(PasswordsTestCase, self).setUp() 54 | 55 | def tearDown(self): 56 | self.UserModel.objects.all().delete() 57 | super(PasswordsTestCase, self).tearDown() 58 | -------------------------------------------------------------------------------- /tests/test_project/tests/test_character_requirements.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | 3 | from django_password_validators.password_character_requirements.password_validation import PasswordCharacterValidator 4 | 5 | from .base import PasswordsTestCase 6 | 7 | 8 | class UniquePasswordsValidatorTestCase(PasswordsTestCase): 9 | 10 | def test_digit(self): 11 | pv = PasswordCharacterValidator( 12 | min_length_digit=1, 13 | min_length_alpha=0, 14 | min_length_special=0, 15 | min_length_lower=0, 16 | min_length_upper=0, 17 | special_characters="[]" 18 | ) 19 | pv.validate('1') 20 | with self.assertRaises(ValidationError): 21 | pv.validate('abc') 22 | 23 | def test_alpha(self): 24 | pv = PasswordCharacterValidator( 25 | min_length_digit=0, 26 | min_length_alpha=1, 27 | min_length_special=0, 28 | min_length_lower=0, 29 | min_length_upper=0, 30 | special_characters="[]" 31 | ) 32 | pv.validate('a') 33 | with self.assertRaises(ValidationError): 34 | pv.validate('1') 35 | 36 | def test_special(self): 37 | pv = PasswordCharacterValidator( 38 | min_length_digit=0, 39 | min_length_alpha=0, 40 | min_length_special=1, 41 | min_length_lower=0, 42 | min_length_upper=0, 43 | special_characters="[]" 44 | ) 45 | pv.validate(']') 46 | with self.assertRaises(ValidationError): 47 | pv.validate('1') 48 | 49 | def test_lower(self): 50 | pv = PasswordCharacterValidator( 51 | min_length_digit=0, 52 | min_length_alpha=0, 53 | min_length_special=0, 54 | min_length_lower=1, 55 | min_length_upper=0, 56 | special_characters="[]" 57 | ) 58 | pv.validate('a') 59 | with self.assertRaises(ValidationError): 60 | pv.validate('A') 61 | 62 | def test_upper(self): 63 | pv = PasswordCharacterValidator( 64 | min_length_digit=0, 65 | min_length_alpha=0, 66 | min_length_special=0, 67 | min_length_lower=0, 68 | min_length_upper=1, 69 | special_characters="[]" 70 | ) 71 | pv.validate('A') 72 | with self.assertRaises(ValidationError): 73 | pv.validate('a') 74 | 75 | def test_all(self): 76 | pv = PasswordCharacterValidator( 77 | min_length_digit=2, 78 | min_length_alpha=4, 79 | min_length_special=2, 80 | min_length_lower=2, 81 | min_length_upper=2, 82 | special_characters="!@#$%[]" 83 | ) 84 | pv.validate('12ab[]AB') 85 | -------------------------------------------------------------------------------- /tests/test_project/tests/test_password_history.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.password_validation import validate_password 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase, override_settings 5 | 6 | from django_password_validators.password_history.password_validation import UniquePasswordsValidator 7 | from django_password_validators.password_history.hashers import ( 8 | HistoryVeryStrongHasher, 9 | HistoryHasher 10 | ) 11 | from django_password_validators.password_history.models import ( 12 | UserPasswordHistoryConfig, 13 | PasswordHistory 14 | ) 15 | 16 | from .base import PasswordsTestCase 17 | 18 | 19 | class UniquePasswordsValidatorTestCase(PasswordsTestCase): 20 | 21 | def test_create_user(self): 22 | self.create_user(1) 23 | self.assertEqual(PasswordHistory.objects.all().count(), 1) 24 | self.assertEqual(UserPasswordHistoryConfig.objects.all().count(), 1) 25 | 26 | def test_none_user(self): 27 | dummy_user = get_user_model() 28 | upv = UniquePasswordsValidator() 29 | upv.validate('qwerty', None) 30 | upv.password_changed('qwerty', None) 31 | 32 | self.assertEqual(PasswordHistory.objects.all().count(), 0) 33 | self.assertEqual(UserPasswordHistoryConfig.objects.all().count(), 0) 34 | 35 | def test_not_saved_user(self): 36 | dummy_user = get_user_model() 37 | upv = UniquePasswordsValidator() 38 | upv.validate('qwerty', dummy_user) 39 | upv.password_changed('qwerty', dummy_user) 40 | 41 | dummy_user = get_user_model()() 42 | upv = UniquePasswordsValidator() 43 | upv.validate('qwerty', dummy_user) 44 | upv.password_changed('qwerty', dummy_user) 45 | 46 | self.assertEqual(PasswordHistory.objects.all().count(), 0) 47 | self.assertEqual(UserPasswordHistoryConfig.objects.all().count(), 0) 48 | 49 | def test_create_multiple_users(self): 50 | self.create_user(1) 51 | self.create_user(2) 52 | self.assertEqual(PasswordHistory.objects.all().count(), 2) 53 | self.assertEqual(UserPasswordHistoryConfig.objects.all().count(), 2) 54 | 55 | def test_user_changed_password(self): 56 | self.create_user(1) 57 | self.user_change_password(user_number=1, password_number=2) 58 | # We check that there are no duplicate hashes passwords in the database 59 | self.user_change_password(user_number=1, password_number=2) 60 | # They must be only two hashes 61 | self.assertEqual(PasswordHistory.objects.all().count(), 2) 62 | self.assert_password_validation_False(user_number=1, password_number=2) 63 | self.assert_password_validation_True(user_number=1, password_number=3) 64 | self.user_change_password(user_number=1, password_number=3) 65 | self.assert_password_validation_False(user_number=1, password_number=3) 66 | 67 | def test_change_number_hasher_iterations(self): 68 | self.create_user(1) 69 | self.user_change_password(user_number=1, password_number=2) 70 | with self.settings( 71 | DPV_DEFAULT_HISTORY_HASHER='django_password_validators.password_history.hashers.HistoryVeryStrongHasher'): 72 | self.assert_password_validation_False( 73 | user_number=1, 74 | password_number=1 75 | ) 76 | self.assert_password_validation_False( 77 | user_number=1, 78 | password_number=2 79 | ) 80 | self.assert_password_validation_True( 81 | user_number=1, 82 | password_number=3 83 | ) 84 | self.user_change_password( 85 | user_number=1, 86 | password_number=3 87 | ) 88 | self.assert_password_validation_False( 89 | user_number=1, 90 | password_number=3 91 | ) 92 | 93 | self.assertEqual( 94 | PasswordHistory.objects.filter( 95 | user_config__iterations=HistoryHasher.iterations).count(), 96 | 2, 97 | ) 98 | self.assertEqual( 99 | PasswordHistory.objects.filter( 100 | user_config__iterations=HistoryVeryStrongHasher.iterations).count(), 101 | 1, 102 | ) 103 | 104 | @override_settings(AUTH_PASSWORD_VALIDATORS=[{ 105 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 106 | 'OPTIONS': { 107 | 'last_passwords': 0 108 | } 109 | }]) 110 | def test_last_password__1_pass_history(self): 111 | user1 = self.create_user(1) 112 | user_config_1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 113 | PasswordHistory.objects.filter(user_config=user_config_1).delete() 114 | 115 | # This password cannot be taken during validation. It is out of range. 116 | # It is the oldest so out of range. 117 | self.user_change_password(user_number=1, password_number=2) 118 | self.user_change_password(user_number=1, password_number=1) 119 | self.user_change_password(user_number=1, password_number=3) 120 | 121 | # Passwords known in the scope we are checking 122 | self.assert_password_validation_False(user_number=1, password_number=2) 123 | # Passwords known in the scope we are checking 124 | self.assert_password_validation_False(user_number=1, password_number=1) 125 | # Passwords known in the scope we are checking 126 | self.assert_password_validation_False(user_number=1, password_number=3) 127 | 128 | # New unknown password 129 | self.assert_password_validation_True(user_number=1, password_number=4) 130 | 131 | self.assertEqual(PasswordHistory.objects.filter(user_config__user=user1).count(), 3) 132 | 133 | @override_settings(AUTH_PASSWORD_VALIDATORS=[{ 134 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 135 | 'OPTIONS': { 136 | 'last_passwords': 1 137 | } 138 | }]) 139 | def test_last_password__1_pass_history(self): 140 | user1 = self.create_user(1) 141 | user_config_1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 142 | PasswordHistory.objects.filter(user_config=user_config_1).delete() 143 | 144 | # This password cannot be taken during validation. It is out of range. 145 | # It is the oldest so out of range. 146 | self.user_change_password(user_number=1, password_number=2) 147 | self.user_change_password(user_number=1, password_number=1) 148 | self.user_change_password(user_number=1, password_number=3) 149 | 150 | # Password out of scope. We interpret it as if it had never been entered. 151 | self.assert_password_validation_True(user_number=1, password_number=2) 152 | # Password out of scope. We interpret it as if it had never been entered. 153 | self.assert_password_validation_True(user_number=1, password_number=1) 154 | # Passwords known in the scope we are checking 155 | self.assert_password_validation_False(user_number=1, password_number=3) 156 | 157 | # New unknown password 158 | self.assert_password_validation_True(user_number=1, password_number=4) 159 | 160 | self.assertEqual(PasswordHistory.objects.filter(user_config__user=user1).count(), 1) 161 | 162 | @override_settings(AUTH_PASSWORD_VALIDATORS=[{ 163 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 164 | 'OPTIONS': { 165 | 'last_passwords': 2 166 | } 167 | }]) 168 | def test_last_password__1_pass_history(self): 169 | user1 = self.create_user(1) 170 | user_config_1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 171 | PasswordHistory.objects.filter(user_config=user_config_1).delete() 172 | 173 | # This password cannot be taken during validation. It is out of range. 174 | # It is the oldest so out of range. 175 | self.user_change_password(user_number=1, password_number=2) 176 | self.user_change_password(user_number=1, password_number=1) 177 | self.user_change_password(user_number=1, password_number=3) 178 | 179 | # Password out of scope. We interpret it as if it had never been entered. 180 | self.assert_password_validation_True(user_number=1, password_number=2) 181 | # Passwords known in the scope we are checking 182 | self.assert_password_validation_False(user_number=1, password_number=1) 183 | # Passwords known in the scope we are checking 184 | self.assert_password_validation_False(user_number=1, password_number=3) 185 | 186 | # New unknown password 187 | self.assert_password_validation_True(user_number=1, password_number=4) 188 | 189 | self.assertEqual(PasswordHistory.objects.filter(user_config__user=user1).count(), 2) 190 | 191 | @override_settings(AUTH_PASSWORD_VALIDATORS=[{ 192 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 193 | 'OPTIONS': { 194 | 'last_passwords': 3 195 | } 196 | }]) 197 | def test_last_password__1_pass_history(self): 198 | user1 = self.create_user(1) 199 | user_config_1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 200 | PasswordHistory.objects.filter(user_config=user_config_1).delete() 201 | 202 | # This password cannot be taken during validation. It is out of range. 203 | # It is the oldest so out of range. 204 | self.user_change_password(user_number=1, password_number=2) 205 | self.user_change_password(user_number=1, password_number=1) 206 | self.user_change_password(user_number=1, password_number=3) 207 | 208 | # Passwords known in the scope we are checking 209 | self.assert_password_validation_False(user_number=1, password_number=2) 210 | # Passwords known in the scope we are checking 211 | self.assert_password_validation_False(user_number=1, password_number=1) 212 | # Passwords known in the scope we are checking 213 | self.assert_password_validation_False(user_number=1, password_number=3) 214 | 215 | # New unknown password 216 | self.assert_password_validation_True(user_number=1, password_number=4) 217 | 218 | self.assertEqual(PasswordHistory.objects.filter(user_config__user=user1).count(), 3) 219 | 220 | @override_settings(AUTH_PASSWORD_VALIDATORS=[{ 221 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 222 | 'OPTIONS': { 223 | 'last_passwords': 4 224 | } 225 | }]) 226 | def test_last_password__1_pass_history(self): 227 | user1 = self.create_user(1) 228 | user_config_1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 229 | PasswordHistory.objects.filter(user_config=user_config_1).delete() 230 | 231 | # This password cannot be taken during validation. It is out of range. 232 | # It is the oldest so out of range. 233 | self.user_change_password(user_number=1, password_number=2) 234 | self.user_change_password(user_number=1, password_number=1) 235 | self.user_change_password(user_number=1, password_number=3) 236 | 237 | # Passwords known in the scope we are checking 238 | self.assert_password_validation_False(user_number=1, password_number=2) 239 | # Passwords known in the scope we are checking 240 | self.assert_password_validation_False(user_number=1, password_number=1) 241 | # Passwords known in the scope we are checking 242 | self.assert_password_validation_False(user_number=1, password_number=3) 243 | 244 | # New unknown password 245 | self.assert_password_validation_True(user_number=1, password_number=4) 246 | 247 | self.assertEqual(PasswordHistory.objects.filter(user_config__user=user1).count(), 3) 248 | 249 | @override_settings(AUTH_PASSWORD_VALIDATORS=[{ 250 | 'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator', 251 | 'OPTIONS': { 252 | 'last_passwords': 2 253 | } 254 | }]) 255 | def test_last_password(self): 256 | user1 = self.create_user(1) 257 | user2 = self.create_user(2) # needed to check if we are not deleting passwords from another user 258 | user_config_1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 259 | user_config_2 = UserPasswordHistoryConfig.objects.filter(user=user2)[0] 260 | PasswordHistory.objects.filter(user_config=user_config_1).delete() 261 | # This password cannot be taken during validation. It is out of range. 262 | # It is the oldest so out of range. 263 | self.user_change_password(user_number=1, password_number=2) 264 | self.user_change_password(user_number=1, password_number=1) 265 | self.user_change_password(user_number=1, password_number=3) 266 | # Password out of scope. We interpret it as if it had never been entered. 267 | self.assert_password_validation_True(user_number=1, password_number=2) 268 | # New unknown password 269 | self.assert_password_validation_True(user_number=1, password_number=4) 270 | # Passwords known in the scope we are checking 271 | self.assert_password_validation_False(user_number=1, password_number=1) 272 | self.assert_password_validation_False(user_number=1, password_number=3) 273 | 274 | self.assertEqual(PasswordHistory.objects.filter(user_config=user_config_1).count(), 2) 275 | self.assertEqual(PasswordHistory.objects.filter(user_config=user_config_2).count(), 1) 276 | 277 | # Two new non-existent passwords 278 | self.user_change_password(user_number=1, password_number=4) 279 | self.user_change_password(user_number=1, password_number=2) 280 | 281 | # Password out of scope. We interpret it as if it had never been entered. 282 | self.assert_password_validation_True(user_number=1, password_number=1) 283 | self.assert_password_validation_True(user_number=1, password_number=3) 284 | 285 | # Passwords known in the scope we are checking 286 | self.assert_password_validation_False(user_number=1, password_number=4) 287 | self.assert_password_validation_False(user_number=1, password_number=2) 288 | 289 | self.assertEqual(PasswordHistory.objects.filter(user_config=user_config_1).count(), 2) 290 | 291 | # We check for interaction with the other user 292 | self.assert_password_validation_False(user_number=2, password_number=1) 293 | self.assert_password_validation_True(user_number=2, password_number=2) 294 | self.assert_password_validation_True(user_number=2, password_number=3) 295 | self.assert_password_validation_True(user_number=2, password_number=4) 296 | self.assertEqual(PasswordHistory.objects.filter(user_config=user_config_2).count(), 1) 297 | 298 | def test_last_password__delete_old_passwords__one_user__infinite_passwords_hist(self): 299 | user1 = self.create_user(1) 300 | PasswordHistory.objects.filter(user_config__user=user1).delete() 301 | user1_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 302 | ph1 = PasswordHistory(user_config=user1_uphc1, password='2 user1 hash1') # to delete 303 | ph1.save() 304 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') # to delete 305 | ph2.save() 306 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 307 | ph3.save() 308 | upv = UniquePasswordsValidator(last_passwords=0) 309 | upv.delete_old_passwords(user1) 310 | upv = UniquePasswordsValidator(last_passwords=-1) 311 | upv.delete_old_passwords(user1) 312 | current_passwords = list( 313 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 314 | self.assertEqual( 315 | current_passwords, 316 | [ph1.pk, ph2.pk, ph3.pk], 317 | ) 318 | 319 | def test_last_password__delete_old_passwords__one_user__one_password_hist(self): 320 | user1 = self.create_user(1) 321 | user1_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 322 | ph1 = PasswordHistory(user_config=user1_uphc1, password='2 user1 hash1') # to delete 323 | ph1.save() 324 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') # to delete 325 | ph2.save() 326 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 327 | ph3.save() 328 | upv = UniquePasswordsValidator(last_passwords=1) 329 | upv.delete_old_passwords(user1) 330 | current_passwords = list( 331 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 332 | self.assertEqual( 333 | current_passwords, 334 | [ph3.pk], 335 | ) 336 | 337 | def test_last_password__delete_old_passwords__one_user__two_passwords_hist(self): 338 | user1 = self.create_user(1) 339 | user1_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 340 | ph1 = PasswordHistory(user_config=user1_uphc1, password='2 user1 hash1') # to delete 341 | ph1.save() 342 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') 343 | ph2.save() 344 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 345 | ph3.save() 346 | upv = UniquePasswordsValidator(last_passwords=2) 347 | upv.delete_old_passwords(user1) 348 | current_passwords = list( 349 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 350 | self.assertEqual( 351 | current_passwords, 352 | [ph2.pk, ph3.pk], 353 | ) 354 | 355 | def test_last_password__delete_old_passwords__one_user__three_passwords_hist(self): 356 | user1 = self.create_user(1) 357 | user1_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 358 | ph1 = PasswordHistory(user_config=user1_uphc1, password='2 user1 hash1') 359 | ph1.save() 360 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') 361 | ph2.save() 362 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 363 | ph3.save() 364 | upv = UniquePasswordsValidator(last_passwords=3) 365 | upv.delete_old_passwords(user1) 366 | current_passwords = list( 367 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 368 | self.assertEqual( 369 | current_passwords, 370 | [ph1.pk, ph2.pk, ph3.pk], 371 | ) 372 | 373 | def test_last_password__delete_old_passwords__two_users(self): 374 | user1 = self.create_user(1) 375 | user2 = self.create_user(2) 376 | PasswordHistory.objects.all().delete() 377 | 378 | user1_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 379 | user2_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user2)[0] 380 | 381 | ph1 = PasswordHistory(user_config=user1_uphc1, password='2 user1 hash1') # to delete 382 | ph1.save() 383 | ph1_u2 = PasswordHistory(user_config=user2_uphc1, password='2 user2 hash1') # to delete 384 | ph1_u2.save() 385 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') 386 | ph2.save() 387 | ph2_u2 = PasswordHistory(user_config=user2_uphc1, password='1 user2 hash2') 388 | ph2_u2.save() 389 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 390 | ph3.save() 391 | ph3_u2 = PasswordHistory(user_config=user2_uphc1, password='3 user2 hash3') 392 | ph3_u2.save() 393 | 394 | upv = UniquePasswordsValidator(last_passwords=2) 395 | upv.delete_old_passwords(user1) 396 | 397 | current_passwords = list( 398 | PasswordHistory.objects. \ 399 | filter(user_config__user=user1). \ 400 | values_list('pk', flat=True). \ 401 | order_by('pk') 402 | ) 403 | self.assertEqual( 404 | current_passwords, 405 | [ph2.pk, ph3.pk], 406 | msg='Only the passwords of the first user can be deleted' 407 | ) 408 | 409 | current_passwords = list( 410 | PasswordHistory.objects. \ 411 | filter(user_config__user=user2). \ 412 | values_list('pk', flat=True). \ 413 | order_by('pk') 414 | ) 415 | self.assertEqual( 416 | current_passwords, 417 | [ph1_u2.pk, ph2_u2.pk, ph3_u2.pk], 418 | msg='Password history of the other user must be unchanged' 419 | ) 420 | 421 | def test_last_password__delete_old_passwords__multiple_UserPasswordHistoryConfig(self): 422 | user1 = self.create_user(1) 423 | PasswordHistory.objects.all().delete() 424 | 425 | user1_uphc1 = UserPasswordHistoryConfig.objects.filter(user=user1)[0] 426 | user1_uphc2 = UserPasswordHistoryConfig(user=user1, salt='qwerty', iterations=10) 427 | user1_uphc2.save() 428 | 429 | ph1 = PasswordHistory(user_config=user1_uphc1, password='2 user1 hash1') # to delete 430 | ph1.save() 431 | ph2 = PasswordHistory(user_config=user1_uphc2, password='1 user1 hash2') 432 | ph2.save() 433 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 434 | ph3.save() 435 | upv = UniquePasswordsValidator(last_passwords=2) 436 | upv.delete_old_passwords(user1) 437 | current_passwords = list( 438 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 439 | self.assertEqual( 440 | current_passwords, 441 | [ph2.pk, ph3.pk], 442 | msg='Only the oldest password can be deleted = ph1' 443 | ) 444 | PasswordHistory.objects.all().delete() 445 | 446 | ph1 = PasswordHistory(user_config=user1_uphc2, password='2 user1 hash1') # to delete 447 | ph1.save() 448 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') 449 | ph2.save() 450 | ph3 = PasswordHistory(user_config=user1_uphc1, password='3 user1 hash3') 451 | ph3.save() 452 | upv = UniquePasswordsValidator(last_passwords=2) 453 | upv.delete_old_passwords(user1) 454 | current_passwords = list( 455 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 456 | self.assertEqual( 457 | current_passwords, 458 | [ph2.pk, ph3.pk], 459 | msg='Only the oldest password can be deleted = ph1' 460 | ) 461 | PasswordHistory.objects.all().delete() 462 | 463 | ph1 = PasswordHistory(user_config=user1_uphc2, password='2 user1 hash1') # to delete 464 | ph1.save() 465 | ph2 = PasswordHistory(user_config=user1_uphc1, password='1 user1 hash2') 466 | ph2.save() 467 | ph3 = PasswordHistory(user_config=user1_uphc2, password='3 user1 hash3') 468 | ph3.save() 469 | upv = UniquePasswordsValidator(last_passwords=2) 470 | upv.delete_old_passwords(user1) 471 | current_passwords = list( 472 | PasswordHistory.objects.filter(user_config__user=user1).values_list('pk', flat=True).order_by('pk')) 473 | self.assertEqual( 474 | current_passwords, 475 | [ph2.pk, ph3.pk], 476 | msg='Only the oldest password can be deleted = ph1' 477 | ) 478 | PasswordHistory.objects.all().delete() 479 | -------------------------------------------------------------------------------- /tests/test_project/urls.py: -------------------------------------------------------------------------------- 1 | """test_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | import django 17 | 18 | if django.VERSION[0] >= 3: 19 | from django.urls import re_path as url 20 | else: 21 | from django.conf.urls import url 22 | 23 | from django.contrib import admin 24 | 25 | urlpatterns = [ 26 | url(r'^admin/', admin.site.urls), 27 | ] 28 | -------------------------------------------------------------------------------- /tests/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py37,py38,py39,py310}-django32 4 | {py38,py39,py310}-django40 5 | {py38,py39,py310,py311}-django41 6 | {py38,py39,py310,py311,py312}-django42 7 | {py310,py311,py312}-django50 8 | 9 | setenv = 10 | PYTHONPATH = {toxinidir}:{toxinidir}/tests/ 11 | 12 | [testenv] 13 | deps = 14 | django32: Django >= 3.2, < 3.3 15 | django40: Django >= 4.0, < 4.1 16 | django41: Django >= 4.1, < 4.2 17 | django42: Django >= 4.2, < 4.3 18 | django50: Django == 5.0 19 | 20 | commands = 21 | python -V 22 | python {toxinidir}/tests/manage.py test 23 | 24 | 25 | --------------------------------------------------------------------------------