diff --git a/.github/workflows/deploy_to_pypi.yaml b/.github/workflows/deploy_to_pypi.yaml new file mode 100644 index 0000000..a4dc51a --- /dev/null +++ b/.github/workflows/deploy_to_pypi.yaml @@ -0,0 +1,70 @@ +name: deploy_to_pypi + +on: + push: + tags: + - "v*" + +jobs: + build-n-publish: + name: Build and publish Python distributions to PyPI and TestPyPI + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@release + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Build source and wheel distributions + run: | + python -m pip install --upgrade build twine + python -m build + twine check --strict dist/* + + - name: Publish distribution to Test PyPI + uses: pypa/gh-action-pypi-publish@release + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + skip_existing: true + + - name: Publish distribution to PyPI + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + skip_existing: true + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v3 + env: + GITHUB-TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false + + - name: Get Asset name + run: | + export PKG=$(ls dist/ | grep tar) + set -- $PKG + echo "name=$1" >> $GITHUB_ENV + + - name: Upload Release Asset (sdist) to GitHub + id: upload-release-asset + uses: actions/upload-release-asset@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: dist/${{ env.name }} + asset_name: ${{ env.name }} + asset_content_type: application/zip \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 19a3820..2960010 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,37 +1,73 @@ name: test -on: [push, pull_request] +on: + push: + branches: + - "main" + - "release" + pull_request: + branches: + - "*" + +env: + GITHUB-TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: + black-lint: + name: Lint with Black + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: psf/black@fcf97961061982656a1384ecc1628e217a52a88c + test: name: Test django-advanced-password-validation runs-on: ubuntu-latest strategy: matrix: versions: + - { "djangoVersion": "2.2.28", "pythonVersion": "3.7" } + - { "djangoVersion": "2.2.28", "pythonVersion": "3.8" } + - { "djangoVersion": "2.2.28", "pythonVersion": "3.9" } + - { "djangoVersion": "2.2.28", "pythonVersion": "3.10" } + - { "djangoVersion": "3.2.16", "pythonVersion": "3.7" } + - { "djangoVersion": "3.2.16", "pythonVersion": "3.8" } + - { "djangoVersion": "3.2.16", "pythonVersion": "3.9" } + - { "djangoVersion": "3.2.16", "pythonVersion": "3.10" } + - { "djangoVersion": "4.0.8", "pythonVersion": "3.8" } + - { "djangoVersion": "4.0.8", "pythonVersion": "3.9" } + - { "djangoVersion": "4.0.8", "pythonVersion": "3.10" } + - { "djangoVersion": "4.1.2", "pythonVersion": "3.8" } + - { "djangoVersion": "4.1.2", "pythonVersion": "3.9" } - { "djangoVersion": "4.1.2", "pythonVersion": "3.10" } steps: - - name: Checkout ๐Ÿ›Ž๏ธ + # Checkout the source + - name: Checkout uses: actions/checkout@v3 - - name: Set up Python ๐Ÿ + # Setup Python + - name: Set up Python uses: actions/setup-python@v3 with: python-version: ${{ matrix.versions.pythonVersion }} - - name: Install dependencies ๐Ÿ“ฆ + # Install Dependencies + - name: Install dependencies run: python -m pip install -r requirements_test.txt && python -m pip install -e . - - name: Install Django ${{ matrix.versions.djangoVersion }} ๐Ÿ“ฆ + # Install Django + - name: Install Django ${{ matrix.versions.djangoVersion }} run: python -m pip install Django==${{ matrix.versions.djangoVersion }} - - name: Check types, syntax and duckstrings ๐Ÿฆ† + # Check syntax + - name: Check types, syntax and duckstrings run: | mypy --exclude=setup.py . flake8 . interrogate --quiet --fail-under=90 . - - name: Test Django ${{ matrix.versions.djangoVersion }} with coverage ๐Ÿงช + # Test package + - name: Test Django ${{ matrix.versions.djangoVersion }} with coverage run: coverage run --source=django_advanced_password_validation -m pytest . && coverage lcov -o coverage.lcov - - name: Submit coverage report to Coveralls ๐Ÿ“ˆ + # Generate Coverage Report + - name: Submit coverage report to Coveralls if: ${{ success() }} uses: coverallsapp/github-action@1.1.3 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./coverage.lcov - diff --git a/README.md b/README.md index cd531ac..f464cce 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,50 @@ -# django-advanced_password_validation +# django_advanced_password_validation -Extends Django password validation options to include minimum uppercase, minimum lowercase, minimum numerical, and minimum special characters. This was created in an attempt to keep up with industry standards for strong user passwords. +[![test](https://github.com/ezrajrice/django_advanced_password_validation/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/ezrajrice/django_advanced_password_validation/actions/workflows/test.yaml) +[![Coverage Status](https://coveralls.io/repos/github/ezrajrice/django_advanced_password_validation/badge.svg?branch=main)](https://coveralls.io/github/ezrajrice/django_advanced_password_validation?branch=main) -This package works for both python 3.x and 2.x versions. +Extends Django password validation options to include minimum uppercase, minimum lowercase, minimum numerical, and minimum special characters. This was created in an attempt to keep up with industry standards for strong user passwords. -> **_NOTE:_** As of January 01, 2020 python 2.x has been deprecated and will no longer receive continued support. See [Python 2.x EOL](https://www.python.org/doc/sunset-python-2/) for more details. +This package works for python 3.6+. -### Prerequisites +## Prerequisites -Requires Django 1.11 or later. +Requires Django 2.2 or later. You can install the latest version of Django via pip: -``` -$ pip install django +```bash +pip install django ``` Alternatively, you can install a specific version of Django via pip: -``` -$ pip install django=2.2 +```bash +pip install django=3.2 ``` > **_NOTE:_** See the [django-project](https://docs.djangoproject.com) documentation for information on non-deprecated Django versions. -### Installation +## Installation -#### Normal installation +### Normal installation Install django-advanced_password_validation via pip: -``` -$ pip install django-advanced_password_validation +```bash +pip install django-advanced_password_validation ``` -#### Development installation +### Development installation -``` -$ git clone https://github.com/ezrajrice/django-advanced_password_validation.git -$ cd django-advanced_password_validation -$ pip install --editable . +```bash +git clone https://github.com/ezrajrice/django-advanced_password_validation.git +cd django-advanced_password_validation +pip install --editable . ``` ### Usage -The four optional validators must be configured in the settings.py file of your django project. +The optional validators must be configured in the settings.py file of your django project to be actively used in your project. #### /my-cool-project/settings.py @@ -101,7 +102,7 @@ Here is a list of the available options with their default values. ## Authors -* **Ezra Rice** - *Initial work* - [ezrajrice](https://github.com/ezrajrice) +* **Ezra Rice** - _Initial work_ - [ezrajrice](https://github.com/ezrajrice) ## License @@ -109,4 +110,5 @@ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md ## Acknowledgments -* **Victor Semionov** - *Contributor* - [vsemionov](https://github.com/vsemionov) +* **Victor Semionov** - _Contributor_ - [vsemionov](https://github.com/vsemionov) +* **Mostafa Moradian** - _Contributor_ - [mostafa](https://github.com/mostafa) diff --git a/django_advanced_password_validation/advanced_password_validation.py b/django_advanced_password_validation/advanced_password_validation.py index 8cada5e..8a4d83e 100644 --- a/django_advanced_password_validation/advanced_password_validation.py +++ b/django_advanced_password_validation/advanced_password_validation.py @@ -47,14 +47,11 @@ def get_help_text(self): """ Get the help text for the validator. """ - return ( - _( - f"Your password must contain at least {self.min_digits} number.", - f"Your password must contain at least {self.min_digits} numbers.", - self.min_digits, - ) - % {"min_digits": self.min_digits} - ) + return _( + f"Your password must contain at least {self.min_digits} number.", + f"Your password must contain at least {self.min_digits} numbers.", + self.min_digits, + ) % {"min_digits": self.min_digits} class ContainsUppercaseValidator: @@ -98,14 +95,11 @@ def get_help_text(self): """ Get the help text for the validator. """ - return ( - _( - f"Your password must contain at least {self.min_uppercase} uppercase character.", - f"Your password must contain at least {self.min_uppercase} uppercase characters.", - self.min_uppercase, - ) - % {"min_uppercase": self.min_uppercase} - ) + return _( + f"Your password must contain at least {self.min_uppercase} uppercase character.", + f"Your password must contain at least {self.min_uppercase} uppercase characters.", + self.min_uppercase, + ) % {"min_uppercase": self.min_uppercase} class ContainsLowercaseValidator: @@ -149,14 +143,11 @@ def get_help_text(self): """ Get the help text for the validator. """ - return ( - _( - f"Your password must contain at least {self.min_lowercase} lowercase character.", - f"Your password must contain at least {self.min_lowercase} lowercase characters.", - self.min_lowercase, - ) - % {"min_lowercase": self.min_lowercase} - ) + return _( + f"Your password must contain at least {self.min_lowercase} lowercase character.", + f"Your password must contain at least {self.min_lowercase} lowercase characters.", + self.min_lowercase, + ) % {"min_lowercase": self.min_lowercase} class ContainsSpecialCharactersValidator: @@ -201,14 +192,11 @@ def get_help_text(self): """ Get the help text for the validator. """ - return ( - _( - f"Your password must contain at least {self.min_characters} special character.", - f"Your password must contain at least {self.min_characters} special characters.", - self.min_characters, - ) - % {"min_characters": self.min_characters} - ) + return _( + f"Your password must contain at least {self.min_characters} special character.", + f"Your password must contain at least {self.min_characters} special characters.", + self.min_characters, + ) % {"min_characters": self.min_characters} class MaximumLengthValidator: @@ -251,19 +239,16 @@ def get_help_text(self): """ Get the help text for the validator. """ - return ( - _( - f"Password must contain at maximum {self.max_length} character.", - f"Password must contain at maximum {self.max_length} characters.", - self.max_length, - ) - % {"max_length": self.max_length} - ) + return _( + f"Password must contain at maximum {self.max_length} character.", + f"Password must contain at maximum {self.max_length} characters.", + self.max_length, + ) % {"max_length": self.max_length} class MaxConsecutiveCharactersValidator: """ - Validates whether the password contains max_consecutive consecutive characters. + Validates whether the password contains more than max_consecutive consecutive characters. """ def __init__(self, max_consecutive=3): @@ -277,7 +262,7 @@ def __init__(self, max_consecutive=3): def validate(self, password, user=None): """ - Validates whether the password contains max_consecutive consecutive characters. + Validates whether the password contains more than max_consecutive consecutive characters. Args: password (str): The password to validate. @@ -289,7 +274,7 @@ def validate(self, password, user=None): """ for c in password: if password.count(c) >= self.max_consecutive: - check = c * self.max_consecutive + check = c * (self.max_consecutive + 1) if check in password: raise ValidationError( gettext( diff --git a/django_advanced_password_validation/tests/test_validators.py b/django_advanced_password_validation/tests/test_validators.py index 845d3e3..ee04900 100644 --- a/django_advanced_password_validation/tests/test_validators.py +++ b/django_advanced_password_validation/tests/test_validators.py @@ -32,6 +32,16 @@ def test_contains_digits_validator(): assert exc.value.message == "Password must contain at least 1 number." +def test_contains_digits_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = ContainsDigitsValidator(min_digits=1) + assert validator.get_help_text() == "Your password must contain at least 1 number." + validator = ContainsDigitsValidator(min_digits=2) + assert validator.get_help_text() == "Your password must contain at least 2 numbers." + + def test_contains_uppercase_validator(): """ Test that the ContainsUppercaseValidator works as expected and raises a @@ -45,6 +55,22 @@ def test_contains_uppercase_validator(): assert exc.value.message == "Password must contain at least 1 uppercase character." +def test_contains_uppercase_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = ContainsUppercaseValidator(min_uppercase=1) + assert ( + validator.get_help_text() + == "Your password must contain at least 1 uppercase character." + ) + validator = ContainsUppercaseValidator(min_uppercase=2) + assert ( + validator.get_help_text() + == "Your password must contain at least 2 uppercase characters." + ) + + def test_contains_lowercase_validator(): """ Test that the ContainsLowercaseValidator works as expected and raises a @@ -58,6 +84,22 @@ def test_contains_lowercase_validator(): assert exc.value.message == "Password must contain at least 1 lowercase character." +def test_contains_lowercase_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = ContainsLowercaseValidator(min_lowercase=1) + assert ( + validator.get_help_text() + == "Your password must contain at least 1 lowercase character." + ) + validator = ContainsLowercaseValidator(min_lowercase=2) + assert ( + validator.get_help_text() + == "Your password must contain at least 2 lowercase characters." + ) + + def test_contains_special_characters_validator(): """ Test that the ContainsSpecialCharactersValidator works as expected and raises a @@ -71,6 +113,22 @@ def test_contains_special_characters_validator(): assert exc.value.message == "Password must contain at least 1 special character." +def test_contains_special_characters_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = ContainsSpecialCharactersValidator(min_characters=1) + assert ( + validator.get_help_text() + == "Your password must contain at least 1 special character." + ) + validator = ContainsSpecialCharactersValidator(min_characters=2) + assert ( + validator.get_help_text() + == "Your password must contain at least 2 special characters." + ) + + def test_maximum_length_validator(): """ Test that the MaximumLengthValidator works as expected and raises a @@ -83,6 +141,16 @@ def test_maximum_length_validator(): assert exc.value.message == "Password must contain at maximum 10 characters." +def test_maximum_length_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = MaximumLengthValidator(max_length=12) + assert ( + validator.get_help_text() == "Password must contain at maximum 12 characters." + ) + + def test_max_consecutive_characters_validator(): """ Test that the MaxConsecutiveCharactersValidator works as expected and raises a @@ -91,22 +159,35 @@ def test_max_consecutive_characters_validator(): """ validator = MaxConsecutiveCharactersValidator() assert validator.validate("abcdefghij") is None + assert validator.validate("aaabbbccc") is None with pytest.raises(ValidationError) as exc: - validator.validate("aaabbbccc") + validator.validate("aaaabbbccc") assert ( exc.value.message == "Password contains consecutively repeating characters. e.g 'aaa' or '111'" ) +def test_max_consecutive_characters_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = MaxConsecutiveCharactersValidator() + assert ( + validator.get_help_text() + == "Password cannot contain consecutively repeating characters. e.g 'aaa' or '111'" + ) + + def test_consecutively_increasing_digit_validator(): """ Test that the ConsecutivelyIncreasingDigitValidator works as expected and raises a ValidationError when the password contains more than the maximum number of consecutive characters. """ - validator = ConsecutivelyIncreasingDigitValidator() + validator = ConsecutivelyIncreasingDigitValidator(max_consecutive=3) assert validator.validate("abcdefghij") is None + assert validator.validate("abcdefg123") is None with pytest.raises(ValidationError) as exc: validator.validate("1234567890") assert ( @@ -115,14 +196,26 @@ def test_consecutively_increasing_digit_validator(): ) +def test_consecutively_increasing_digit_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = ConsecutivelyIncreasingDigitValidator(max_consecutive=3) + assert ( + validator.get_help_text() + == "Password cannot contain consecutively increasing digits. e.g '12345'" + ) + + def test_consecutively_decreasing_digit_validator(): """ Test that the ConsecutivelyDecreasingDigitValidator works as expected and raises a ValidationError when the password contains more than the maximum number of consecutive characters. """ - validator = ConsecutivelyDecreasingDigitValidator() + validator = ConsecutivelyDecreasingDigitValidator(max_consecutive=3) assert validator.validate("abcdefghij") is None + assert validator.validate("abcdefg321") is None with pytest.raises(ValidationError) as exc: validator.validate("9876543210") assert ( @@ -131,6 +224,17 @@ def test_consecutively_decreasing_digit_validator(): ) +def test_consecutively_decreasing_digit_get_help_text(): + """ + Test that the get_help_text string works as expected. + """ + validator = ConsecutivelyDecreasingDigitValidator(max_consecutive=3) + assert ( + validator.get_help_text() + == "Password cannot contain consecutively decreasing digits. e.g '54321'" + ) + + def test_valid_password(): """ Test that the validate_password function works as expected. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..236e5c8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=65", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "django_advanced_password_validation" +description = "Extends Django password validation options in an attempt to keep up with industry standards for strong user passwords." +readme = "README.md" +license = {file = "LICENSE.txt"} +keywords = ["django", "password", "validator"] +authors = [ + {name = "Ezra Rice", email = "ezra.j.rice@gmail.com"}, +] +classifiers = [ + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", +] +dynamic = ["version"] + +[project.urls] +repository = "https://github.com/ezrajrice/django_advanced_password_validation" \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 459c5a5..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -from setuptools import setup, find_packages - -with open("README.md", encoding="utf-8") as readme_file: - README = readme_file.read() - -with open("HISTORY.md", encoding="utf-8") as history_file: - HISTORY = history_file.read() - -setup_args = dict( - name="django_advanced_password_validation", - version="1.1.0", - description="Extends Django password validation options to include minimum uppercase, " - "lowercase, numerical, special characters, maximum length, maximum consecutive " - "characters, maximum consecutively increasing digits, and maximum consecutively " - "decreasing digits.", - long_description_content_type="text/markdown", - long_description=README + "\n\n" + HISTORY, - license="MIT", - packages=find_packages(), - author="Ezra Rice", - author_email="ezra.j.rice@gmail.com", - keywords=["django", "password", "validator"], - url="https://github.com/ezrajrice/django-advanced_password_validation.git", - download_url="https://pypi.org/project/django-advanced_password_validation", -) - -install_requires = ["Django>=1.11"] - -if __name__ == "__main__": - setup(**setup_args, install_requires=install_requires)