diff --git a/.editorconfig b/.editorconfig index 568b397..612e8db 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org root = true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ef07d7..61d4eac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,8 +6,14 @@ on: - master - develop pull_request: + workflow_dispatch: jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 tests: name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63d431e..a92a4a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,36 +8,14 @@ repos: entry: 'breakpoint\(\)|set_trace' language: pygrep - - repo: https://github.com/myint/autoflake - rev: 'v1.4' - hooks: - - id: autoflake - args: ['--in-place', '--remove-all-unused-imports',] - - - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: 'v1.6.0' - hooks: - - id: autopep8 - args: ['--in-place', '-aaa',] - - - repo: https://github.com/hadialqattan/pycln - rev: 'v1.1.0' - hooks: - - id: pycln - - - repo: https://github.com/pre-commit/mirrors-isort - rev: 'v5.10.1' # Use the revision sha / tag you want to point at - hooks: - - id: isort - args: ["--profile", "black"] - - - repo: https://github.com/psf/black - rev: '22.3.0' - hooks: - - id: black + - repo: https://github.com/adamchainz/django-upgrade + rev: "1.20.0" + hooks: + - id: django-upgrade + args: [--target-version, "4.2"] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 'v4.1.0' + rev: 'v4.6.0' hooks: - id: check-yaml - id: check-toml @@ -45,28 +23,11 @@ repos: - id: trailing-whitespace - id: mixed-line-ending - - repo: https://github.com/PyCQA/flake8 - rev: '6.0.0' - hooks: - - id: flake8 - args: ["--exclude=*/migrations/*"] - -# -# - repo: local -# hooks: -# - id: pylint -# name: pylint -# entry: pylint -# language: system -# types: [python] -# args: -# [ -# "-rn", # Only display messages -# "-sn", # Don't display the score -# ] -# - - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.5 hooks: - - id: pyupgrade - args: ["--py37-plus"] + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/HISTORY.rst b/HISTORY.rst index 1b8ec85..b365038 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,9 +1,16 @@ +Unreleased +---------- + +* Remove unsupported Django code +* Added ``__str__()`` definitions for models +* Use path instead of re_path for some URLs + 0.8.6 ----- * Django 5 support * default to JSONSerializer - + 0.8.4 ----- diff --git a/README.rst b/README.rst index 6434bfb..f975188 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ django-password-policies provides unicode-aware password policies on password changes and resets and a mechanism to force password changes. -As for now (Jan 2021), this fork is actively maintained by |iplweb| +As for now (Jan 2021), this fork is actively maintained by `IPLweb`_. .. |travis| image:: https://travis-ci.org/iplweb/django-password-policies.svg?branch=master :target: https://travis-ci.org/iplweb/django-password-policies-iplweb @@ -22,9 +22,9 @@ As for now (Jan 2021), this fork is actively maintained by |iplweb| .. _requirements: Requirements -============= +============ -This application requires `Django`_ 2.2 or newer +This application requires `Django`_ 4.2 and Python 3.8 or newer. .. _documentation: @@ -33,6 +33,6 @@ Documentation A detailled documentation is available on `the project's GitHub Pages`_. -.. _`the project's GitHub Pages`: http://github.com/iplweb/django-password-policies-iplweb +.. _`the project's GitHub Pages`: https://iplweb.github.io/django-password-policies-iplweb/ .. _`Django`: https://www.djangoproject.com/ -.. -`IPLweb on github`: https://github.com/iplweb/ +.. _`IPLweb`: https://github.com/iplweb/ diff --git a/docs/_ext/__init__.py b/docs/_ext/__init__.py index 8b13789..e69de29 100644 --- a/docs/_ext/__init__.py +++ b/docs/_ext/__init__.py @@ -1 +0,0 @@ - diff --git a/docs/_ext/exts.py b/docs/_ext/exts.py index aa0504f..de3f405 100644 --- a/docs/_ext/exts.py +++ b/docs/_ext/exts.py @@ -1,137 +1,128 @@ import inspect -from django.utils.html import strip_tags -from django.utils.encoding import force_unicode -from fields import model_fields -from fields import model_meta_fields +from django.utils.encoding import force_str +from django.utils.html import strip_tags +from fields import model_fields, model_meta_fields def process_docstring(app, what, name, obj, options, lines): # This causes import errors if left outside the function - from django.db import models from django import forms + from django.db import models # Only look at objects that inherit from Django's base model class if inspect.isclass(obj) and issubclass(obj, models.Model): # Grab the field list from the meta class fields = obj._meta.fields - lines.append(u'') + lines.append("") for field in fields: # Do not document AutoFields - if type(field).__name__ == 'AutoField' and field.primary_key: + if type(field).__name__ == "AutoField" and field.primary_key: continue - k = type(field).__name__ # Decode and strip any html out of the field's help text - help_text = strip_tags(force_unicode(field.help_text)) + help_text = strip_tags(force_str(field.help_text)) - # Decode and capitalize the verbose name, for use if there isn't - # any help text - verbose_name = force_unicode(field.verbose_name).capitalize() - - lines.append(u'.. attribute:: %s' % field.name) - lines.append(u' ') + lines.append(f".. attribute:: {field.name}") + lines.append(" ") # Add the field's type to the docstring if isinstance(field, models.ForeignKey): - to = field.rel.to - l = u' %s(\':class:`~%s.%s`\')' % (type(field).__name__, - to.__module__, - to.__name__) + to = field.related_model + msg = f" %s(':class:`~{type(field).__name__}.{to.__module__}`')" elif isinstance(field, models.OneToOneField): - to = field.rel.to - l = u' %s(\':class:`~%s.%s`\')' % (type(field).__name__, - to.__module__, - to.__name__) + to = field.related_model + msg = f" {type(field).__name__}(':class:`~{to.__module__}.{to.__name__}`')" else: - l = u' %s' % type(field).__name__ + msg = f" {type(field).__name__}" if not field.blank: - l = l + ' (Required)' - if hasattr(field, 'auto_now') and field.auto_now: - l = l + ' (Automatically set when updated)' - if hasattr(field, 'auto_now_add') and field.auto_now_add: - l = l + ' (Automatically set when created)' - lines.append(l) + msg = msg + " (Required)" + if hasattr(field, "auto_now") and field.auto_now: + msg = msg + " (Automatically set when updated)" + if hasattr(field, "auto_now_add") and field.auto_now_add: + msg = msg + " (Automatically set when created)" + lines.append(msg) if help_text: - lines.append(u'') + lines.append("") # Add the model field to the end of the docstring as a param # using the help text as the description - lines.append(u' %s' % help_text) - lines.append(u' ') + lines.append(f" {help_text}") + lines.append(" ") f = model_fields[type(field).__name__] - for key in sorted(f.iterkeys()): - - if hasattr(field, key) and getattr(field, key) != f[key] and getattr(field, key): + for key in sorted(f.keys()): + if ( + hasattr(field, key) + and getattr(field, key) != f[key] + and getattr(field, key) + ): attr = getattr(field, key) - if key == 'error_messages': + if key == "error_messages": error_dict = {} - for i in sorted(attr.iterkeys()): - error_dict[i] = force_unicode(attr[i]) + for i in sorted(attr.keys()): + error_dict[i] = force_str(attr[i]) attr = error_dict - if key == 'validators': + if key == "validators": v = [] for i in sorted(attr): - n = ':class:`~%s.%s`' % (type(i).__module__, - type(i).__name__) + n = f":class:`~{type(i).__module__}.{type(i).__name__}`" v.append(n) attr = v - lines.append(u' :param %s: %s' % (key, attr)) - lines.append(u'') - lines.append(u'.. attribute:: Meta') - lines.append(u'') - for key in sorted(model_meta_fields.iterkeys()): - if hasattr(obj._meta, key) and getattr(obj._meta, key) != model_meta_fields[key]: - lines.append(u' %s = %s' % (key, getattr(obj._meta, key))) - lines.append(u'') - + lines.append(f" :param {key}: {attr}") + lines.append("") + lines.append(".. attribute:: Meta") + lines.append("") + for key in sorted(model_meta_fields.keys()): + if ( + hasattr(obj._meta, key) + and getattr(obj._meta, key) != model_meta_fields[key] + ): + lines.append(f" {key} = {getattr(obj._meta, key)}") + lines.append("") # Only look at objects that inherit from Django's base model class if inspect.isclass(obj): if issubclass(obj, forms.Form) or issubclass(obj, forms.ModelForm): # Grab the field list from the meta class fields = obj.base_fields - lines.append(u'') + lines.append("") for field in fields: f = obj.base_fields[field] # Decode and strip any html out of the field's help text - if hasattr(f, 'help_text'): - help_text = strip_tags(force_unicode(f.help_text)) - # Decode and capitalize the verbose name, for use if there isn't - # any help text - label = force_unicode(f.label).capitalize() + if hasattr(f, "help_text"): + help_text = strip_tags(force_str(f.help_text)) - lines.append(u'.. attribute:: %s' % field) - lines.append(u'') + lines.append(f".. attribute:: {field}") + lines.append("") # Add the field's type to the docstring field_inst = obj.base_fields[field] - l = u' :class:`~%s.%s`' % (type(field_inst).__module__, - type(field_inst).__name__) + info = f" :class:`~{type(field_inst).__module__}.{type(field_inst).__name__}`" if field_inst.required: - l = l + ' (Required)' - lines.append(l) - lines.append(u'') - if hasattr(f, 'error_messages') and f.error_messages: + info = info + " (Required)" + lines.append(info) + lines.append("") + if hasattr(f, "error_messages") and f.error_messages: msgs = {} for key, value in f.error_messages.items(): - msgs[key] = force_unicode(value) - lines.append(u':kwarg error_messages: %s' % msgs) + msgs[key] = force_str(value) + lines.append(f":kwarg error_messages: {msgs}") if f.help_text: # Add the model field to the end of the docstring as a param # using the help text as the description - lines.append(u':kwarg help_text: %s' % help_text) - if hasattr(f, 'initial') and f.initial: - lines.append(u':kwarg initial: %s' % f.initial) - if hasattr(f, 'localize'): - lines.append(u':kwarg localize: %s' % f.localize) - if hasattr(f, 'validators') and f.validators: - l = [] + lines.append(f":kwarg help_text: {help_text}") + if hasattr(f, "initial") and f.initial: + lines.append(f":kwarg initial: {f.initial}") + if hasattr(f, "localize"): + lines.append(f":kwarg localize: {f.localize}") + if hasattr(f, "validators") and f.validators: + line_array = [] for v in f.validators: - l.append(':class:`~%s.%s`' % (type(v).__module__, - type(v).__name__)) - lines.append(u':kwarg validators: %s' % l) - lines.append(u':kwarg widget: %s' % type(f.widget).__name__) - lines.append(u'') + line_array.append( + f":class:`~{type(v).__module__}.{type(v).__name__}`" + ) + lines.append(f":kwarg validators: {line_array}") + lines.append(f":kwarg widget: {type(f.widget).__name__}") + lines.append("") # Return the extended docstring return lines @@ -139,74 +130,74 @@ def process_docstring(app, what, name, obj, options, lines): def setup(app): # Register the docstring processor with sphinx - app.connect('autodoc-process-docstring', process_docstring) + app.connect("autodoc-process-docstring", process_docstring) app.add_crossref_type( - directivename = "admin", - rolename = "admin", - indextemplate = "pair: %s; admin", + directivename="admin", + rolename="admin", + indextemplate="pair: %s; admin", ) app.add_crossref_type( - directivename = "command", - rolename = "command", - indextemplate = "pair: %s; command", + directivename="command", + rolename="command", + indextemplate="pair: %s; command", ) app.add_crossref_type( - directivename = "context_processors", - rolename = "context_processors", - indextemplate = "pair: %s; context_processors", + directivename="context_processors", + rolename="context_processors", + indextemplate="pair: %s; context_processors", ) app.add_crossref_type( - directivename = "form", - rolename = "form", - indextemplate = "pair: %s; form", + directivename="form", + rolename="form", + indextemplate="pair: %s; form", ) app.add_crossref_type( - directivename = "formfield", - rolename = "formfield", - indextemplate = "pair: %s; formfield", + directivename="formfield", + rolename="formfield", + indextemplate="pair: %s; formfield", ) app.add_crossref_type( - directivename = "manager", - rolename = "manager", - indextemplate = "pair: %s; manager", + directivename="manager", + rolename="manager", + indextemplate="pair: %s; manager", ) app.add_crossref_type( - directivename = "middleware", - rolename = "middleware", - indextemplate = "pair: %s; middleware", + directivename="middleware", + rolename="middleware", + indextemplate="pair: %s; middleware", ) app.add_crossref_type( - directivename = "model", - rolename = "model", - indextemplate = "pair: %s; model", + directivename="model", + rolename="model", + indextemplate="pair: %s; model", ) app.add_crossref_type( - directivename = "setting", - rolename = "setting", - indextemplate = "pair: %s; setting", + directivename="setting", + rolename="setting", + indextemplate="pair: %s; setting", ) app.add_crossref_type( - directivename = "settings", - rolename = "settings", - indextemplate = "pair: %s; settings", + directivename="settings", + rolename="settings", + indextemplate="pair: %s; settings", ) app.add_crossref_type( - directivename = "signal", - rolename = "signal", - indextemplate = "pair: %s; signal", + directivename="signal", + rolename="signal", + indextemplate="pair: %s; signal", ) app.add_crossref_type( - directivename = "token", - rolename = "token", - indextemplate = "pair: %s; token", + directivename="token", + rolename="token", + indextemplate="pair: %s; token", ) app.add_crossref_type( - directivename = "validator", - rolename = "validator", - indextemplate = "pair: %s; validator", + directivename="validator", + rolename="validator", + indextemplate="pair: %s; validator", ) app.add_crossref_type( - directivename = "view", - rolename = "view", - indextemplate = "pair: %s; view", + directivename="view", + rolename="view", + indextemplate="pair: %s; view", ) diff --git a/docs/_ext/fields.py b/docs/_ext/fields.py index d98c778..382e384 100644 --- a/docs/_ext/fields.py +++ b/docs/_ext/fields.py @@ -1,167 +1,194 @@ from django.db import models from django.db.models.fields import NOT_PROVIDED -_core_model_field_attr = {'blank': False, - 'choices': [], - 'db_column': None, - 'db_index': False, - 'db_tablespace': None, - 'default': NOT_PROVIDED, - 'editable': True, - 'error_messages': None, - 'help_text': None, - 'null': False, - 'primary_key': False, - 'unique': False, - 'validators': None, - 'verbose_name': None} +_core_model_field_attr = { + "blank": False, + "choices": [], + "db_column": None, + "db_index": False, + "db_tablespace": None, + "default": NOT_PROVIDED, + "editable": True, + "error_messages": None, + "help_text": None, + "null": False, + "primary_key": False, + "unique": False, + "validators": None, + "verbose_name": None, +} -_model_fields = {'AutoField': _core_model_field_attr, - 'BigIntegerField': _core_model_field_attr, - 'BooleanField': _core_model_field_attr, - 'CharField': _core_model_field_attr, - 'CommaSeparatedIntegerField': _core_model_field_attr, - 'DateField': _core_model_field_attr, - 'DateTimeField': _core_model_field_attr, - 'DecimalField': _core_model_field_attr, - 'EmailField': _core_model_field_attr, - 'FileField': _core_model_field_attr, - 'FilePathField': _core_model_field_attr, - 'FloatField': _core_model_field_attr, - 'ForeignKey': _core_model_field_attr, - 'GenericIPAddressField': _core_model_field_attr, - 'IPAddressField': _core_model_field_attr, - 'ImageField': _core_model_field_attr, - 'IntegerField': _core_model_field_attr, - 'ManyToManyField': _core_model_field_attr, - 'NullBooleanField': _core_model_field_attr, - 'OneToOneField': _core_model_field_attr, - 'PositiveIntegerField': _core_model_field_attr, - 'PositiveSmallIntegerField': _core_model_field_attr, - 'SlugField': _core_model_field_attr, - 'SmallIntegerField': _core_model_field_attr, - 'TextField': _core_model_field_attr, - 'TimeField': _core_model_field_attr, - 'URLField': _core_model_field_attr} +_model_fields = { + "AutoField": _core_model_field_attr, + "BigIntegerField": _core_model_field_attr, + "BooleanField": _core_model_field_attr, + "CharField": _core_model_field_attr, + "CommaSeparatedIntegerField": _core_model_field_attr, + "DateField": _core_model_field_attr, + "DateTimeField": _core_model_field_attr, + "DecimalField": _core_model_field_attr, + "EmailField": _core_model_field_attr, + "FileField": _core_model_field_attr, + "FilePathField": _core_model_field_attr, + "FloatField": _core_model_field_attr, + "ForeignKey": _core_model_field_attr, + "GenericIPAddressField": _core_model_field_attr, + "IPAddressField": _core_model_field_attr, + "ImageField": _core_model_field_attr, + "IntegerField": _core_model_field_attr, + "ManyToManyField": _core_model_field_attr, + "NullBooleanField": _core_model_field_attr, + "OneToOneField": _core_model_field_attr, + "PositiveIntegerField": _core_model_field_attr, + "PositiveSmallIntegerField": _core_model_field_attr, + "SlugField": _core_model_field_attr, + "SmallIntegerField": _core_model_field_attr, + "TextField": _core_model_field_attr, + "TimeField": _core_model_field_attr, + "URLField": _core_model_field_attr, +} -_add_model_attr = {'CharField': {'max_length': None}, - 'CommaSeparatedIntegerField': {'max_length': None}, - 'DateField': {'auto_now': False, - 'auto_now_add': False, - 'unique_for_date': None, - 'unique_for_month': None, - 'unique_for_year': None,}, - 'DateTimeField': {'auto_now': False, - 'auto_now_add': False, - 'unique_for_date': None, - 'unique_for_month': None, - 'unique_for_year': None,}, - 'DecimalField': {'max_digits': None, - 'decimal_places': None}, - 'EmailField': {'max_length': 75}, - 'FileField': {'max_length': 100, 'upload_to': None, - 'storage': None}, - 'FilePathField': {'allow_files': True, - 'allow_folders': False, - 'match': None, - 'path': None, - 'recursive': False}, - 'ForeignKey': {'limit_choices_to': None, - 'on_delete': models.CASCADE, - 'related_name': None, - 'to_field': None}, - 'GenericIPAddressField': {'protocol': 'both', - 'unpack_ipv4': False}, - 'ImageField': {'max_length': 100, 'upload_to': None, - 'height_field': None, 'width_field': None,}, - 'OneToOneField': {}, - 'SlugField': {'max_length': 50}, - 'TimeField': {'auto_now': False, - 'auto_now_add': False, - 'unique_for_date': None, - 'unique_for_month': None, - 'unique_for_year': None,}, - 'URLField': {'max_length': 200}} +_add_model_attr = { + "CharField": {"max_length": None}, + "CommaSeparatedIntegerField": {"max_length": None}, + "DateField": { + "auto_now": False, + "auto_now_add": False, + "unique_for_date": None, + "unique_for_month": None, + "unique_for_year": None, + }, + "DateTimeField": { + "auto_now": False, + "auto_now_add": False, + "unique_for_date": None, + "unique_for_month": None, + "unique_for_year": None, + }, + "DecimalField": {"max_digits": None, "decimal_places": None}, + "EmailField": {"max_length": 75}, + "FileField": {"max_length": 100, "upload_to": None, "storage": None}, + "FilePathField": { + "allow_files": True, + "allow_folders": False, + "match": None, + "path": None, + "recursive": False, + }, + "ForeignKey": { + "limit_choices_to": None, + "on_delete": models.CASCADE, + "related_name": None, + "to_field": None, + }, + "GenericIPAddressField": {"protocol": "both", "unpack_ipv4": False}, + "ImageField": { + "max_length": 100, + "upload_to": None, + "height_field": None, + "width_field": None, + }, + "OneToOneField": {}, + "SlugField": {"max_length": 50}, + "TimeField": { + "auto_now": False, + "auto_now_add": False, + "unique_for_date": None, + "unique_for_month": None, + "unique_for_year": None, + }, + "URLField": {"max_length": 200}, +} for k, v in _add_model_attr.items(): - _model_fields[k]= dict(_model_fields[k], **v) + _model_fields[k] = dict(_model_fields[k], **v) model_fields = _model_fields -model_meta_fields = {'abstract': False, - 'db_tablespace': '', - 'get_latest_by': None, - 'managed': True, - 'order_with_respect_to': None, - 'ordering': None, - 'permissions': [], - 'proxy': False, - 'unique_together': [], - 'verbose_name': None, - 'verbose_name_plural': None} +model_meta_fields = { + "abstract": False, + "db_tablespace": "", + "get_latest_by": None, + "managed": True, + "order_with_respect_to": None, + "ordering": None, + "permissions": [], + "proxy": False, + "unique_together": [], + "verbose_name": None, + "verbose_name_plural": None, +} -_core_form_field_attr = {'required': True, - 'label': None, - 'initial': None, - 'widget': None, - 'help_text': None, - 'error_messages': None, - 'localize': False, - 'validators': None,} +_core_form_field_attr = { + "required": True, + "label": None, + "initial": None, + "widget": None, + "help_text": None, + "error_messages": None, + "localize": False, + "validators": None, +} -_form_fields = {'BooleanField': _core_form_field_attr, - 'CharField': _core_form_field_attr, - 'ChoiceField': _core_form_field_attr, - 'DateField': _core_form_field_attr, - 'DateTimeField': _core_form_field_attr, - 'DecimalField': _core_form_field_attr, - 'EmailField': _core_form_field_attr, - 'FileField': _core_form_field_attr, - 'FilePathField': _core_form_field_attr, - 'FloatField': _core_form_field_attr, - 'GenericIPAddressField': _core_form_field_attr, - 'IPAddressField': _core_form_field_attr, - 'ImageField': _core_form_field_attr, - 'IntegerField': _core_form_field_attr, - 'MultipleChoiceField': _core_form_field_attr, - 'NullBooleanField': _core_form_field_attr, - 'RegexField': _core_form_field_attr, - 'SlugField': _core_form_field_attr, - 'TimeField': _core_form_field_attr, - 'TypedChoiceField': _core_form_field_attr, - 'TypedMultipleChoiceField': _core_form_field_attr, - 'URLField': _core_form_field_attr} +_form_fields = { + "BooleanField": _core_form_field_attr, + "CharField": _core_form_field_attr, + "ChoiceField": _core_form_field_attr, + "DateField": _core_form_field_attr, + "DateTimeField": _core_form_field_attr, + "DecimalField": _core_form_field_attr, + "EmailField": _core_form_field_attr, + "FileField": _core_form_field_attr, + "FilePathField": _core_form_field_attr, + "FloatField": _core_form_field_attr, + "GenericIPAddressField": _core_form_field_attr, + "IPAddressField": _core_form_field_attr, + "ImageField": _core_form_field_attr, + "IntegerField": _core_form_field_attr, + "MultipleChoiceField": _core_form_field_attr, + "NullBooleanField": _core_form_field_attr, + "RegexField": _core_form_field_attr, + "SlugField": _core_form_field_attr, + "TimeField": _core_form_field_attr, + "TypedChoiceField": _core_form_field_attr, + "TypedMultipleChoiceField": _core_form_field_attr, + "URLField": _core_form_field_attr, +} -_add_form_attr = {'CharField': {'max_length': None, 'min_length': None}, - 'ChoiceField': {'choices': None}, - 'DateField': {'input_formats': None}, - 'DateTimeField': {'input_formats': None}, - 'DecimalField': {'max_value': None, 'min_value': None, - 'max_digits': None, 'decimal_places': None}, - 'EmailField': {'max_length': None, 'min_length': None}, - 'FileField': {'max_length': None, 'allow_empty_file': False}, - 'FilePathField': {'allow_files': True, - 'allow_folders': False, - 'match': None, - 'path': None, - 'recursive': False}, - 'FloatField': {'max_value': None, 'min_value': None}, - 'GenericIPAddressField': {'protocol': 'both', - 'unpack_ipv4': False}, - 'IntegerField': {'max_value': None, 'min_value': None}, - 'MultipleChoiceField': {'choices': None}, - 'RegexField': {'regex': None}, - 'TimeField': _core_form_field_attr, - 'TypedChoiceField': {'choices': None, 'empty_value': None, - 'coerce': None}, - 'TypedMultipleChoiceField': {'choices': None, - 'empty_value': None, - 'coerce': None}, - 'URLField': {'max_length': None, 'min_length': None}} +_add_form_attr = { + "CharField": {"max_length": None, "min_length": None}, + "ChoiceField": {"choices": None}, + "DateField": {"input_formats": None}, + "DateTimeField": {"input_formats": None}, + "DecimalField": { + "max_value": None, + "min_value": None, + "max_digits": None, + "decimal_places": None, + }, + "EmailField": {"max_length": None, "min_length": None}, + "FileField": {"max_length": None, "allow_empty_file": False}, + "FilePathField": { + "allow_files": True, + "allow_folders": False, + "match": None, + "path": None, + "recursive": False, + }, + "FloatField": {"max_value": None, "min_value": None}, + "GenericIPAddressField": {"protocol": "both", "unpack_ipv4": False}, + "IntegerField": {"max_value": None, "min_value": None}, + "MultipleChoiceField": {"choices": None}, + "RegexField": {"regex": None}, + "TimeField": _core_form_field_attr, + "TypedChoiceField": {"choices": None, "empty_value": None, "coerce": None}, + "TypedMultipleChoiceField": {"choices": None, "empty_value": None, "coerce": None}, + "URLField": {"max_length": None, "min_length": None}, +} for k, v in _add_form_attr.items(): - _form_fields[k]= dict(_form_fields[k], **v) + _form_fields[k] = dict(_form_fields[k], **v) form_fields = _form_fields diff --git a/docs/_theme/djangodocs/genindex.html b/docs/_theme/djangodocs/genindex.html index 486994a..032b70d 100644 --- a/docs/_theme/djangodocs/genindex.html +++ b/docs/_theme/djangodocs/genindex.html @@ -1,4 +1,4 @@ {% extends "basic/genindex.html" %} {% block bodyclass %}{% endblock %} -{% block sidebarwrapper %}{% endblock %} \ No newline at end of file +{% block sidebarwrapper %}{% endblock %} diff --git a/docs/_theme/djangodocs/layout.html b/docs/_theme/djangodocs/layout.html index ef91dd7..aefc0fd 100644 --- a/docs/_theme/djangodocs/layout.html +++ b/docs/_theme/djangodocs/layout.html @@ -2,13 +2,13 @@ {%- macro secondnav() %} {%- if prev %} - « previous + « previous {{ reldelim2 }} {%- endif %} {%- if parents %} - up + up {%- else %} - up + up {%- endif %} {%- if next %} {{ reldelim2 }} @@ -65,13 +65,13 @@

{{ docstitle }}

- +
{% block body %}{% endblock %} -
+
{% block sidebarwrapper %} @@ -82,11 +82,11 @@

{{ docstitle }}

Last update:

{{ last_updated }}

{%- endif %} - + {% endif %} {% endblock %} - +
@@ -113,7 +113,7 @@

You are here:

{% for p in parents %}{% endfor %} - + {% endblock %} {# Empty some default blocks out #} @@ -121,4 +121,4 @@

You are here:

{% block relbar2 %}{% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} -{% block footer %}{% endblock %} \ No newline at end of file +{% block footer %}{% endblock %} diff --git a/docs/_theme/djangodocs/modindex.html b/docs/_theme/djangodocs/modindex.html index 59a5cb3..ca5a2d4 100644 --- a/docs/_theme/djangodocs/modindex.html +++ b/docs/_theme/djangodocs/modindex.html @@ -1,3 +1,3 @@ {% extends "basic/modindex.html" %} {% block bodyclass %}{% endblock %} -{% block sidebarwrapper %}{% endblock %} \ No newline at end of file +{% block sidebarwrapper %}{% endblock %} diff --git a/docs/_theme/djangodocs/search.html b/docs/_theme/djangodocs/search.html index 943478c..4fdc7f6 100644 --- a/docs/_theme/djangodocs/search.html +++ b/docs/_theme/djangodocs/search.html @@ -1,3 +1,3 @@ {% extends "basic/search.html" %} {% block bodyclass %}{% endblock %} -{% block sidebarwrapper %}{% endblock %} \ No newline at end of file +{% block sidebarwrapper %}{% endblock %} diff --git a/docs/_theme/djangodocs/static/default.css b/docs/_theme/djangodocs/static/default.css index 9dc69ee..8f1e38f 100644 --- a/docs/_theme/djangodocs/static/default.css +++ b/docs/_theme/djangodocs/static/default.css @@ -1,3 +1,3 @@ @import url(reset-fonts-grids.css); @import url(djangodocs.css); -@import url(homepage.css); \ No newline at end of file +@import url(homepage.css); diff --git a/docs/_theme/djangodocs/static/djangodocs.css b/docs/_theme/djangodocs/static/djangodocs.css index 4adb838..df8b409 100644 --- a/docs/_theme/djangodocs/static/djangodocs.css +++ b/docs/_theme/djangodocs/static/djangodocs.css @@ -1,7 +1,7 @@ /*** setup ***/ html { background:#092e20;} body { font:12px/1.5 Verdana,sans-serif; background:#092e20; color: white;} -#custom-doc { width:76.54em;*width:74.69em;min-width:995px; max-width:100em; margin:auto; text-align:left; padding-top:16px; margin-top:0;} +#custom-doc { width:76.54em;*width:74.69em;min-width:995px; max-width:100em; margin:auto; text-align:left; padding-top:16px; margin-top:0;} #hd { padding: 4px 0 12px 0; } #bd { background:#234F32; } #ft { color:#487858; font-size:90%; padding-bottom: 2em; } @@ -54,7 +54,7 @@ hr { color:#ccc; background-color:#ccc; height:1px; border:0; } p, ul, dl { margin-top:.6em; margin-bottom:1em; padding-bottom: 0.1em;} #yui-main div.yui-b img { max-width: 50em; margin-left: auto; margin-right: auto; display: block; } caption { font-size:1em; font-weight:bold; margin-top:0.5em; margin-bottom:0.5em; margin-left: 2px; text-align: center; } -blockquote { padding: 0 1em; margin: 1em 0; font:125%/1.2em "Trebuchet MS", sans-serif; color:#234f32; border-left:2px solid #94da3a; } +blockquote { padding: 0 1em; margin: 1em 0; font:125%/1.2em "Trebuchet MS", sans-serif; color:#234f32; border-left:2px solid #94da3a; } strong { font-weight: bold; } em { font-style: italic; } ins { font-weight: bold; text-decoration: none; } diff --git a/docs/_theme/djangodocs/static/homepage.css b/docs/_theme/djangodocs/static/homepage.css index 276c547..3f69f01 100644 --- a/docs/_theme/djangodocs/static/homepage.css +++ b/docs/_theme/djangodocs/static/homepage.css @@ -19,4 +19,4 @@ #index #s-solving-specific-problems, #index #s-reference, #index #s-and-all-the-rest - { clear: left; } \ No newline at end of file + { clear: left; } diff --git a/docs/_theme/djangodocs/static/reset-fonts-grids.css b/docs/_theme/djangodocs/static/reset-fonts-grids.css index f5238d7..3d44d85 100644 --- a/docs/_theme/djangodocs/static/reset-fonts-grids.css +++ b/docs/_theme/djangodocs/static/reset-fonts-grids.css @@ -5,4 +5,4 @@ http://developer.yahoo.net/yui/license.txt version: 2.5.1 */ html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;font-variant:normal;}sup {vertical-align:text-top;}sub {vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}body {font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}table {font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;} -body{text-align:center;}#ft{clear:both;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;min-width:750px;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}s .yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#bd:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#bd,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;} \ No newline at end of file +body{text-align:center;}#ft{clear:both;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;min-width:750px;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}s .yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#bd:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#bd,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;} diff --git a/docs/api/password_policies.conf.rst b/docs/api/password_policies.conf.rst index 5a363e7..c5630e3 100644 --- a/docs/api/password_policies.conf.rst +++ b/docs/api/password_policies.conf.rst @@ -15,6 +15,6 @@ and its new value to the project's settings file: ``Settings`` ------------ -.. autoclass:: password_policies.conf.Settings +.. autoclass:: password_policies.conf.settings :members: :show-inheritance: diff --git a/docs/api/password_policies.forms.validators.rst b/docs/api/password_policies.forms.validators.rst index df33eee..a88322a 100644 --- a/docs/api/password_policies.forms.validators.rst +++ b/docs/api/password_policies.forms.validators.rst @@ -238,4 +238,4 @@ django-password-policies provides validators to check new passwords: A :class:`SymbolCountValidator` instance. -.. _`Python bindings for cracklib documentation`: http://www.nongnu.org/python-crack/doc/index.html +.. _`Python bindings for cracklib documentation`: https://www.nongnu.org/python-crack/doc/index.html diff --git a/docs/conf.py b/docs/conf.py index 9b29770..dd6933b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # password_policies documentation build configuration file, created by # sphinx-quickstart on Thu Aug 23 13:18:53 2012. # @@ -11,8 +9,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys + import django # If extensions (or modules to document with autodoc) are in another directory, @@ -22,11 +21,10 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext"))) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "password_policies.tests.test_settings") -from password_policies.tests import test_settings django.setup() -password_policies = __import__('password_policies') +password_policies = __import__("password_policies") # -- General configuration ----------------------------------------------------- @@ -35,187 +33,195 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'exts'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.ifconfig", + "exts", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'contents' +master_doc = "contents" # General information about the project. -project = u'django-password-policies' -copyright = u'2012, Tarak Blah' +project = "django-password-policies-iplweb" +copyright = "2012, Tarak Blah" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = password_policies.get_version() +version = password_policies.__version__ # The full version, including alpha/beta/rc tags. release = password_policies.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['build'] +exclude_patterns = ["build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'djangodocs' +html_theme = "djangodocs" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_theme'] +html_theme_path = ["_theme"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'password_policiesdoc' +htmlhelp_basename = "password_policiesdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'password_policies.tex', u'change\\_email Documentation', - u'Tarak Blah', 'manual'), + ( + "index", + "password_policies.tex", + "change\\_email Documentation", + "Tarak Blah", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -223,12 +229,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'password_policies', u'password_policies Documentation', - [u'Tarak Blah'], 1) + ("index", "password_policies", "password_policies Documentation", ["Tarak Blah"], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -237,20 +242,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'password_policies', u'password_policies Documentation', - u'Tarak Blah', 'password_policies', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "password_policies", + "password_policies Documentation", + "Tarak Blah", + "password_policies", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +# texinfo_show_urls = 'footnote' diff --git a/docs/license.rst b/docs/license.rst index ce39e22..bd540cc 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -4,4 +4,3 @@ License ======= .. literalinclude:: ../LICENSE - diff --git a/docs/topics/contributing.rst b/docs/topics/contributing.rst index d708bec..2f5b1f1 100644 --- a/docs/topics/contributing.rst +++ b/docs/topics/contributing.rst @@ -33,7 +33,7 @@ Please note the following guidelines for contributing: * Documentation must be formatted to be used with `Sphinx`_. -.. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ -.. _`Django coding style`: http://docs.djangoproject.com/en/dev/internals/contributing/#coding-style +.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ +.. _`Django coding style`: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/ .. _`GitHub`: https://github.com/iplweb/django-password-policies-iplweb/ -.. _`Sphinx`: http://sphinx.pocoo.org/ +.. _`Sphinx`: https://www.sphinx-doc.org/ diff --git a/docs/topics/custom.validation.rst b/docs/topics/custom.validation.rst index 9e28121..5a8a0f0 100644 --- a/docs/topics/custom.validation.rst +++ b/docs/topics/custom.validation.rst @@ -49,7 +49,7 @@ Customizing password validation can be used by simply using a your_custom_validators.another_validator],) def clean(self): - cleaned_data = super(CustomPasswordPoliciesForm, self).clean() + cleaned_data = super().clean() new_password1 = cleaned_data.get("new_password1") new_password2 = cleaned_data.get("new_password2") @@ -68,6 +68,6 @@ URL pattern needs to be added to a project's ``URLconf``:: from your_app.forms import CustomPasswordPoliciesForm - urlpatterns = patterns('', - (r'^password/reset/', PasswordResetConfirmView.as_view(form_class=CustomPasswordPoliciesForm)), - ) + urlpatterns = [ + path("password/reset/", PasswordResetConfirmView.as_view(form_class=CustomPasswordPoliciesForm)), + ] diff --git a/docs/topics/install.rst b/docs/topics/install.rst index fb4e037..b611b27 100644 --- a/docs/topics/install.rst +++ b/docs/topics/install.rst @@ -11,7 +11,7 @@ Requirements This application requires -* `Django`_ 3.0 or newer +* `Django`_ 4.2 or newer .. _install-cracklib: @@ -54,11 +54,11 @@ From Pypi To install from `PyPi`_:: - [sudo] pip install django-password-policies + [sudo] pip install django-password-policies-iplweb or:: - [sudo] easy_install django-password-policies + [sudo] easy_install django-password-policies-iplweb .. _`PyPi`: https://pypi.python.org/pypi/django-password-policies @@ -97,5 +97,5 @@ inside the Python path:: .. _`Django`: https://www.djangoproject.com/ -.. _`Python bindings for cracklib`: http://www.nongnu.org/python-crack/ +.. _`Python bindings for cracklib`: https://www.nongnu.org/python-crack/ .. _`Levenshtein Python C extension module`: https://github.com/miohtama/python-Levenshtein diff --git a/docs/topics/overview.rst b/docs/topics/overview.rst index 65245b6..d69036e 100644 --- a/docs/topics/overview.rst +++ b/docs/topics/overview.rst @@ -43,5 +43,5 @@ Features for password resets. .. _`Django`: https://www.djangoproject.com/ -.. _`RFC 4013`: http://tools.ietf.org/html/rfc4013 -.. _`Python bindings for cracklib`: http://www.nongnu.org/python-crack/ +.. _`RFC 4013`: https://datatracker.ietf.org/doc/html/rfc4013 +.. _`Python bindings for cracklib`: https://www.nongnu.org/python-crack/ diff --git a/docs/topics/security.rst b/docs/topics/security.rst index 5238354..8d1e201 100644 --- a/docs/topics/security.rst +++ b/docs/topics/security.rst @@ -59,4 +59,4 @@ works: includes a mechanism to compare a raw password with different encrypted passwords. No unencrypted password is saved to the database! -.. _`Python bindings for cracklib`: http://www.nongnu.org/python-crack/ +.. _`Python bindings for cracklib`: https://www.nongnu.org/python-crack/ diff --git a/docs/topics/support.rst b/docs/topics/support.rst index 128b94a..a9750dc 100644 --- a/docs/topics/support.rst +++ b/docs/topics/support.rst @@ -12,4 +12,3 @@ At the present time this help is available: * To report a bug or other type of issue, please use the `GitHub issue tracker`_. .. _`GitHub issue tracker`: https://github.com/iplweb/django-password-policies-iplweb/issues - diff --git a/docs/topics/testing.rst b/docs/topics/testing.rst index bfa696f..4cb010a 100644 --- a/docs/topics/testing.rst +++ b/docs/topics/testing.rst @@ -9,4 +9,3 @@ tests do not require to be run from within a project. They can be executed in the application's source directory:: $ python setup.py test - diff --git a/password_policies/__init__.py b/password_policies/__init__.py index e0f37ac..0f64540 100644 --- a/password_policies/__init__.py +++ b/password_policies/__init__.py @@ -1,3 +1,3 @@ VERSION = (0, 8, 6) -__version__ = "%s.%s.%s" % VERSION +__version__ = ".".join(map(str, VERSION)) diff --git a/password_policies/admin.py b/password_policies/admin.py index 7099fdc..fd2d0b7 100644 --- a/password_policies/admin.py +++ b/password_policies/admin.py @@ -1,9 +1,5 @@ from django.contrib import admin -try: - from django.utils.translation import gettext_lazy as _ -except ImportError: - # Before Django 3.0 - from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from password_policies.conf import settings from password_policies.models import PasswordChangeRequired, PasswordHistory @@ -18,6 +14,7 @@ def force_password_change(modeladmin, request, queryset): ) +@admin.register(PasswordHistory) class PasswordHistoryAdmin(admin.ModelAdmin): date_hierarchy = "created" exclude = ("password",) @@ -33,6 +30,7 @@ def has_add_permission(self, request): return False +@admin.register(PasswordChangeRequired) class PasswordChangeRequiredAdmin(admin.ModelAdmin): date_hierarchy = "created" list_display = ("id", "user", "created") @@ -51,7 +49,3 @@ def get_readonly_fields(self, request, obj=None): return ["user"] else: return [] - - -admin.site.register(PasswordHistory, PasswordHistoryAdmin) -admin.site.register(PasswordChangeRequired, PasswordChangeRequiredAdmin) diff --git a/password_policies/conf/settings.py b/password_policies/conf/settings.py index 1a37f29..270906d 100644 --- a/password_policies/conf/settings.py +++ b/password_policies/conf/settings.py @@ -33,7 +33,7 @@ #: be performed if the user's password has expired. #: #: Defaults to 1 hour. -PASSWORD_CHECK_SECONDS = getattr(settings, "PASSWORD_CHECK_SECONDS", 60 ** 2) +PASSWORD_CHECK_SECONDS = getattr(settings, "PASSWORD_CHECK_SECONDS", 60**2) #: Specifies a list of common sequences to attempt to #: match a password against. @@ -41,15 +41,15 @@ settings, "PASSWORD_COMMON_SEQUENCES", [ - u"0123456789", - u"`1234567890-=", - u"~!@#$%^&*()_+", - u"abcdefghijklmnopqrstuvwxyz", - u"quertyuiop[]\\asdfghjkl;'zxcvbnm,./", - u'quertyuiop{}|asdfghjkl;"zxcvbnm<>?', - u"quertyuiopasdfghjklzxcvbnm", - u"1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]\\", - u"qazwsxedcrfvtgbyhnujmikolp", + "0123456789", + "`1234567890-=", + "~!@#$%^&*()_+", + "abcdefghijklmnopqrstuvwxyz", + "quertyuiop[]\\asdfghjkl;'zxcvbnm,./", + 'quertyuiop{}|asdfghjkl;"zxcvbnm<>?', + "quertyuiopasdfghjklzxcvbnm", + "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]\\", + "qazwsxedcrfvtgbyhnujmikolp", ], ) PASSWORD_DICTIONARY = getattr(settings, "PASSWORD_DICTIONARY", None) @@ -72,7 +72,7 @@ #: to change his/her password. #: #: Defaults to 60 days. -PASSWORD_DURATION_SECONDS = getattr(settings, "PASSWORD_DURATION_SECONDS", 24 * 60 ** 3) +PASSWORD_DURATION_SECONDS = getattr(settings, "PASSWORD_DURATION_SECONDS", 24 * 60**3) #: The field on the user model as defined by settings.AUTH_USER_MODEL #: where the password is stored. PASSWORD_MODEL_FIELD = getattr(settings, "PASSWORD_MODEL_FIELD", "password") @@ -183,7 +183,7 @@ TEMPLATE_403_PAGE = getattr(settings, "TEMPLATE_403_PAGE", "403.html") -PASSWORD_RESET_TIMEOUT_DAYS = 1 +PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 1 PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY = "_password_policies_last_checked" PASSWORD_POLICIES_EXPIRED_SESSION_KEY = "_password_policies_expired" diff --git a/password_policies/context_processors.py b/password_policies/context_processors.py index 50d37ef..1e04cc6 100644 --- a/password_policies/context_processors.py +++ b/password_policies/context_processors.py @@ -4,35 +4,38 @@ def password_status(request): """ -Adds a variable determining the state of a user's password -to the context if the user has authenticated: + Adds a variable determining the state of a user's password + to the context if the user has authenticated: -* ``password_change_required`` - Determines if the user needs to change his/her password. + * ``password_change_required`` + Determines if the user needs to change his/her password. - Set to ``True`` if the user has to change his/her password, - ``False`` otherwise. + Set to ``True`` if the user has to change his/her password, + ``False`` otherwise. -To use it add it to the list of ``TEMPLATE_CONTEXT_PROCESSORS`` -in a project's settings file:: + To use it add it to the list of ``TEMPLATE_CONTEXT_PROCESSORS`` + in a project's settings file:: - TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.contrib.auth.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.contrib.messages.context_processors.messages', - 'password_policies.context_processors.password_status', - ) -""" + TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.contrib.messages.context_processors.messages', + 'password_policies.context_processors.password_status', + ) + """ d = {} auth = request.user.is_authenticated if callable(auth): # Before Django 1.10 auth = auth() if auth: - if settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY not in request.session: + if ( + settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY + not in request.session + ): r = PasswordHistory.objects.change_required(request.user) else: r = request.session[settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY] - d['password_change_required'] = r + d["password_change_required"] = r return d diff --git a/password_policies/forms/__init__.py b/password_policies/forms/__init__.py index b4f71cb..b4c33ac 100644 --- a/password_policies/forms/__init__.py +++ b/password_policies/forms/__init__.py @@ -1,33 +1,15 @@ -from __future__ import unicode_literals +from collections import OrderedDict from django import forms from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.hashers import is_password_usable, make_password +from django.contrib.sites.shortcuts import get_current_site from django.core import signing from django.core.exceptions import ObjectDoesNotExist from django.template import loader - -try: - # SortedDict is deprecated as of Django 1.7 and will be removed in Django 1.9. - # https://code.djangoproject.com/wiki/SortedDict - from collections import OrderedDict as SortedDict -except ImportError: - from django.utils.datastructures import SortedDict - - -try: - from django.contrib.sites.shortcuts import get_current_site -except ImportError: - # Before Django 1.9 - from django.contrib.sites.models import get_current_site - from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode -try: - from django.utils.translation import gettext_lazy as _ -except ImportError: - # Before in Django 3.0 - from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from password_policies.conf import settings from password_policies.forms.fields import PasswordPoliciesField @@ -63,7 +45,7 @@ def __init__(self, user, *args, **kwargs): :arg user: A :class:`~django.contrib.auth.models.User` instance.""" self.user = user - super(PasswordPoliciesForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def clean_new_password1(self): """ @@ -121,7 +103,7 @@ class PasswordPoliciesChangeForm(PasswordPoliciesForm): ), "password_similar": _("The old and the new password are too similar."), "password_identical": _("The old and the new password are the same."), - } + }, ) old_password = forms.CharField(label=_("Old password"), widget=forms.PasswordInput) @@ -136,7 +118,7 @@ def clean_old_password(self): def clean(self): """ Validates that old and new password are not too similar.""" - cleaned_data = super(PasswordPoliciesChangeForm, self).clean() + cleaned_data = super().clean() old_password = cleaned_data.get("old_password") new_password1 = cleaned_data.get("new_password1") @@ -158,7 +140,7 @@ def clean(self): return cleaned_data def save(self, commit=True): - user = super(PasswordPoliciesChangeForm, self).save(commit=commit) + user = super().save(commit=commit) try: # Checking the object id to prevent AssertionError id is None when deleting. if user.password_change_required and user.password_change_required.id: @@ -168,7 +150,7 @@ def save(self, commit=True): return user -PasswordPoliciesChangeForm.base_fields = SortedDict( +PasswordPoliciesChangeForm.base_fields = OrderedDict( [ (k, PasswordPoliciesChangeForm.base_fields[k]) for k in ["old_password", "new_password1", "new_password2"] diff --git a/password_policies/forms/admin.py b/password_policies/forms/admin.py index 3d55d0f..a344bd8 100644 --- a/password_policies/forms/admin.py +++ b/password_policies/forms/admin.py @@ -1,62 +1,62 @@ from django import forms from django.contrib.auth.forms import AdminPasswordChangeForm -try: - from django.utils.translation import gettext_lazy as _ -except ImportError: - # Before in Django 3.0 - from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from password_policies.conf import settings from password_policies.forms.fields import PasswordPoliciesField -from password_policies.models import PasswordHistory -from password_policies.models import PasswordChangeRequired +from password_policies.models import PasswordChangeRequired, PasswordHistory class PasswordPoliciesAdminForm(AdminPasswordChangeForm): """Enforces password policies in the admin interface. -Use this form to enforce strong passwords in the admin interface. -""" + Use this form to enforce strong passwords in the admin interface. + """ + error_messages = { - 'password_mismatch': _("The two password fields didn't match."), - 'password_used': _("The new password was used before. Please enter another one.") + "password_mismatch": _("The two password fields didn't match."), + "password_used": _( + "The new password was used before. Please enter another one." + ), } - password1 = PasswordPoliciesField(label=_("Password new"), - max_length=settings.PASSWORD_MAX_LENGTH, - min_length=settings.PASSWORD_MIN_LENGTH - ) + password1 = PasswordPoliciesField( + label=_("Password new"), + max_length=settings.PASSWORD_MAX_LENGTH, + min_length=settings.PASSWORD_MIN_LENGTH, + ) def clean_password1(self): """ -Validates that a given password was not used before. -""" - password1 = self.cleaned_data.get('password1') + Validates that a given password was not used before. + """ + password1 = self.cleaned_data.get("password1") if settings.PASSWORD_USE_HISTORY: if self.user.check_password(password1): - raise forms.ValidationError( - self.error_messages['password_used']) - if not PasswordHistory.objects.check_password(self.user, - password1): - raise forms.ValidationError( - self.error_messages['password_used']) + raise forms.ValidationError(self.error_messages["password_used"]) + if not PasswordHistory.objects.check_password(self.user, password1): + raise forms.ValidationError(self.error_messages["password_used"]) return password1 class ForceChangeAdminForm(PasswordPoliciesAdminForm): - change_required = forms.BooleanField(initial=True, required=False, label=_('Must change?')) + change_required = forms.BooleanField( + initial=True, required=False, label=_("Must change?") + ) def save(self, commit=True): - user = super(ForceChangeAdminForm, self).save(commit=commit) - if self.cleaned_data["change_required"] and not PasswordChangeRequired.objects.filter(user=user).count(): + user = super().save(commit=commit) + if ( + self.cleaned_data["change_required"] + and not PasswordChangeRequired.objects.filter(user=user).count() + ): PasswordChangeRequired.objects.create(user=user) return user class ForceChangeRequiredAdminForm(PasswordPoliciesAdminForm): - def save(self, commit=True): - user = super(ForceChangeRequiredAdminForm, self).save(commit=commit) + user = super().save(commit=commit) if not PasswordChangeRequired.objects.filter(user=user).count(): PasswordChangeRequired.objects.create(user=user) return user diff --git a/password_policies/forms/fields.py b/password_policies/forms/fields.py index 1d9d0d4..19f150a 100644 --- a/password_policies/forms/fields.py +++ b/password_policies/forms/fields.py @@ -1,35 +1,40 @@ from django import forms -from password_policies.forms.validators import validate_common_sequences -from password_policies.forms.validators import validate_consecutive_count -from password_policies.forms.validators import validate_cracklib -from password_policies.forms.validators import validate_dictionary_words -from password_policies.forms.validators import validate_entropy -from password_policies.forms.validators import validate_letter_count -from password_policies.forms.validators import validate_lowercase_letter_count -from password_policies.forms.validators import validate_uppercase_letter_count -from password_policies.forms.validators import validate_number_count -from password_policies.forms.validators import validate_symbol_count -from password_policies.forms.validators import validate_not_email +from password_policies.forms.validators import ( + validate_common_sequences, + validate_consecutive_count, + validate_cracklib, + validate_dictionary_words, + validate_entropy, + validate_letter_count, + validate_lowercase_letter_count, + validate_not_email, + validate_number_count, + validate_symbol_count, + validate_uppercase_letter_count, +) class PasswordPoliciesField(forms.CharField): """ -A form field that validates a password using :ref:`api-validators`. -""" - default_validators = [validate_common_sequences, - validate_consecutive_count, - validate_cracklib, - validate_dictionary_words, - validate_letter_count, - validate_lowercase_letter_count, - validate_uppercase_letter_count, - validate_number_count, - validate_symbol_count, - validate_entropy, - validate_not_email] + A form field that validates a password using :ref:`api-validators`. + """ + + default_validators = [ + validate_common_sequences, + validate_consecutive_count, + validate_cracklib, + validate_dictionary_words, + validate_letter_count, + validate_lowercase_letter_count, + validate_uppercase_letter_count, + validate_number_count, + validate_symbol_count, + validate_entropy, + validate_not_email, + ] def __init__(self, *args, **kwargs): if "widget" not in kwargs: kwargs["widget"] = forms.PasswordInput(render_value=False) - super(PasswordPoliciesField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) diff --git a/password_policies/forms/validators.py b/password_policies/forms/validators.py index 69b0185..0c88d28 100644 --- a/password_policies/forms/validators.py +++ b/password_policies/forms/validators.py @@ -5,37 +5,12 @@ import unicodedata from django.core.exceptions import ValidationError - -try: - from django.utils.encoding import smart_str as smart_text -except ImportError: - # Before in Django 2.0 - from django.utils.encoding import smart_text - -try: - from django.utils.encoding import force_str as force_text -except ImportError: - # Before in Django 2.0 - from django.utils.encoding import force_text -try: - from django.utils.translation import gettext_lazy as _ - from django.utils.translation import ngettext as ungettext -except ImportError: - # Before in Django 3.0 - from django.utils.translation import ugettext_lazy as _ - from django.utils.translation import ungettext +from django.utils.encoding import force_str, smart_str +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ngettext from password_policies.conf import settings -try: - # Python 3 does not have an xrange, this will throw a NameError - xrange -except NameError: - pass -else: - # alias range to xrange for Python 2 - range = xrange # noqa - class BaseCountValidator: """ @@ -48,7 +23,7 @@ def __call__(self, value): if not self.get_min_count(): return counter = 0 - for character in force_text(value): + for character in force_str(value): category = unicodedata.category(character) if category in self.categories: counter += 1 @@ -69,7 +44,7 @@ class BaseRFC4013Validator: Validates that a given password passes the requirements as defined in `RFC 4013`_. - .. _`RFC 4013`: http://tools.ietf.org/html/rfc4013""" + .. _`RFC 4013`: https://datatracker.ietf.org/doc/html/rfc4013""" first = "" invalid = True @@ -78,13 +53,13 @@ class BaseRFC4013Validator: r_and_al_cat = False def __call__(self, value): - value = force_text(value) + value = force_str(value) self.first = value[0] self.last = value[:-1] self._process(value) def _process(self, value): - for code in force_text(value): + for code in force_str(value): # TODO: Is this long enough? if ( stringprep.in_table_c12(code) @@ -123,7 +98,7 @@ def __init__(self, haystacks=[]): self.haystacks = haystacks def __call__(self, value): - needle = force_text(value) + needle = force_str(value) for haystack in self.haystacks: distance = self.fuzzy_substring(needle, haystack) longest = max(len(needle), len(haystack)) @@ -171,7 +146,7 @@ class BidirectionalValidator(BaseRFC4013Validator): For more information read `RFC 4013, section 2.3`_. - .. _`RFC 4013, section 2.3`: http://tools.ietf.org/html/rfc4013#section-2.3""" + .. _`RFC 4013, section 2.3`: https://datatracker.ietf.org/doc/html/rfc4013#section-2.3""" #: The validator's error code. code = "invalid_bidirectional" @@ -212,11 +187,11 @@ def __call__(self, value): if not self.get_max_count(): return consecutive_found = False - for _ign, group in itertools.groupby(force_text(value)): + for _ign, group in itertools.groupby(force_str(value)): if len(list(group)) > self.get_max_count(): consecutive_found = True if consecutive_found: - msg = ungettext( + msg = ngettext( "The new password contains consecutive" " characters. Only %(count)d consecutive character" " is allowed.", @@ -278,7 +253,7 @@ def __call__(self, value): crack.FascistCheck(value) except ValueError as reason: reason = _(str(reason)) - message = _("Please choose a different password, %s." % reason) + message = _(f"Please choose a different password, {reason}.") raise ValidationError(message, code=self.code) def __init__( @@ -397,9 +372,7 @@ def __init__(self, dictionary="", words=[]): haystacks = [] if self.dictionary: with open(self.dictionary) as dictionary: - haystacks.extend( - [smart_text(x.strip()) for x in dictionary.readlines()] - ) + haystacks.extend([smart_str(x.strip()) for x in dictionary.readlines()]) if self.words: haystacks.extend(self.words) super().__init__(haystacks=haystacks) @@ -410,7 +383,7 @@ class InvalidCharacterValidator(BaseRFC4013Validator): Validates that a given password does not contain invalid unicode characters as defined in `RFC 4013, section 2.3`_. - .. _`RFC 4013, section 2.3`: http://tools.ietf.org/html/rfc4013#section-2.3""" + .. _`RFC 4013, section 2.3`: https://datatracker.ietf.org/doc/html/rfc4013#section-2.3""" #: The validator's error code. code = "invalid_unicode" @@ -452,7 +425,7 @@ def get_error_message(self): """ Returns this validator's error message.""" msg = ( - ungettext( + ngettext( "The new password must contain %d or more letter.", "The new password must contain %d or more letters.", self.get_min_count(), @@ -493,7 +466,7 @@ def get_error_message(self): """ Returns this validator's error message.""" msg = ( - ungettext( + ngettext( "The new password must contain %d or more lowercase letter.", "The new password must contain %d or more lowercase letters.", self.get_min_count(), @@ -535,7 +508,7 @@ def get_error_message(self): """ Returns this validator's error message.""" msg = ( - ungettext( + ngettext( "The new password must contain %d or more uppercase letter.", "The new password must contain %d or more uppercase letters.", self.get_min_count(), @@ -619,7 +592,7 @@ def get_error_message(self): """ Returns this validator's error message.""" msg = ( - ungettext( + ngettext( "The new password must contain %d or more number.", "The new password must contain %d or more numbers.", self.get_min_count(), @@ -691,7 +664,7 @@ def get_error_message(self): """ Returns this validator's error message.""" msg = ( - ungettext( + ngettext( "The new password must contain %d or more symbol.", "The new password must contain %d or more symbols.", self.get_min_count(), diff --git a/password_policies/locale/pl/LC_MESSAGES/django.po b/password_policies/locale/pl/LC_MESSAGES/django.po index be53c04..8269599 100644 --- a/password_policies/locale/pl/LC_MESSAGES/django.po +++ b/password_policies/locale/pl/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # This file is distributed under the same license as the django-password-policies. -# Url: http://tarak.github.io/django-password-policies/ +# Url: https://tarak.github.io/django-password-policies/ # Translators: # Jarek Miazga , 2014. msgid "" diff --git a/password_policies/locale/ru/LC_MESSAGES/django.po b/password_policies/locale/ru/LC_MESSAGES/django.po index 1d1c732..746862f 100644 --- a/password_policies/locale/ru/LC_MESSAGES/django.po +++ b/password_policies/locale/ru/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # This file is distributed under the same license as the django-password-policies. -# Url: http://tarak.github.io/django-password-policies/ +# Url: https://tarak.github.io/django-password-policies/ # Translators: # Dmitry Pyatkov , 2014. msgid "" diff --git a/password_policies/managers.py b/password_policies/managers.py index d5aa7d6..5f842f6 100644 --- a/password_policies/managers.py +++ b/password_policies/managers.py @@ -1,9 +1,9 @@ from datetime import timedelta -from django.db import models -from django.utils import timezone from django.contrib.auth.hashers import identify_hasher from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from django.utils import timezone from password_policies.conf import settings @@ -13,29 +13,29 @@ class PasswordHistoryManager(models.Manager): def delete_expired(self, user, offset=None): """ -Deletes expired password history entries from the database(s). + Deletes expired password history entries from the database(s). -:arg user: A :class:`~django.contrib.auth.models.User` instance. -:arg int offset: A number specifying how much entries are to be kept - in the user's password history. Defaults - to :py:attr:`~password_policies.conf.Settings.PASSWORD_HISTORY_COUNT`. -""" + :arg user: A :class:`~django.contrib.auth.models.User` instance. + :arg int offset: A number specifying how much entries are to be kept + in the user's password history. Defaults + to :py:attr:`~password_policies.conf.Settings.PASSWORD_HISTORY_COUNT`. + """ if not offset: offset = self.default_offset qs = self.filter(user=user) if qs.count() > offset: - entry = qs[offset:offset + 1].get() + entry = qs[offset : offset + 1].get() qs.filter(created__lte=entry.created).delete() def change_required(self, user): """ -Checks if the user needs to change his/her password. + Checks if the user needs to change his/her password. -:arg object user: A :class:`~django.contrib.auth.models.User` instance. -:returns: ``True`` if the user needs to change his/her password, - ``False`` otherwise. -:rtype: bool -""" + :arg object user: A :class:`~django.contrib.auth.models.User` instance. + :returns: ``True`` if the user needs to change his/her password, + ``False`` otherwise. + :rtype: bool + """ newest = self.get_newest(user) if newest: last_change = newest.created @@ -50,19 +50,19 @@ def change_required(self, user): def check_password(self, user, raw_password): """ -Compares a raw (UNENCRYPTED!!!) password to entries in the users's -password history. + Compares a raw (UNENCRYPTED!!!) password to entries in the users's + password history. -:arg object user: A :class:`~django.contrib.auth.models.User` instance. -:arg str raw_password: A unicode string representing a password. -:returns: ``False`` if a password has been used before, ``True`` if not. -:rtype: bool -""" + :arg object user: A :class:`~django.contrib.auth.models.User` instance. + :arg str raw_password: A unicode string representing a password. + :returns: ``False`` if a password has been used before, ``True`` if not. + :rtype: bool + """ result = True if user.check_password(raw_password): result = False else: - entries = self.filter(user=user).all()[:self.default_offset] + entries = self.filter(user=user).all()[: self.default_offset] for entry in entries: hasher = identify_hasher(entry.password) if hasher.verify(raw_password, entry.password): @@ -72,12 +72,12 @@ def check_password(self, user, raw_password): def get_newest(self, user): """ -Gets the newest password history entry. + Gets the newest password history entry. -:arg object user: A :class:`~django.contrib.auth.models.User` instance. -:returns: A :class:`~password_policies.models.PasswordHistory` instance - if found, ``None`` if not. -""" + :arg object user: A :class:`~django.contrib.auth.models.User` instance. + :returns: A :class:`~password_policies.models.PasswordHistory` instance + if found, ``None`` if not. + """ try: entry = self.filter(user=user).latest() except ObjectDoesNotExist: diff --git a/password_policies/middleware.py b/password_policies/middleware.py index 3efd6a9..527b870 100644 --- a/password_policies/middleware.py +++ b/password_policies/middleware.py @@ -1,26 +1,20 @@ import re from datetime import timedelta -try: - from django.urls.base import reverse, resolve, NoReverseMatch, Resolver404 -except ImportError: - # Before Django 2.0 - from django.core.urlresolvers import NoReverseMatch, Resolver404, resolve, reverse - +from django.conf import settings as django_setings from django.http import HttpResponseRedirect +from django.urls.base import NoReverseMatch, Resolver404, resolve, reverse from django.utils import timezone - -import django.utils.deprecation -if hasattr(django.utils.deprecation, 'MiddlewareMixin'): - from django.utils.deprecation import MiddlewareMixin -else: - MiddlewareMixin = object - -from django.conf import settings as django_setings +from django.utils.deprecation import MiddlewareMixin from password_policies.conf import settings from password_policies.models import PasswordChangeRequired, PasswordHistory -from password_policies.utils import PasswordCheck, string_to_datetime, datetime_to_string +from password_policies.utils import ( + PasswordCheck, + datetime_to_string, + string_to_datetime, +) + class PasswordChangeMiddleware(MiddlewareMixin): """ @@ -37,19 +31,7 @@ class PasswordChangeMiddleware(MiddlewareMixin): is not taken... To use this middleware you need to add it to the - ``MIDDLEWARE_CLASSES`` list in a project's settings:: - - MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'password_policies.middleware.PasswordChangeMiddleware', - # ... other middleware ... - ) - - - or ``MIDDLEWARE`` if using Django 1.10 or higher: + ``MIDDLEWARE`` setting: MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', @@ -67,7 +49,8 @@ class PasswordChangeMiddleware(MiddlewareMixin): .. warning:: This middleware does not try to redirect using the HTTPS - protocol.""" + protocol. + """ checked = settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY expired = settings.PASSWORD_POLICIES_EXPIRED_SESSION_KEY @@ -83,7 +66,9 @@ def _check_history(self, request): else: # TODO: This relies on request.user.date_joined which might not # be available!!! - request.session[self.last] = datetime_to_string(request.user.date_joined) + request.session[self.last] = datetime_to_string( + request.user.date_joined + ) date_last = string_to_datetime(request.session[self.last]) if date_last < self.expiry_datetime: @@ -94,16 +79,14 @@ def _check_history(self, request): request.session[self.required] = False def _check_necessary(self, request): - if not request.session.get(self.checked, None): request.session[self.checked] = datetime_to_string(self.now) # If the PASSWORD_CHECK_ONLY_AT_LOGIN is set, then only check at the beginning of session, which we can # tell by self.now time having just been set. - if ( - not settings.PASSWORD_CHECK_ONLY_AT_LOGIN - or request.session.get(self.checked, None) == datetime_to_string(self.now) - ): + if not settings.PASSWORD_CHECK_ONLY_AT_LOGIN or request.session.get( + self.checked, None + ) == datetime_to_string(self.now): # If a password change is enforced we won't check # the user's password history, thus reducing DB hits... if PasswordChangeRequired.objects.filter(user=request.user).count(): @@ -129,28 +112,28 @@ def _check_necessary(self, request): def _is_excluded_path(self, actual_path): paths = settings.PASSWORD_CHANGE_MIDDLEWARE_EXCLUDED_PATHS[:] - path = r"^%s$" % self.url + path = rf"^{self.url}$" paths.append(path) media_url = django_setings.MEDIA_URL if media_url: - paths.append(r"^%s?" % media_url) + paths.append(rf"^{media_url}?") static_url = django_setings.STATIC_URL if static_url: - paths.append(r"^%s?" % static_url) + paths.append(rf"^{static_url}?") if settings.PASSWORD_CHANGE_MIDDLEWARE_ALLOW_LOGOUT: try: logout_url = reverse("logout") except NoReverseMatch: pass else: - paths.append(r"^%s$" % logout_url) + paths.append(rf"^{logout_url}$") try: - logout_url = u"/admin/logout/" + logout_url = "/admin/logout/" resolve(logout_url) except Resolver404: pass else: - paths.append(r"^%s$" % logout_url) + paths.append(rf"^{logout_url}$") for path in paths: if re.match(path, actual_path): return True @@ -163,7 +146,7 @@ def _redirect(self, request): next_to = redirect_to else: next_to = request.get_full_path() - url = "%s?%s=%s" % (self.url, settings.REDIRECT_FIELD_NAME, next_to) + url = f"{self.url}?{settings.REDIRECT_FIELD_NAME}={next_to}" return HttpResponseRedirect(url) def process_request(self, request): diff --git a/password_policies/migrations/0001_initial.py b/password_policies/migrations/0001_initial.py index e9afa50..4b6a77e 100644 --- a/password_policies/migrations/0001_initial.py +++ b/password_policies/migrations/0001_initial.py @@ -1,12 +1,11 @@ # Generated by Django 2.0 on 2017-12-08 19:54 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,32 +14,89 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='PasswordChangeRequired', + name="PasswordChangeRequired", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, db_index=True, help_text='The date the entry was created.', verbose_name='created')), - ('user', models.OneToOneField(help_text='The user who needs to change his/her password.', on_delete=django.db.models.deletion.CASCADE, related_name='password_change_required', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="The date the entry was created.", + verbose_name="created", + ), + ), + ( + "user", + models.OneToOneField( + help_text="The user who needs to change his/her password.", + on_delete=django.db.models.deletion.CASCADE, + related_name="password_change_required", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'enforced password change', - 'verbose_name_plural': 'enforced password changes', - 'ordering': ['-created'], - 'get_latest_by': 'created', + "verbose_name": "enforced password change", + "verbose_name_plural": "enforced password changes", + "ordering": ["-created"], + "get_latest_by": "created", }, ), migrations.CreateModel( - name='PasswordHistory', + name="PasswordHistory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, db_index=True, help_text='The date the entry was created.', verbose_name='created')), - ('password', models.CharField(help_text='The encrypted password.', max_length=128, verbose_name='password')), - ('user', models.ForeignKey(help_text='The user this password history entry belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='password_history_entries', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="The date the entry was created.", + verbose_name="created", + ), + ), + ( + "password", + models.CharField( + help_text="The encrypted password.", + max_length=128, + verbose_name="password", + ), + ), + ( + "user", + models.ForeignKey( + help_text="The user this password history entry belongs to.", + on_delete=django.db.models.deletion.CASCADE, + related_name="password_history_entries", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'password history entry', - 'verbose_name_plural': 'password history entries', - 'ordering': ['-created'], - 'get_latest_by': 'created', + "verbose_name": "password history entry", + "verbose_name_plural": "password history entries", + "ordering": ["-created"], + "get_latest_by": "created", }, ), ] diff --git a/password_policies/migrations/0002_passwordprofile.py b/password_policies/migrations/0002_passwordprofile.py index e506865..14b3849 100644 --- a/password_policies/migrations/0002_passwordprofile.py +++ b/password_policies/migrations/0002_passwordprofile.py @@ -1,31 +1,61 @@ # Generated by Django 3.1.5 on 2021-01-09 07:27 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('password_policies', '0001_initial'), + ("password_policies", "0001_initial"), ] operations = [ migrations.CreateModel( - name='PasswordProfile', + name="PasswordProfile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(db_index=True, help_text='The date the entry was created.', verbose_name='created')), - ('last_changed', models.DateTimeField(db_index=True, help_text='The date the password was last changed.', verbose_name='last changed')), - ('user', models.OneToOneField(help_text='The user this password profile belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='password_profile', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField( + db_index=True, + help_text="The date the entry was created.", + verbose_name="created", + ), + ), + ( + "last_changed", + models.DateTimeField( + db_index=True, + help_text="The date the password was last changed.", + verbose_name="last changed", + ), + ), + ( + "user", + models.OneToOneField( + help_text="The user this password profile belongs to.", + on_delete=django.db.models.deletion.CASCADE, + related_name="password_profile", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'password profile', - 'verbose_name_plural': 'password profiles', - 'ordering': ['-created'], - 'get_latest_by': 'created', + "verbose_name": "password profile", + "verbose_name_plural": "password profiles", + "ordering": ["-created"], + "get_latest_by": "created", }, ), ] diff --git a/password_policies/migrations/0003_update_passwordprofile.py b/password_policies/migrations/0003_update_passwordprofile.py index ca29ac6..d10dcfa 100644 --- a/password_policies/migrations/0003_update_passwordprofile.py +++ b/password_policies/migrations/0003_update_passwordprofile.py @@ -4,20 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('password_policies', '0002_passwordprofile'), + ("password_policies", "0002_passwordprofile"), ] operations = [ migrations.AlterField( - model_name='passwordprofile', - name='created', - field=models.DateTimeField(auto_now_add=True, db_index=True, help_text='The date the entry was created.', verbose_name='created'), + model_name="passwordprofile", + name="created", + field=models.DateTimeField( + auto_now_add=True, + db_index=True, + help_text="The date the entry was created.", + verbose_name="created", + ), ), migrations.AlterField( - model_name='passwordprofile', - name='last_changed', - field=models.DateTimeField(auto_now=True, db_index=True, help_text='The date the password was last changed.', verbose_name='last changed'), + model_name="passwordprofile", + name="last_changed", + field=models.DateTimeField( + auto_now=True, + db_index=True, + help_text="The date the password was last changed.", + verbose_name="last changed", + ), ), ] diff --git a/password_policies/models.py b/password_policies/models.py index 9426d6b..0f92c3d 100644 --- a/password_policies/models.py +++ b/password_policies/models.py @@ -3,11 +3,7 @@ from django.db import models from django.db.models import signals from django.utils import timezone -try: - from django.utils.translation import gettext_lazy as _ -except ImportError: - # Before in Django 3.0 - from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from password_policies.conf import settings from password_policies.managers import PasswordHistoryManager @@ -39,6 +35,9 @@ class Meta: verbose_name = _("enforced password change") verbose_name_plural = _("enforced password changes") + def __str__(self): + return f"{self.user} f{self.created}" + class PasswordHistory(models.Model): """ @@ -73,6 +72,9 @@ class Meta: verbose_name = _("password history entry") verbose_name_plural = _("password history entries") + def __str__(self): + return f"{self.user} f{self.created}" + class PasswordProfile(models.Model): """ @@ -106,6 +108,9 @@ class Meta: verbose_name = _("password profile") verbose_name_plural = _("password profiles") + def __str__(self): + return f"{self.user} last changed f{self.last_changed}" + def create_password_profile_signal(sender, instance, created, **kwargs): if created: diff --git a/password_policies/receivers.py b/password_policies/receivers.py index 95b56c4..3bdc551 100644 --- a/password_policies/receivers.py +++ b/password_policies/receivers.py @@ -1,13 +1,16 @@ import importlib + from django.core.signals import setting_changed from django.dispatch import receiver + from .conf import settings as password_settings + @receiver(setting_changed) def app_settings_reload_handler(**kwargs): """ When you modify settings in your test using override_settings, we need to reload the app settings module this receiver is in fact imported in tests/__init__.py module """ - if "PASSWORD_" in kwargs['setting']: + if "PASSWORD_" in kwargs["setting"]: importlib.reload(password_settings) diff --git a/password_policies/south_migrations/0001_initial.py b/password_policies/south_migrations/0001_initial.py index dc0620c..9fbf5a7 100644 --- a/password_policies/south_migrations/0001_initial.py +++ b/password_policies/south_migrations/0001_initial.py @@ -1,81 +1,171 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime +from django.contrib.auth import get_user_model from south.db import db from south.v2 import SchemaMigration -from django.db import models -try: - from django.contrib.auth import get_user_model -except ImportError: # django < 1.5 - from django.contrib.auth.models import User -else: - User = get_user_model() +User = get_user_model() class Migration(SchemaMigration): - def forwards(self, orm): # Adding model 'PasswordChangeRequired' - db.create_table(u'password_policies_passwordchangerequired', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='password_change_required', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])), - )) - db.send_create_signal(u'password_policies', ['PasswordChangeRequired']) + db.create_table( + "password_policies_passwordchangerequired", + ( + ("id", self.gf("django.db.models.fields.AutoField")(primary_key=True)), + ( + "created", + self.gf("django.db.models.fields.DateTimeField")( + auto_now_add=True, db_index=True, blank=True + ), + ), + ( + "user", + self.gf("django.db.models.fields.related.OneToOneField")( + related_name="password_change_required", + unique=True, + to=orm[f"{User._meta.app_label}.{User._meta.object_name}"], + ), + ), + ), + ) + db.send_create_signal("password_policies", ["PasswordChangeRequired"]) # Adding model 'PasswordHistory' - db.create_table(u'password_policies_passwordhistory', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)), - ('password', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='password_history_entries', to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])), - )) - db.send_create_signal(u'password_policies', ['PasswordHistory']) + db.create_table( + "password_policies_passwordhistory", + ( + ("id", self.gf("django.db.models.fields.AutoField")(primary_key=True)), + ( + "created", + self.gf("django.db.models.fields.DateTimeField")( + auto_now_add=True, db_index=True, blank=True + ), + ), + ( + "password", + self.gf("django.db.models.fields.CharField")(max_length=128), + ), + ( + "user", + self.gf("django.db.models.fields.related.ForeignKey")( + related_name="password_history_entries", + to=orm[f"{User._meta.app_label}.{User._meta.object_name}"], + ), + ), + ), + ) + db.send_create_signal("password_policies", ["PasswordHistory"]) def backwards(self, orm): # Deleting model 'PasswordChangeRequired' - db.delete_table(u'password_policies_passwordchangerequired') + db.delete_table("password_policies_passwordchangerequired") # Deleting model 'PasswordHistory' - db.delete_table(u'password_policies_passwordhistory') + db.delete_table("password_policies_passwordhistory") models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + "auth.group": { + "Meta": {"object_name": "Group"}, + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "name": ( + "django.db.models.fields.CharField", + [], + {"unique": "True", "max_length": "80"}, + ), + "permissions": ( + "django.db.models.fields.related.ManyToManyField", + [], + { + "to": "orm['auth.Permission']", + "symmetrical": "False", + "blank": "True", + }, + ), + }, + "auth.permission": { + "Meta": { + "ordering": "(u'content_type__app_label', u'content_type__model', u'codename')", + "unique_together": "((u'content_type', u'codename'),)", + "object_name": "Permission", + }, + "codename": ( + "django.db.models.fields.CharField", + [], + {"max_length": "100"}, + ), + "content_type": ( + "django.db.models.fields.related.ForeignKey", + [], + {"to": "orm['contenttypes.ContentType']"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "name": ("django.db.models.fields.CharField", [], {"max_length": "50"}), }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + f"{User._meta.app_label}.{User._meta.module_name}": { + "Meta": { + "object_name": User._meta.module_name, + "db_table": repr(User._meta.db_table), + }, }, - "%s.%s" % (User._meta.app_label, User._meta.module_name): { - 'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)}, + "contenttypes.contenttype": { + "Meta": { + "ordering": "('name',)", + "unique_together": "(('app_label', 'model'),)", + "object_name": "ContentType", + "db_table": "'django_content_type'", + }, + "app_label": ( + "django.db.models.fields.CharField", + [], + {"max_length": "100"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "model": ("django.db.models.fields.CharField", [], {"max_length": "100"}), + "name": ("django.db.models.fields.CharField", [], {"max_length": "100"}), }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + "password_policies.passwordchangerequired": { + "Meta": { + "ordering": "['-created']", + "object_name": "PasswordChangeRequired", + }, + "created": ( + "django.db.models.fields.DateTimeField", + [], + {"auto_now_add": "True", "db_index": "True", "blank": "True"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "user": ( + "django.db.models.fields.related.OneToOneField", + [], + { + "related_name": "'password_change_required'", + "unique": "True", + "to": "orm['auth.User']", + }, + ), }, - u'password_policies.passwordchangerequired': { - 'Meta': {'ordering': "['-created']", 'object_name': 'PasswordChangeRequired'}, - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'password_change_required'", 'unique': 'True', 'to': u"orm['auth.User']"}) + "password_policies.passwordhistory": { + "Meta": {"ordering": "['-created']", "object_name": "PasswordHistory"}, + "created": ( + "django.db.models.fields.DateTimeField", + [], + {"auto_now_add": "True", "db_index": "True", "blank": "True"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "password": ( + "django.db.models.fields.CharField", + [], + {"max_length": "128"}, + ), + "user": ( + "django.db.models.fields.related.ForeignKey", + [], + { + "related_name": "'password_history_entries'", + "to": "orm['auth.User']", + }, + ), }, - u'password_policies.passwordhistory': { - 'Meta': {'ordering': "['-created']", 'object_name': 'PasswordHistory'}, - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'password_history_entries'", 'to': u"orm['auth.User']"}) - } } - complete_apps = ['password_policies'] + complete_apps = ["password_policies"] diff --git a/password_policies/tests/templates/403.html b/password_policies/tests/templates/403.html index 051c9f6..d018624 100644 --- a/password_policies/tests/templates/403.html +++ b/password_policies/tests/templates/403.html @@ -7,4 +7,4 @@

403 Forbidden

- \ No newline at end of file + diff --git a/password_policies/tests/templates/registration/password_change_done.html b/password_policies/tests/templates/registration/password_change_done.html index d56b10f..49997a2 100644 --- a/password_policies/tests/templates/registration/password_change_done.html +++ b/password_policies/tests/templates/registration/password_change_done.html @@ -1 +1 @@ -E-mail sent \ No newline at end of file +E-mail sent diff --git a/password_policies/tests/templates/registration/password_change_form.html b/password_policies/tests/templates/registration/password_change_form.html index 027da71..5dd03cb 100644 --- a/password_policies/tests/templates/registration/password_change_form.html +++ b/password_policies/tests/templates/registration/password_change_form.html @@ -1 +1 @@ -{{ form.as_ul }} \ No newline at end of file +{{ form.as_ul }} diff --git a/password_policies/tests/templates/registration/password_reset_complete.html b/password_policies/tests/templates/registration/password_reset_complete.html index d81138b..c99b8da 100644 --- a/password_policies/tests/templates/registration/password_reset_complete.html +++ b/password_policies/tests/templates/registration/password_reset_complete.html @@ -1 +1 @@ -Password reset. \ No newline at end of file +Password reset. diff --git a/password_policies/tests/templates/registration/password_reset_confirm.html b/password_policies/tests/templates/registration/password_reset_confirm.html index 1db7762..420bf42 100644 --- a/password_policies/tests/templates/registration/password_reset_confirm.html +++ b/password_policies/tests/templates/registration/password_reset_confirm.html @@ -1 +1 @@ -{% if validlink %}Please enter your new password: {{ form }}{% else %}The password reset link was invalid.{% endif %} \ No newline at end of file +{% if validlink %}Please enter your new password: {{ form }}{% else %}The password reset link was invalid.{% endif %} diff --git a/password_policies/tests/templates/registration/password_reset_done.html b/password_policies/tests/templates/registration/password_reset_done.html index d56b10f..49997a2 100644 --- a/password_policies/tests/templates/registration/password_reset_done.html +++ b/password_policies/tests/templates/registration/password_reset_done.html @@ -1 +1 @@ -E-mail sent \ No newline at end of file +E-mail sent diff --git a/password_policies/tests/templates/registration/password_reset_form.html b/password_policies/tests/templates/registration/password_reset_form.html index 027da71..5dd03cb 100644 --- a/password_policies/tests/templates/registration/password_reset_form.html +++ b/password_policies/tests/templates/registration/password_reset_form.html @@ -1 +1 @@ -{{ form.as_ul }} \ No newline at end of file +{{ form.as_ul }} diff --git a/password_policies/tests/test_forms.py b/password_policies/tests/test_forms.py index 1e38b7c..428cbec 100644 --- a/password_policies/tests/test_forms.py +++ b/password_policies/tests/test_forms.py @@ -1,10 +1,6 @@ from django.test import TestCase from django.test.utils import override_settings - -try: - from django.utils.encoding import force_text -except ImportError: - from django.utils.encoding import force_str as force_text +from django.utils.encoding import force_str from password_policies.forms import ( PasswordPoliciesChangeForm, @@ -136,7 +132,7 @@ def test_reused_password(self): self.assertFalse(form.is_valid()) self.assertEqual( form["new_password1"].errors, - [force_text(form.error_messages["password_used"])], + [force_str(form.error_messages["password_used"])], ) def test_password_mismatch(self): @@ -145,7 +141,7 @@ def test_password_mismatch(self): self.assertFalse(form.is_valid()) self.assertEqual( form["new_password2"].errors, - [force_text(form.error_messages["password_mismatch"])], + [force_str(form.error_messages["password_mismatch"])], ) def test_password_verification_unicode(self): @@ -176,7 +172,7 @@ def test_password_invalid(self): self.assertFalse(form.is_valid()) self.assertEqual( form["old_password"].errors, - [force_text(form.error_messages["password_incorrect"])], + [force_str(form.error_messages["password_incorrect"])], ) self.assertFalse(form.is_valid()) @@ -202,7 +198,7 @@ def test_unusable_password(self): form = PasswordResetForm(data) self.assertFalse(form.is_valid()) self.assertEqual( - form["email"].errors, [force_text(form.error_messages["unusable"])] + form["email"].errors, [force_str(form.error_messages["unusable"])] ) self.assertFalse(form.is_valid()) diff --git a/password_policies/tests/test_middleware.py b/password_policies/tests/test_middleware.py index 0cc9bc5..0a085e5 100644 --- a/password_policies/tests/test_middleware.py +++ b/password_policies/tests/test_middleware.py @@ -1,16 +1,7 @@ -from django.test import TestCase - -try: - from urllib.parse import urljoin -except ImportError: - from urlparse import urljoin +from urllib.parse import urljoin -try: - from django.core.urlresolvers import reverse -except ImportError: - from django.urls.base import reverse - -from django.test.utils import override_settings +from django.test import TestCase +from django.urls import reverse from django.utils import timezone from password_policies.conf import settings diff --git a/password_policies/tests/test_models.py b/password_policies/tests/test_models.py index f032495..3c75530 100644 --- a/password_policies/tests/test_models.py +++ b/password_policies/tests/test_models.py @@ -9,7 +9,7 @@ class PasswordHistoryModelTestCase(TestCase): def setUp(self): self.user = create_user() create_password_history(self.user) - return super(PasswordHistoryModelTestCase, self).setUp() + return super().setUp() def test_password_history_expiration_with_offset(self): offset = settings.PASSWORD_HISTORY_COUNT + 2 @@ -23,4 +23,6 @@ def test_password_history_expiration(self): self.assertEqual(count, settings.PASSWORD_HISTORY_COUNT) def test_password_history_recent_passwords(self): - self.assertFalse(PasswordHistory.objects.check_password(self.user, passwords[-1])) + self.assertFalse( + PasswordHistory.objects.check_password(self.user, passwords[-1]) + ) diff --git a/password_policies/tests/test_utils.py b/password_policies/tests/test_utils.py index 4643049..14cb612 100644 --- a/password_policies/tests/test_utils.py +++ b/password_policies/tests/test_utils.py @@ -10,7 +10,7 @@ def setUp(self): self.user = create_user() self.check = PasswordCheck(self.user) create_password_history(self.user) - return super(PasswordPoliciesUtilsTest, self).setUp() + return super().setUp() def test_password_check_is_required(self): # by default no change is required diff --git a/password_policies/tests/test_views.py b/password_policies/tests/test_views.py index bb5de5b..26b7341 100644 --- a/password_policies/tests/test_views.py +++ b/password_policies/tests/test_views.py @@ -3,24 +3,23 @@ from django import VERSION as DJANGO_VERSION from django.core import signing from django.test import Client, TestCase, override_settings -from django.utils import timezone from django.urls.base import reverse +from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +from freezegun import freeze_time from password_policies.conf import settings from password_policies.forms import PasswordPoliciesChangeForm from password_policies.models import PasswordHistory from password_policies.tests.lib import create_user, passwords -from password_policies.utils import string_to_datetime, datetime_to_string +from password_policies.utils import datetime_to_string, string_to_datetime -from freezegun import freeze_time class PasswordChangeViewsTestCase(TestCase): def setUp(self): self.user = create_user() - return super(PasswordChangeViewsTestCase, self).setUp() - # + return super().setUp() def test_password_change(self): """ @@ -51,10 +50,7 @@ def test_password_change_failure(self): response = self.client.post(reverse("password_change"), data=data) self.assertEqual(response.status_code, 200) self.assertFalse(response.context["form"].is_valid()) - if DJANGO_VERSION > (4, 1): - self.assertFormError(response.context["form"], field="old_password", errors=msg) - else: - self.assertFormError(response, "form", field="old_password", errors=msg) + self.assertFormError(response.context["form"], field="old_password", errors=msg) self.client.logout() def test_password_change_success(self): @@ -89,12 +85,14 @@ def test_password_change_confirm(self): ) assert res.status_code == 200 - @override_settings(AUTH_PASSWORD_VALIDATORS=[ - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - "OPTIONS": {"min_length": 20}, - } - ]) + @override_settings( + AUTH_PASSWORD_VALIDATORS=[ + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "OPTIONS": {"min_length": 20}, + } + ] + ) def test_password_change_wrong_validators(self): """ A ``POST`` to the ``change_email_create`` view with valid data properly @@ -106,15 +104,14 @@ def test_password_change_wrong_validators(self): "new_password1": "Chah+pher9k", "new_password2": "Chah+pher9k", } - msg = 'This password is too short. It must contain at least 20 characters.' + msg = "This password is too short. It must contain at least 20 characters." self.client.login(username="alice", password=data["old_password"]) response = self.client.post(reverse("password_change"), data=data) self.assertEqual(response.status_code, 200) self.assertFalse(response.context["form"].is_valid()) - if DJANGO_VERSION > (4, 1): - self.assertFormError(response.context["form"], field="new_password2", errors=msg) - else: - self.assertFormError(response, "form", field="new_password2", errors=msg) + self.assertFormError( + response.context["form"], field="new_password2", errors=msg + ) self.client.logout() def test_password_reset_complete(self): @@ -125,8 +122,11 @@ def test_password_reset_complete(self): ) assert res.status_code == 200 - @skipIf(DJANGO_VERSION >= (5, 0), 'PickleSerializer not supported in this version') - @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer', USE_TZ=False) + @skipIf(DJANGO_VERSION >= (5, 0), "PickleSerializer not supported in this version") + @override_settings( + SESSION_SERIALIZER="django.contrib.sessions.serializers.PickleSerializer", + USE_TZ=False, + ) @freeze_time("2021-07-21T17:00:00.000000") def test_pickle_serializer_set_datetime_USE_TZ_false(self): data = { @@ -135,28 +135,51 @@ def test_pickle_serializer_set_datetime_USE_TZ_false(self): "new_password2": "Chah+pher9k", } self.client.login(username="alice", password=data["old_password"]) - response = self.client.post(reverse("password_change"), data=data) + self.client.post(reverse("password_change"), data=data) session = self.client.session # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - "2021-07-21T17:00:00.000000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + ), + timezone.now(), + ) # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - "2021-07-21T17:00:00.000000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + ), + timezone.now(), + ) - @skipIf(DJANGO_VERSION >= (5, 0), 'PickleSerializer not supported in this version') - @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer', USE_TZ=True) + @skipIf(DJANGO_VERSION >= (5, 0), "PickleSerializer not supported in this version") + @override_settings( + SESSION_SERIALIZER="django.contrib.sessions.serializers.PickleSerializer", + USE_TZ=True, + ) @freeze_time("2021-07-21T17:00:00.000000") def test_pickle_serializer_set_datetime_USE_TZ_true(self): data = { @@ -165,28 +188,51 @@ def test_pickle_serializer_set_datetime_USE_TZ_true(self): "new_password2": "Chah+pher9k", } self.client.login(username="alice", password=data["old_password"]) - response = self.client.post(reverse("password_change"), data=data) + self.client.post(reverse("password_change"), data=data) session = self.client.session # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + ), + timezone.now(), + ) # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + ), + timezone.now(), + ) - @skipIf(DJANGO_VERSION >= (5, 0), 'PickleSerializer not supported in this version') - @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer', USE_TZ=True) + @skipIf(DJANGO_VERSION >= (5, 0), "PickleSerializer not supported in this version") + @override_settings( + SESSION_SERIALIZER="django.contrib.sessions.serializers.PickleSerializer", + USE_TZ=True, + ) @freeze_time("2021-07-21T18:00:00.000000+0100") def test_pickle_serializer_set_datetime_USE_TZ_true_localized(self): data = { @@ -195,27 +241,50 @@ def test_pickle_serializer_set_datetime_USE_TZ_true_localized(self): "new_password2": "Chah+pher9k", } self.client.login(username="alice", password=data["old_password"]) - response = self.client.post(reverse("password_change"), data=data) + self.client.post(reverse("password_change"), data=data) session = self.client.session # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + ), + timezone.now(), + ) # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + ), + timezone.now(), + ) - @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', USE_TZ=False) + @override_settings( + SESSION_SERIALIZER="django.contrib.sessions.serializers.JSONSerializer", + USE_TZ=False, + ) @freeze_time("2021-07-21T17:00:00.000000") def test_json_serializer_set_datetime_USE_TZ_false(self): data = { @@ -224,27 +293,50 @@ def test_json_serializer_set_datetime_USE_TZ_false(self): "new_password2": "Chah+pher9k", } self.client.login(username="alice", password=data["old_password"]) - response = self.client.post(reverse("password_change"), data=data) + self.client.post(reverse("password_change"), data=data) session = self.client.session # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - "2021-07-21T17:00:00.000000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + ), + timezone.now(), + ) # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - "2021-07-21T17:00:00.000000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + ), + timezone.now(), + ) - @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', USE_TZ=True) + @override_settings( + SESSION_SERIALIZER="django.contrib.sessions.serializers.JSONSerializer", + USE_TZ=True, + ) @freeze_time("2021-07-21T17:00:00.000000") def test_json_serializer_set_datetime_USE_TZ_true(self): data = { @@ -253,28 +345,50 @@ def test_json_serializer_set_datetime_USE_TZ_true(self): "new_password2": "Chah+pher9k", } self.client.login(username="alice", password=data["old_password"]) - response = self.client.post(reverse("password_change"), data=data) + self.client.post(reverse("password_change"), data=data) session = self.client.session # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + ), + timezone.now(), + ) # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), - timezone.now()) - + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + ), + timezone.now(), + ) - @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', USE_TZ=True) + @override_settings( + SESSION_SERIALIZER="django.contrib.sessions.serializers.JSONSerializer", + USE_TZ=True, + ) @freeze_time("2021-07-21T18:00:00.000000+0100") def test_json_serializer_set_datetime_USE_TZ_true_localized(self): data = { @@ -283,25 +397,45 @@ def test_json_serializer_set_datetime_USE_TZ_true_localized(self): "new_password2": "Chah+pher9k", } self.client.login(username="alice", password=data["old_password"]) - response = self.client.post(reverse("password_change"), data=data) + self.client.post(reverse("password_change"), data=data) session = self.client.session # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + ), + timezone.now(), + ) # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] - self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - datetime_to_string(timezone.now())) - self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], - "2021-07-21T17:00:00.000000+0000") - self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), - timezone.now()) + self.assertIsInstance( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now()), + ) + self.assertEqual( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000", + ) + self.assertEqual( + string_to_datetime( + session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + ), + timezone.now(), + ) class TestLOMixinView(TestCase): diff --git a/password_policies/tests/urls.py b/password_policies/tests/urls.py index 4371bed..03fd8f5 100644 --- a/password_policies/tests/urls.py +++ b/password_policies/tests/urls.py @@ -1,21 +1,9 @@ -try: - from django.conf.urls import include, url -except ImportError: - from django.urls import include - from django.urls import re_path as url - -try: - from django.conf.urls import patterns -except ImportError: - patterns = False +from django.urls import include, path from password_policies.tests.views import TestHomeView, TestLoggedOutMixinView urlpatterns = [ - url(r"^password/", include("password_policies.urls")), - url(r"^$", TestHomeView.as_view(), name="home"), - url(r"^fubar/", TestLoggedOutMixinView.as_view(), name="loggedoutmixin"), + path("password/", include("password_policies.urls")), + path("", TestHomeView.as_view(), name="home"), + path("fubar/", TestLoggedOutMixinView.as_view(), name="loggedoutmixin"), ] - -if patterns: - urlpatterns = patterns("", *urlpatterns) # noqa diff --git a/password_policies/tests/views.py b/password_policies/tests/views.py index e6ef35b..727b35c 100644 --- a/password_policies/tests/views.py +++ b/password_policies/tests/views.py @@ -1,7 +1,8 @@ -from django.http import HttpResponse -from django.views.generic.base import View from django.contrib.auth.decorators import login_required +from django.http import HttpResponse from django.utils.decorators import method_decorator +from django.views.generic.base import View + from password_policies.views import LoggedOutMixin @@ -13,7 +14,7 @@ def get(self, request): @method_decorator(login_required) def dispatch(self, *args, **kwargs): - return super(TestHomeView, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) class TestLoggedOutMixinView(LoggedOutMixin): diff --git a/password_policies/urls.py b/password_policies/urls.py index 37515db..430e1ea 100644 --- a/password_policies/urls.py +++ b/password_policies/urls.py @@ -1,16 +1,4 @@ -try: - from django.urls import re_path as url -except ImportError: - # Before Django 2.0 - from django.conf.urls import url - -import django.conf.urls -if hasattr(django.conf.urls, 'patterns'): - # patterns was deprecated in Django 1.8 - from django.conf.urls import patterns -else: - # patterns is unavailable in Django 1.10+ - patterns = False +from django.urls import path, re_path from password_policies.views import ( PasswordChangeDoneView, @@ -22,24 +10,18 @@ ) urlpatterns = [ - url( - r"^change/done/$", PasswordChangeDoneView.as_view(), name="password_change_done" - ), - url(r"^change/$", PasswordChangeFormView.as_view(), name="password_change"), - url(r"^reset/$", PasswordResetFormView.as_view(), name="password_reset"), - url( - r"^reset/complete/$", + path("change/done/", PasswordChangeDoneView.as_view(), name="password_change_done"), + path("change/", PasswordChangeFormView.as_view(), name="password_change"), + path("reset/", PasswordResetFormView.as_view(), name="password_reset"), + path( + "reset/complete/", PasswordResetCompleteView.as_view(), name="password_reset_complete", ), - url( + re_path( r"^reset/confirm/([0-9A-Za-z_\-]+)/([0-9A-Za-z]{1,13})/([0-9A-Za-z-=_]{1,128})/$", PasswordResetConfirmView.as_view(), name="password_reset_confirm", ), - url(r"^reset/done/$", PasswordResetDoneView.as_view(), name="password_reset_done"), + path("reset/done/", PasswordResetDoneView.as_view(), name="password_reset_done"), ] - -if patterns: - # Django 1.7 - urlpatterns = patterns("", *urlpatterns) diff --git a/password_policies/utils.py b/password_policies/utils.py index 7023556..c408900 100644 --- a/password_policies/utils.py +++ b/password_policies/utils.py @@ -1,14 +1,14 @@ -from datetime import timedelta, datetime +from datetime import datetime, timedelta -from django.utils import timezone from django.conf import settings as django_settings from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone from password_policies.conf import settings from password_policies.models import PasswordHistory -class PasswordCheck(object): +class PasswordCheck: "Checks if a given user needs to change his/her password." def __init__(self, user): @@ -18,13 +18,13 @@ def __init__(self, user): def is_required(self): """Checks if a given user is forced to change his/her password. -If an instance of :class:`~password_policies.models.PasswordChangeRequired` -exists the verification is successful. + If an instance of :class:`~password_policies.models.PasswordChangeRequired` + exists the verification is successful. -:returns: ``True`` if the user needs to change his/her password, - ``False`` otherwise. -:rtype: bool -""" + :returns: ``True`` if the user needs to change his/her password, + ``False`` otherwise. + :rtype: bool + """ try: if self.user.password_change_required: return True @@ -35,10 +35,10 @@ def is_required(self): def is_expired(self): """Checks if a given user's password has expired. -:returns: ``True`` if the user's password has expired, - ``False`` otherwise. -:rtype: bool -""" + :returns: ``True`` if the user's password has expired, + ``False`` otherwise. + :rtype: bool + """ if PasswordHistory.objects.change_required(self.user): return True return False @@ -48,26 +48,36 @@ def get_expiry_datetime(self): seconds = settings.PASSWORD_DURATION_SECONDS return timezone.now() - timedelta(seconds=seconds) + def datetime_to_string(value, format=None): - """ Transform datetime object in a string with input format -:returns: formatted datetime -:rtype: str -""" + """Transform datetime object in a string with input format + :returns: formatted datetime + :rtype: str + """ if format is None: - format = "%Y-%m-%dT%H:%M:%S.%f%z" if django_settings.USE_TZ else "%Y-%m-%dT%H:%M:%S.%f" + format = ( + "%Y-%m-%dT%H:%M:%S.%f%z" + if django_settings.USE_TZ + else "%Y-%m-%dT%H:%M:%S.%f" + ) if not isinstance(value, str): return datetime.strftime(value, format) else: return value + def string_to_datetime(value, format=None): - """ Transform string object in a datetime with input format -:returns: formatted string -:rtype: datetime -""" + """Transform string object in a datetime with input format + :returns: formatted string + :rtype: datetime + """ if format is None: - format = "%Y-%m-%dT%H:%M:%S.%f%z" if django_settings.USE_TZ else "%Y-%m-%dT%H:%M:%S.%f" + format = ( + "%Y-%m-%dT%H:%M:%S.%f%z" + if django_settings.USE_TZ + else "%Y-%m-%dT%H:%M:%S.%f" + ) if not isinstance(value, datetime): return datetime.strptime(value, format) diff --git a/password_policies/views.py b/password_policies/views.py index 7b4ad68..b57f982 100644 --- a/password_policies/views.py +++ b/password_policies/views.py @@ -1,25 +1,11 @@ from django.contrib.auth import get_user_model, update_session_auth_hash from django.contrib.auth.decorators import login_required from django.core import signing -from django.utils import timezone - -from password_policies.exceptions import MustBeLoggedOutException - -try: - from django.urls.base import reverse -except ImportError: - # Before Django 2.0 - from django.core.urlresolvers import reverse - from django.shortcuts import resolve_url +from django.urls.base import reverse +from django.utils import timezone from django.utils.decorators import method_decorator - -try: - from django.utils.encoding import force_str as force_text -except ImportError: - # Before in Django 2.0 - from django.utils.encoding import force_text - +from django.utils.encoding import force_str from django.utils.http import urlsafe_base64_decode from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect @@ -29,12 +15,14 @@ from django.views.generic.edit import FormView from password_policies.conf import settings +from password_policies.exceptions import MustBeLoggedOutException from password_policies.forms import ( PasswordPoliciesChangeForm, PasswordPoliciesForm, PasswordResetForm, ) -from password_policies.utils import string_to_datetime, datetime_to_string +from password_policies.utils import datetime_to_string + class LoggedOutMixin(View): """ @@ -174,13 +162,13 @@ def dispatch(self, request, *args, **kwargs): self.validlink = False if self.uidb64 and self.timestamp and self.signature: try: - uid = force_text(urlsafe_base64_decode(self.uidb64)) + uid = force_str(urlsafe_base64_decode(self.uidb64)) self.user = get_user_model().objects.get(id=uid) except (ValueError, get_user_model().DoesNotExist): self.user = None else: signer = signing.TimestampSigner() - max_age = settings.PASSWORD_RESET_TIMEOUT_DAYS * 24 * 60 * 60 + max_age = settings.PASSWORD_RESET_TIMEOUT il = (self.user.password, self.timestamp, self.signature) try: signer.unsign(":".join(il), max_age=max_age) @@ -273,7 +261,7 @@ def form_valid(self, form): "request": self.request, } if self.is_admin_site: - opts = dict(opts, domain_override=self.request.META["HTTP_HOST"]) + opts = dict(opts, domain_override=self.request.headers["host"]) form.save(**opts) return super().form_valid(form) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f343b76 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-password-policies-iplweb" +version = "0.8.6" + +authors = [ + {name = "Michal Pasternak", email = "michal.dtz@gmail.com"} +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Framework :: Django", + "License :: OSI Approved :: BSD License", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities" +] +description = "A Django application to implent password policies." +readme = "README" +requires-python = ">=3.8" +dependencies = ["django"] + +[tool.ruff] +target-version = "py38" + +[tool.ruff.lint] +select = [ + "DJ", # flake8-django + "F", # flake8 + "I", # isort + "UP" # pyupgrade +] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = + py38-django42 + py39-django42 + py310-django42 + py311-django42 + py312-django42 + py310-django50 + py311-django50 + py312-django50 + py310-django51 + py311-django51 + py312-django51 +[testenv] +deps = + django51: Django>=5.1rc1,<5.2 + django50: Django>=5.0,<5.1 + django42: Django>=4.2,<4.3 + freezegun + pytest + pytest-django + pytest-cov +# package = editable +use_develop = true +commands = + pytest --cov password_policies --ds=password_policies.tests.test_settings password_policies/tests/ -s +""" diff --git a/requirements.txt b/requirements.txt index 8c8868a..0cb74bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -django>=2.2 +django>=4.2 diff --git a/setup.py b/setup.py deleted file mode 100644 index d5352b2..0000000 --- a/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -from setuptools import find_packages, setup - -install_requires = ["django"] - -setup( - name="django-password-policies-iplweb", - version=__import__("password_policies").__version__, - description="A Django application to implent password policies.", - long_description="""\ -django-password-policies is an application for the Django framework that -provides unicode-aware password policies on password changes and resets -and a mechanism to force password changes. -""", - author="Michal Pasternak", - author_email="michal.dtz@gmail.com", - url="https://github.com/iplweb/django-password-policies-iplweb", - include_package_data=True, - packages=find_packages(), - zip_safe=False, - classifiers=[ - "Development Status :: 3 - Alpha", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Framework :: Django", - "License :: OSI Approved :: BSD License", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Utilities", - ], - install_requires=install_requires, -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 2fcc06e..0000000 --- a/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -[flake8] -ignore = D503 - -[tox] -envlist = - py38-django22 - py39-django22 - py38-django30 - py39-django30 - py38-django31 - py39-django31 - py38-django32 - py39-django32 - py39-django40 - py310-django40 - py39-django41 - py310-django41 - py310-django42 - py311-django42 - py312-django42 - py310-django50 - py311-django50 - py312-django50 -[testenv] -deps = - django50: Django>=5.0,<5.1 - django42: Django>=4.2,<4.3 - django41: Django>=4.1,<4.2 - django40: Django>=4.0,<4.1 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 - django32: Django>=3.2,<3.3 - django22: Django>=2.2,<2.3 - freezegun - pytest - pytest-django - pytest-cov -# package = editable -use_develop = true -commands = - pytest --cov password_policies --ds=password_policies.tests.test_settings password_policies/tests/ -s