pax_global_header00006660000000000000000000000064147754064320014526gustar00rootroot0000000000000052 comment=0eb1e36ea3cf54f9d46b1eeae8f741d43b83e0da django-contact-form-5.2.0/000077500000000000000000000000001477540643200153665ustar00rootroot00000000000000django-contact-form-5.2.0/.editorconfig000066400000000000000000000004531477540643200200450ustar00rootroot00000000000000# https://editorconfig.org/ root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 [*.py] max_line_length = 88 [*.toml] indent_size = 2 [docs/**.rst] max_line_length = 79 [*.{yaml,yml}] indent_size = 2 django-contact-form-5.2.0/.flake8000066400000000000000000000001531477540643200165400ustar00rootroot00000000000000[flake8] extend-ignore = E203, E501, W503 max-complexity = 13 max-line-length = 88 select = C,E,F,W,B,B950 django-contact-form-5.2.0/.github/000077500000000000000000000000001477540643200167265ustar00rootroot00000000000000django-contact-form-5.2.0/.github/workflows/000077500000000000000000000000001477540643200207635ustar00rootroot00000000000000django-contact-form-5.2.0/.github/workflows/ci.yml000066400000000000000000000124171477540643200221060ustar00rootroot00000000000000--- name: CI on: push: branches: [ trunk ] pull_request: workflow_dispatch: env: FORCE_COLOR: "1" PIP_DISABLE_PIP_VERSION_CHECK: "1" PIP_NO_PYTHON_VERSION_WARNING: "1" permissions: { } jobs: build-package: name: Build and verify package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - uses: hynek/build-and-inspect-python-package@v2 id: baipp outputs: python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} tests: name: Tests on Python ${{ matrix.python-version }} needs: build-package runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ${{ fromJson(needs.build-package.outputs.python-versions) }} steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install test runner run: | python -VV python -Im site python -Im pip install --upgrade nox python -Im nox --version - name: Run tests run: "python -Im nox --non-interactive --error-on-external-run --tag tests --python ${{ matrix.python-version }}" - name: Upload coverage data uses: actions/upload-artifact@v4 with: name: coverage-data-${{ matrix.python-version }} path: .coverage.* include-hidden-files: true if-no-files-found: ignore coverage: name: Combine and check coverage needs: tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 with: python-version: "3.12" - uses: actions/download-artifact@v4 with: pattern: coverage-data-* merge-multiple: true - name: Combine coverage & fail under 100% run: | python -Im pip install --upgrade "coverage[toml]" coverage combine coverage html --skip-covered --skip-empty # Report and write to summary. coverage report --format=markdown >> $GITHUB_STEP_SUMMARY # Report again and fail if under 100%. coverage report --fail-under=100 - name: Upload HTML report if check failed. uses: actions/upload-artifact@v4 with: name: html-report path: htmlcov if: ${{ failure() }} docs: name: Check documentation needs: build-package runs-on: ubuntu-latest steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up test runner run: | python -VV python -Im site python -Im pip install --upgrade nox python -Im nox --version - name: Run documentation checks run: "python -Im nox --non-interactive --error-on-external-run --tag docs" lint-format: name: Lint code and check formatting needs: build-package runs-on: ubuntu-latest steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: python-version: "3.13" - name: Set up test runner run: | python -VV python -Im site python -Im pip install --upgrade nox python -Im nox --version - name: Check code formatting run: "python -Im nox --non-interactive --error-on-external-run --tag formatters --python 3.13" - name: Lint code run: "python -Im nox --non-interactive --error-on-external-run --tag linters --python 3.13" check-package: name: Additional package checks needs: build-package runs-on: ubuntu-latest steps: - name: Download pre-built packages uses: actions/download-artifact@v4 with: name: Packages path: dist - run: tar xf dist/*.tar.gz --strip-components=1 - uses: actions/setup-python@v5 with: python-version: "3.13" - name: Set up test runner run: | python -VV python -Im site python -Im pip install --upgrade nox python -Im nox --version - name: Check package run: "python -Im nox --non-interactive --error-on-external-run --tag packaging --python 3.13" required-checks-pass: name: Ensure required checks pass for branch protection if: always() needs: - check-package - coverage - docs - lint-format runs-on: ubuntu-latest steps: - name: Decide whether the jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} django-contact-form-5.2.0/.gitignore000066400000000000000000000035401477540643200173600ustar00rootroot00000000000000# Adapted from https://github.com/github/gitignore/blob/main/Python.gitignore # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations # *mo # commented out during work on #65 *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # IDEs. .idea/ .vscode/ # PDM local file. .pdm-python django-contact-form-5.2.0/.pre-commit-config.yaml000066400000000000000000000020231477540643200216440ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-added-large-files - id: check-ast - id: check-byte-order-marker - id: check-case-conflict - id: check-docstring-first - id: check-merge-conflict - id: check-toml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black language_version: python3.12 name: black (Python formatter) - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: - id: flake8 name: flake8 (Python linter) - repo: https://github.com/econchick/interrogate rev: 1.7.0 hooks: - id: interrogate name: interrogate (Python docstring enforcer) args: [--quiet] - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort name: isort (Python formatter) django-contact-form-5.2.0/.readthedocs.yaml000066400000000000000000000005131477540643200206140ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.12" sphinx: configuration: docs/conf.py python: install: - method: pip path: . - requirements: docs/requirements.txt django-contact-form-5.2.0/CONTRIBUTING.rst000066400000000000000000000065161477540643200200370ustar00rootroot00000000000000Contributor guide ================= Contributions are welcome, and several things about this repository have been set up to make the process easier for everyone (including you!). Prerequisites ------------- * Please use a code editor/IDE that supports `EditorConfig `_. Most editors do nowadays, so you probably don't have to worry about it, but it will help to automatically apply some formatting and style rules. * Please make sure you have `pre-commit `_ installed, and in your local checkout of this repository run ``pre-commit install`` to set up the pre-commit hooks. The following two tools are *required* for working with this repository: * `PDM `_ * `nox `_ You will also need at least one supported Python version. It is also recommended that you test against *all* the supported Python verisions before opening a pull request; you can use `PDM's Python installer `_ to install any versions of Python you need. Local setup ----------- Once you have the tools above installed, run the following in the root of your git checkout:: pdm install This will create a local virtual environment and install ``django-contact-form`` and its dependencies. Testing ------- To run the tests, use ``nox``:: nox --tags tests By default this will run against as many supported Python versions as you have installed. To select a single specific Python version, you can run:: nox --tags tests --python "3.11" You can also run the full CI suite locally by just invoking ``nox``. This will run the tests, check the documentation, lint the code and check formatting, and build a package and perform checks on it. For more information about available tasks, run ``nox --list`` or read the file ``noxfile.py`` in the root of your source checkout. Code style ---------- The pre-commit hooks will auto-format code with `isort `_ and `Black `_. Many editors and IDEs also support auto-formatting with these tools every time you save a file. The CI suite will disallow any code that does not follow the isort/Black format. All code must also be compatible with all supported versions of Python. Other guidelines ---------------- * If you need to add a new file of code, please make sure to put a license identifier comment near the top of the file. You can copy and paste the license identifier comment from any existing file, where it looks like this: ``# SPDX-License-Identifier: BSD-3-Clause`` * Documentation and tests are not just recommended -- they're required. Any new file, class, method or function must have a docstring and must either include that docstring (via autodoc) in the built documentation, or must have manually-written documentation in the ``docs/`` directory. Any new feature or bugfix must have sufficient tests to prove that it works, and the test coverage report must come out at 100%. The CI suite will fail if test coverage is below 100%, if there's any code which doesn't have a docstring, or if there are any misspelled words in the documentation (and if there's a word the spell-checker should learn to recognize, add it to ``docs/spelling_wordlist.txt``). django-contact-form-5.2.0/LICENSE000066400000000000000000000027731477540643200164040ustar00rootroot00000000000000Copyright (c) James Bennett, and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-contact-form-5.2.0/README.rst000066400000000000000000000026111477540643200170550ustar00rootroot00000000000000.. -*-restructuredtext-*- .. image:: https://github.com/ubernostrum/django-contact-form/workflows/CI/badge.svg :alt: CI status image :target: https://github.com/ubernostrum/django-contact-form/actions?query=workflow%3ACI ``django-contact-form`` provides customizable contact-form functionality for `Django `_-powered sites. This application includes: * An extensible base contact-form class which is also usable as-is for basic functionality (collecting a name, email address, and message) * A subclass of the base form which uses the Akismet spam-filtering service to detect and reject spam submissions * A class-based Django view which can be used with either of the built-in contact form classes, or your own customized form For the default contact-form functionality, add ``"django_contact_form"`` to your Django site's ``INSTALLED_APPS`` setting, add the following line to your site's root URLConf, and create the templates specified in `the usage guide `_: .. code-block:: python from django.urls import include, path urlpatterns = [ # ... other URL patterns for your site ... path("contact/", include("django_contact_form.urls")), ] Full documentation for all functionality is `available online `_. django-contact-form-5.2.0/docs/000077500000000000000000000000001477540643200163165ustar00rootroot00000000000000django-contact-form-5.2.0/docs/Makefile000066400000000000000000000127601477540643200177640ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-contact-form.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-contact-form.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-contact-form" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-contact-form" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-contact-form-5.2.0/docs/changelog.rst000066400000000000000000000153351477540643200210060ustar00rootroot00000000000000.. _changelog: Changelog ========= This is a list of changes made in released versions of ``django-contact-form`` over time. Version numbering ----------------- ``django-contact-form`` uses "DjangoVer", a version number system based on the corresponding supported Django versions. The format of a ``django-contact-form`` version number is ``A.B.C``, where ``A.B`` is the version number of the latest Django feature release supported by that version of ``django-contact-form``, and ``C`` is an incrementing value for releases of ``django-contact-form`` paired to that Django feature release. The policy of ``django-contact-form`` is to support the Django feature release indicated in the version number, along with any other lower-numbered Django feature releases receiving support from the Django project at the time of release. For example, consider a hypothetical ``django-contact-form`` version 5.0.2. This indicates that the most recent supported Django feature release is 5.0, and that it is the third release of ``django-contact-form`` to support Django 5.0 (after 5.0.0 and 5.0.1). Since the Django project at the time was supporting Django 5.0 and 4.2, that version of ``django-contact-form`` would also support Django 5.0 and 4.2. API stability and deprecations ------------------------------ The API stability/deprecation policy for ``django-contact-form`` is as follows: * The supported stable public API is the set of symbols which are documented in this documentation. For classes, the supported stable public API is the set of methods and attributes of those classes whose names do not begin with one or more underscore (``_``) characters and which are documented in this documentation. * When a public API is to be removed, or undergo a backwards-incompatible change, it will emit a deprecation warning which serves as notice of the intended removal or change. This warning will be emitted for at least two releases, after which the removal or change may occur without further warning. This is different from Django's own deprecation policy, which avoids completing a removal/change in "LTS"-designated releases. Since ``django-contact-form`` does not have "LTS" releases, it does not need that exception. * Security fixes, and fixes for high-severity bugs (such as those which might cause unrecoverable crash or data loss), are not required to emit deprecation warnings, and may -- if needed -- impose backwards-incompatible change in any release. If this occurs, this changelog document will contain a note explaining why the usual deprecation process could not be followed for that case. * This policy is in effect as of the adoption of "DjangoVer" versioning, with version 5.0.0 of ``django-contact-form``. Releases under DjangoVer ------------------------ Version 5.2.0 ~~~~~~~~~~~~~ Released April 2025 * Supported Django versions are now 4.2, 5.1, and 5.2. Version 5.1.2 ~~~~~~~~~~~~~ Released March 2025 * Fixed a bug with the Akismet integration where the first call to create an Akismet client would succeed in creating and returning it, but subsequent calls would not return the cached client instance. Now subsequent calls do correctly return the cached instance. Version 5.1.1 ~~~~~~~~~~~~~ Released November 2024 * Supported Python versions are now 3.9, 3.10, 3.11, 3.12, and 3.13. Version 5.1.0 ~~~~~~~~~~~~~ Released August 2024 * Supported Django versions are now 4.2, 5.0, and 5.1. Version 5.0.1 ~~~~~~~~~~~~~ Released May 2024 * Correct an issue in the changelog for 5.0.0. * Correct an issue with the display of the package's documentation/source URLs on the Python Package Index. Version 5.0.0 ~~~~~~~~~~~~~ Released May 2024 * Adopted "DjangoVer" versioning. * The :class:`~django_contact_form.forms.AkismetContactForm` and its Akismet integration have been rewritten to make use of more recent versions of the Python Akismet client. Configuring the Akismet client via Django settings is now deprecated, and support for configuring via Django settings will be removed in a future version of ``django-contact-form``. The ``AkismetContactForm`` class now also provides two overridable public methods to allow customization of the Akismet API client instance and the arguments passed to the Akismet spam check. Releases not under DjangoVer ---------------------------- Version 2.1 ~~~~~~~~~~~ Released July 2023 * The supported Django versions are now 3.2, 4.1, and 4.2. Version 2.0.1 ~~~~~~~~~~~~~ Released May 2022 * Corrected several issues in the documentation of the 2.0 release. Version 2.0 ~~~~~~~~~~~ Released May 2022 Major version bump, with several changes: * The supported Django versions are 3.2 and 4.0. * Prior to 2.x, django-contact-form installed a Python module named ``contact_form``. To avoid silent incompatibilities, and to conform to more recent best practices, django-contact-form 2.x now installs a module named ``django_contact_form``. Attempts to import from the ``contact_form`` module will immediately fail with :exc:`ImportError`. Many installations will be able to adapt by replacing references to ``contact_form`` with references to ``django_contact_form``. * Similar to the module renaming above, the name of the default directory in which django-contact-form looks for templates has changed from ``contact_form/`` to ``django_contact_form/``. * Prior to 2.x, :class:`~django_contact_form.forms.ContactForm` provided a method named ``get_context()`` which was used to generate the template context from which the message would be rendered. However, Django 4.0 introduced `a new template-based system for rendering forms `_, and as a result :class:`django.forms.Form` now has a method named :meth:`~django.forms.Form.get_context`. To resolve this conflict with Django's own base form class, the method in django-contact-form has been renamed to :meth:`~django_contact_form.forms.ContactForm.get_message_context`, which hopefully will not be adopted by any future version of Django's own forms system. If you were previously overriding ``get_context()``, you should rename your overridden method to :meth:`~django_contact_form.forms.ContactForm.get_message_context` to ensure it is still called properly. If you have other code which called ``get_context()``, you should update any such references to call :meth:`~django_contact_form.forms.ContactForm.get_message_context` instead. Pre-2.0 versions ~~~~~~~~~~~~~~~~ ``django-contact-form`` 1.0 was released in August 2013. Between that release and 2.0 in 2022, no new features were added to ``django-contact-form``, and releases were concerned solely with ensuring and documenting support for new Django releases. django-contact-form-5.2.0/docs/conf.py000066400000000000000000000040741477540643200176220ustar00rootroot00000000000000""" Configuration file for the Sphinx documentation builder: https://www.sphinx-doc.org/ """ # SPDX-License-Identifier: BSD-3-Clause import os import sys from importlib.metadata import version as get_version import django from django.conf import settings settings.configure( INSTALLED_APPS=[ "django_contact_form", ], DEBUG=True, ) django.setup() extensions = [ "notfound.extension", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinxext.opengraph", "sphinx_copybutton", "sphinx_inline_tabs", ] templates_path = ["_templates"] source_suffix = {".rst": "restructuredtext"} master_doc = "index" project = "django-contact-form" copyright = "James Bennett and contributors" version = get_version("django-contact-form") release = version exclude_trees = ["_build"] pygments_style = "sphinx" htmlhelp_basename = "django-contact-formdoc" html_theme = "furo" latex_documents = [ ( "index", "django-contact-form.tex", "django-contact-form Documentation", "James Bennett", "manual", ) ] intersphinx_mapping = { "django": ( "https://docs.djangoproject.com/en/stable/", "https://docs.djangoproject.com/en/stable/_objects/", ), "python": ("https://docs.python.org/3", None), } # Spelling check needs an additional module that is not installed by default. # Add it only if spelling check is requested so docs can be generated without it. if "spelling" in sys.argv: extensions.append("sphinxcontrib.spelling") # Spelling language. spelling_lang = "en_US" # Location of word list. spelling_word_list_filename = "spelling_wordlist.txt" # The documentation does not include contributor names, so we skip this because it's # flaky about needing to scan commit history. spelling_ignore_contributor_names = False # OGP metadata configuration. ogp_enable_meta_description = True ogp_site_url = "https://django-contact-form.readthedocs.io/" # Django settings for sphinxcontrib-django. sys.path.insert(0, os.path.abspath(".")) django_settings = "docs_settings" django-contact-form-5.2.0/docs/docs_settings.py000066400000000000000000000001521477540643200215360ustar00rootroot00000000000000""" Minimal Django settings file for documentation builds. """ INSTALLED_APPS = ["django_contact_form"] django-contact-form-5.2.0/docs/faq.rst000066400000000000000000000072001477540643200176160ustar00rootroot00000000000000.. _faq: Frequently asked questions ========================== The following notes answer some common questions, and may be useful to you when installing, configuring or using django-contact-form. What versions of Django and Python are supported? ------------------------------------------------- ``django-contact-form`` |release| supports Django 4.2, 5.1, and 5.2, and Python 3.9 through 3.13. See `Django's Python support matrix `_ for details of which Python versions are compatible with each version of Django. What license is ``django-contact-form`` under? ---------------------------------------------- ``django-contact-form`` is offered under a three-clause BSD-style license; this is `an OSI-approved open-source license `_, and allows you a large degree of freedom in modifying and redistributing the code. For the full terms, see the file `LICENSE` which came with your copy of ``django-contact-form``; if you did not receive a copy of this file, you can view it online at . Why aren't there any default templates? --------------------------------------- Usable default templates, for a Django application designed to be widely reused, are essentially impossible to produce; variations in site design, block structure, etc. cannot be reliably accounted for. As such, ``django-contact-form`` provides bare-bones (i.e., containing no HTML structure whatsoever) templates in its source distribution to enable running tests, and otherwise just provides good documentation of all required templates and the context made available to them. Why am I getting a bunch of ``BadHeaderError`` exceptions? ---------------------------------------------------------- Most likely, you have an error in your :class:`~django_contact_form.forms.ContactForm` subclass. Specifically, one or more of :attr:`~django_contact_form.forms.ContactForm.from_email`, :attr:`~django_contact_form.forms.ContactForm.recipient_list` or :meth:`~django_contact_form.forms.ContactForm.subject` are returning values which contain newlines. As a security precaution against `email header injection attacks `_ (which allow spammers and other malicious users to manipulate email and potentially cause automated systems to send mail to unintended recipients), `Django's email-sending framework does not permit newlines in message headers `_. :exc:`~django.core.mail.BadHeaderError` is the exception Django raises when a newline is detected in a header. By default, :meth:`~django_contact_form.forms.ContactForm.subject` will forcibly condense the subject to a single line. Note that this only applies to the headers of an email message; the message body can (and usually does) contain newlines. I found a bug or want to make an improvement! --------------------------------------------- The canonical development repository for ``django-contact-form`` is online at . Issues and pull requests can both be filed there. If you'd like to contribute to ``django-contact-form``, that's great! Just please remember that pull requests should include tests and documentation for any changes made, and that following `PEP 8 `_ is mandatory. Pull requests without documentation won't be merged, and PEP 8 style violations or test coverage below 100% are both configured to break the build. django-contact-form-5.2.0/docs/forms.rst000066400000000000000000000011071477540643200201750ustar00rootroot00000000000000.. _forms: .. module:: django_contact_form.forms Contact form classes ==================== There are two contact-form classes included in django-contact-form; one provides all the infrastructure for a contact form, and will usually be the base class for subclasses which want to extend or modify functionality. The other is a subclass which adds spam filtering to the contact form. The base contact form class --------------------------- .. autoclass:: ContactForm The spam-filtering contact form class ------------------------------------- .. autoclass:: AkismetContactForm django-contact-form-5.2.0/docs/index.rst000066400000000000000000000024761477540643200201700ustar00rootroot00000000000000.. _index: django-contact-form |release| ============================= ``django-contact-form`` provides customizable contact-form functionality for `Django `_-powered Web sites. This application includes: * An extensible base contact-form class which is also usable as-is for basic functionality (collecting a name, email address, and message) * A subclass of the base form which uses the Akismet spam-filtering service to detect and reject spam submissions * A class-based Django view which can be used with either of the built-in contact form classes, or your own customized form For the default contact-form functionality, add ``"django_contact_form"`` to your Django site's ``INSTALLED_APPS`` setting, add the following line to your site's root URLConf, and create the templates specified in :ref:`the usage guide `: .. code-block:: python from django.urls import include, path urlpatterns = [ # ... other URL patterns for your site ... path("contact/", include("django_contact_form.urls")), ] .. toctree:: :caption: Installation and usage :maxdepth: 1 install usage .. toctree:: :caption: API reference :maxdepth: 1 forms views .. toctree:: :caption: Other documentation :maxdepth: 1 changelog faq django-contact-form-5.2.0/docs/install.rst000066400000000000000000000053211477540643200205170ustar00rootroot00000000000000.. _install: Installation guide ================== ``django-contact-form`` |release| supports Django 4.2, 5.1, and 5.2, and Python 3.9 through 3.13. See `Django's Python support matrix `_ for details of which Python versions are compatible with each version of Django. Installing ``django-contact-form`` ---------------------------------- To install ``django-contact-form``, run the following command from a command prompt/terminal: .. tab:: macOS/Linux/other Unix .. code-block:: shell python -m pip install django-contact-form .. tab:: Windows .. code-block:: shell py -m pip install django-contact-form If you plan to use the spam-filtering :class:`~django_contact_form.forms.AkismetContactForm`, you will also need `the Python akismet client `_. You can install this separately (in which case, be sure to install at least version 24.5.0 of ``akismet``), or you can have it automatically installed for you alongside ``django-contact-form``, by running: .. tab:: macOS/Linux/other Unix .. code-block:: shell python -m pip install "django-contact-form[akismet]" .. tab:: Windows .. code-block:: shell py -m pip install "django-contact-form[akismet]" This will use ``pip``, the standard Python package-installation tool. If you are using a supported version of Python, your installation of Python should have come with ``pip`` bundled. If ``pip`` does not appear to be present, you can try running the following from a command prompt/terminal: .. tab:: macOS/Linux/other Unix .. code-block:: shell python -m ensurepip --upgrade .. tab:: Windows .. code-block:: shell py -m ensurepip --upgrade Instructions are also available for `how to obtain and manually install or upgrade pip `_. If you don't already have a supported version of Django installed, using ``pip`` to install ``django-contact-form`` will also install the latest supported version of Django. Installing for local development -------------------------------- If you want to work on ``django-contact-form``, you can obtain a source checkout. The development repository for ``django-contact-form`` is at . If you have `git `_ installed, you can obtain a copy of the repository by typing:: git clone https://github.com/ubernostrum/django-contact-form.git Then follow the instructions in the file ``CONTRIBUTING.rst`` in the root directory of the source checkout. Next steps ---------- To start using ``django-contact-form``, check out :ref:`the usage guide `. django-contact-form-5.2.0/docs/make.bat000066400000000000000000000120021477540643200177160ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-contact-form.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-contact-form.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-contact-form-5.2.0/docs/requirements.txt000066400000000000000000000010011477540643200215720ustar00rootroot00000000000000# These dependencies are not "extras" defined in the pyproject.toml, # because they're not something you actually want to install alongside # the main package. They're also not "dev dependencies" in the PDM # sense of the term, because PDM would constrain them to the oldest # Python version the main package supports (3.9), while all our # documentation jobs, whether run via nox or via readthedocs, run on # Python 3.12. furo sphinx sphinx-copybutton sphinx-inline-tabs sphinx-notfound-page sphinxext-opengraph django-contact-form-5.2.0/docs/spelling_wordlist.txt000066400000000000000000000002551477540643200226250ustar00rootroot00000000000000Akismet Changelog LTS Pre akismet async bugfix callables changelog config customizable deprecations dev django multi online overridable spam spammers subclasses subclassing django-contact-form-5.2.0/docs/usage.rst000066400000000000000000000137371477540643200201670ustar00rootroot00000000000000.. _usage: Usage guide =========== First you'll need to have Django and ``django-contact-form`` installed. For details on that, see :ref:`the installation guide `. Once that's done, you can start setting up django-contact-form. Add ``"django_contact_form"`` to your ``INSTALLED_APPS`` setting. Then, you can begin configuring. URL configuration ----------------- The quickest way to set up the views in ``django-contact-form`` is to use the provided URLconf, found at ``django_contact_form.urls``. You can include it wherever you like in your site's URL configuration. For example, to have it live at the URL ``/contact/``: .. code-block:: python from django.urls import include, path urlpatterns = [ # ... other URL patterns for your site ... path("contact/", include("django_contact_form.urls")), ] If you'll be using a custom form class, you'll need to manually set up your URLs so you can tell ``django-contact-form`` about your form class. For example: .. code-block:: python from django.urls import include, path from django.views.generic import TemplateView from django_contact_form.views import ContactFormView from yourapp.forms import YourCustomFormClass urlpatterns = [ # ... other URL patterns for your site ... path("contact/", ContactFormView.as_view( form_class=YourCustomFormClass ), name="django_contact_form"), path("contact/sent/", TemplateView.as_view( template_name="django_contact_form/contact_form_sent.html" ), name="django_contact_form_sent"), ] .. important:: **Where to put custom forms and views** When writing a custom form class (or custom :class:`~django_contact_form.views.ContactFormView` subclass), **don't** put your custom code inside ``django-contact-form``. Instead, put your custom code in the appropriate place (a ``forms.py`` or ``views.py`` file) in a Django application you've written. .. _default-templates: Required templates ------------------ In the default configuration, ``django-contact-form`` requires the following templates to exist: ``django_contact_form/contact_form.html`` ````````````````````````````````````````` This is used to display the contact form. It has a :class:`~django.template.RequestContext` (so any context processors will be applied), and also provides the form instance as the context variable ``form``. ``django_contact_form/contact_form_sent.html`` `````````````````````````````````````````````` This is used after a successful form submission, to let the user know their message has been sent. It has a :class:`~django.template.RequestContext`, but provides no additional context variables of its own. ``django_contact_form/contact_form.txt`` ```````````````````````````````````````` Used to render the subject of the email. Will receive a :class:`~django.template.RequestContext` with the following additional variables: ``body`` The message the user supplied. ``email`` The email address the user supplied. ``name`` The name the user supplied. ``site`` The current site. Either a :class:`~django.contrib.sites.models.Site` or :class:`~django.contrib.sites.requests.RequestSite` instance, depending on whether `Django's sites framework `_ is installed). ``django_contact_form/contact_form_subject.txt`` ```````````````````````````````````````````````` Used to render the subject of the email. Will receive a :class:`~django.template.RequestContext` with the following additional variables: ``body`` The message the user supplied. ``email`` The email address the user supplied. ``name`` The name the user supplied. ``site`` The current site. Either a :class:`~django.contrib.sites.models.Site` or :class:`~django.contrib.sites.requests.RequestSite` instance, depending on whether `Django's sites framework `_ is installed). .. warning:: **Subject must be a single line** In order to prevent `header injection attacks `_, the subject *must* be only a single line of text, and Django's email framework will reject any attempt to send an email with a multi-line subject. So it's a good idea to ensure your ``contact_form_subject.txt`` template only produces a single line of output when rendered; as a precaution, however, ``django-contact-form`` will, by default, condense the output of this template to a single line. Using a spam-filtering contact form ----------------------------------- Spam filtering is a common desire for contact forms, due to the large amount of spam they can attract. There is a spam-filtering contact form class included in ``django-contact-form``: :class:`~django_contact_form.forms.AkismetContactForm`, which uses `the Akismet spam-detection service `_. To use this form, you will need to do the following things: 1. Install `the Python akismet client `_ to allow ``django-contact-form`` to communicate with the Akismet service. You can do this manually (in which case you must install at least version 24.5.0 of ``akismet``) or as you install ``django-contact-form`` by telling ``pip`` to install ``"django-contact-form[akismet]"``. 2. Obtain an Akismet API key from , and associate it with the URL of your site. 3. Supply the API key and URL for ``django-contact-form`` to use, by placing them in the environment variables ``PYTHON_AKISMET_API_KEY`` and ``PYTHON_AKISMET_BLOG_URL``. Then you can replace the suggested URLconf above with the following: .. code-block:: python from django.urls import include, path urlpatterns = [ # ... other URL patterns for your site ... path("contact/", include("django_contact_form.akismet_urls")), ] django-contact-form-5.2.0/docs/views.rst000066400000000000000000000001611477540643200202030ustar00rootroot00000000000000.. _views: .. module:: django_contact_form.views Built-in views ============== .. autoclass:: ContactFormView django-contact-form-5.2.0/noxfile.py000066400000000000000000000300561477540643200174100ustar00rootroot00000000000000""" Automated testing via nox (https://nox.thea.codes/). Combined with a working installation of nox (``pip install nox``), this file specifies a matrix of tests, linters, and other quality checks which can be run individually or as a suite. To see available tasks, run ``nox --list``. To run all available tasks -- which requires a functioning installation of at least one supported Python version -- run ``nox``. To run a single task, use ``nox -s`` with the name of that task. """ # SPDX-License-Identifier: BSD-3-Clause import os import pathlib import shutil import typing import nox nox.options.default_venv_backend = "venv" nox.options.reuse_existing_virtualenvs = True IS_CI = bool(os.getenv("CI", False)) PACKAGE_NAME = "django_contact_form" NOXFILE_PATH = pathlib.Path(__file__).parents[0] ARTIFACT_PATHS = ( NOXFILE_PATH / "src" / f"{PACKAGE_NAME}.egg-info", NOXFILE_PATH / "build", NOXFILE_PATH / "dist", NOXFILE_PATH / "__pycache__", NOXFILE_PATH / "src" / "__pycache__", NOXFILE_PATH / "src" / PACKAGE_NAME / "__pycache__", NOXFILE_PATH / "tests" / "__pycache__", ) def clean(paths: typing.Iterable[pathlib.Path] = ARTIFACT_PATHS) -> None: """ Clean up after a test run. """ # This cleanup is only useful for the working directory of a local checkout; in CI # we don't need it because CI environments are ephemeral anyway. if IS_CI: return [ shutil.rmtree(path) if path.is_dir() else path.unlink() for path in paths if path.exists() ] # Tasks which run the package's test suites. # ----------------------------------------------------------------------------------- @nox.session(tags=["tests"]) @nox.parametrize( "python,django", [ # Python/Django testing matrix. Tests Django 4.2, 5.1, 5.2 on Python 3.9 through # 3.13, skipping unsupported combinations. (python, django) for python in ["3.9", "3.10", "3.11", "3.12", "3.13"] for django in ["4.2", "5.1", "5.2"] if (python, django) not in [("3.9", "5.1"), ("3.9", "5.2"), ("3.13", "4.2")] ], ) def tests_with_coverage(session: nox.Session, django: str) -> None: """ Run the package's unit tests, with coverage report. """ session.install( f"Django~={django}.0", ".[tests,akismet]", "coverage", 'tomli; python_full_version < "3.11.0a7"', ) python_version = session.run( f"{session.bin}/python{session.python}", "--version", silent=True ).strip() django_version = session.run( f"{session.bin}/python{session.python}", "-Im", "django", "--version", silent=True, ).strip() session.log(f"Running tests with {python_version}/Django {django_version}") session.run(f"{session.bin}/python{session.python}", "-Im", "coverage", "--version") session.run( f"{session.bin}/python{session.python}", "-Wonce::DeprecationWarning", "-m", "coverage", "run", "--source", PACKAGE_NAME, "runtests.py", env={"DJANGO_SETTINGS_MODULE": "test_settings"}, ) clean() @nox.session(python=["3.13"], tags=["tests"]) def coverage_report(session: nox.Session) -> None: """ Combine coverage from the various test runs and output the report. """ # In CI this job does not run because we substitute one that integrates with the CI # system. if IS_CI: session.skip( "Running in CI -- skipping nox coverage job in favor of CI coverage job" ) session.install("coverage[toml]") session.run(f"python{session.python}", "-Im", "coverage", "combine") session.run( f"python{session.python}", "-Im", "coverage", "report", "--show-missing" ) session.run(f"python{session.python}", "-Im", "coverage", "erase") # Tasks which test the package's documentation. # ----------------------------------------------------------------------------------- # The documentation jobs ordinarily would want to use the latest Python version, but # currently that's 3.13 and Read The Docs doesn't yet support it. So to ensure the # documentation jobs are as closely matched to what would happen on RTD, these jobs stay # on 3.12 for now. @nox.session(python=["3.12"], tags=["docs"]) def docs_build(session: nox.Session) -> None: """ Build the package's documentation as HTML. """ session.install(".", "-r", "docs/requirements.txt") build_dir = session.create_tmp() session.run( f"{session.bin}/python{session.python}", "-Im", "sphinx", "--builder", "html", "--write-all", "-c", "docs/", "--doctree-dir", f"{build_dir}/doctrees", "docs/", f"{build_dir}/html", ) clean() @nox.session(python=["3.12"], tags=["docs"]) def docs_docstrings(session: nox.Session) -> None: """ Enforce the presence of docstrings on all modules, classes, functions, and methods. """ session.install("interrogate") session.run( f"{session.bin}/python{session.python}", "-Im", "interrogate", "--version" ) session.run( f"{session.bin}/python{session.python}", "-Im", "interrogate", "-v", "src/", "tests/", "noxfile.py", ) clean() @nox.session(python=["3.12"], tags=["docs"]) def docs_spellcheck(session: nox.Session) -> None: """ Spell-check the package's documentation. """ session.install(".", "-r", "docs/requirements.txt") session.install("pyenchant", "sphinxcontrib-spelling") build_dir = session.create_tmp() session.run( f"{session.bin}/python{session.python}", "-Im", "sphinx", "-W", # Promote warnings to errors, so that misspelled words fail the build. "--builder", "spelling", "-c", "docs/", "--doctree-dir", f"{build_dir}/doctrees", "docs/", f"{build_dir}/html", # On Apple Silicon Macs, this environment variable needs to be set so # pyenchant can find the "enchant" C library. See # https://github.com/pyenchant/pyenchant/issues/265#issuecomment-1126415843 env={"PYENCHANT_LIBRARY_PATH": os.getenv("PYENCHANT_LIBRARY_PATH", "")}, ) clean() # Code formatting checks. # # These checks do *not* reformat code -- that happens in pre-commit hooks -- but will # fail a CI build if they find any code that needs reformatting. # ----------------------------------------------------------------------------------- @nox.session(python=["3.13"], tags=["formatters"]) def format_black(session: nox.Session) -> None: """ Check code formatting with Black. """ session.install("black") session.run(f"{session.bin}/python{session.python}", "-Im", "black", "--version") session.run( f"{session.bin}/python{session.python}", "-Im", "black", "--check", "--diff", "src/", "tests/", "docs/", "noxfile.py", ) clean() @nox.session(python=["3.13"], tags=["formatters"]) def format_isort(session: nox.Session) -> None: """ Check code formating with Black. """ session.install("isort") session.run(f"{session.bin}/python{session.python}", "-Im", "isort", "--version") session.run( f"{session.bin}/python{session.python}", "-Im", "isort", "--check-only", "--diff", "src/", "tests/", "docs/", "noxfile.py", ) clean() # Linters. # ----------------------------------------------------------------------------------- @nox.session(python=["3.13"], tags=["linters", "security"]) def lint_bandit(session: nox.Session) -> None: """ Lint code with the Bandit security analyzer. """ session.install("bandit[toml]") session.run(f"{session.bin}/python{session.python}", "-Im", "bandit", "--version") session.run( f"{session.bin}/python{session.python}", "-Im", "bandit", "-c", "./pyproject.toml", "-r", "src/", "tests/", ) clean() @nox.session(python=["3.13"], tags=["linters"]) def lint_flake8(session: nox.Session) -> None: """ Lint code with flake8. """ session.install("flake8", "flake8-bugbear") session.run(f"{session.bin}/python{session.python}", "-Im", "flake8", "--version") session.run( f"{session.bin}/python{session.python}", "-Im", "flake8", "src/", "tests/", "docs/", "noxfile.py", ) clean() @nox.session(python=["3.13"], tags=["linters"]) def lint_pylint(session: nox.Session) -> None: """ Lint code with Pylint. """ # Pylint requires that all dependencies be importable during the run, so unlike # other lint tasks we just install the package. session.install("pylint", "pylint-django", "akismet>=24.5.0", ".[tests]") session.run(f"python{session.python}", "-Im", "pylint", "--version") session.run(f"python{session.python}", "-Im", "pylint", "src/", "tests/") clean() # Packaging checks. # ----------------------------------------------------------------------------------- @nox.session(python=["3.13"], tags=["packaging"]) def package_build(session: nox.Session) -> None: """ Check that the package builds. """ clean() session.install("build") session.run(f"{session.bin}/python{session.python}", "-Im", "build", "--version") session.run(f"{session.bin}/python{session.python}", "-Im", "build") @nox.session(python=["3.13"], tags=["packaging"]) def package_description(session: nox.Session) -> None: """ Check that the package description will render on the Python Package Index. """ package_dir = session.create_tmp() session.install("build", "twine") session.run(f"{session.bin}/python{session.python}", "-Im", "build", "--version") session.run(f"{session.bin}/python{session.python}", "-Im", "twine", "--version") session.run( f"{session.bin}/python{session.python}", "-Im", "build", "--wheel", "--outdir", f"{package_dir}/build", ) session.run( f"{session.bin}/python{session.python}", "-Im", "twine", "check", f"{package_dir}/build/*", ) clean() @nox.session(python=["3.13"], tags=["packaging"]) def package_manifest(session: nox.Session) -> None: """ Check that the set of files in the package matches the set under version control. """ if IS_CI: session.skip("check-manifest already run by earlier CI steps.") session.install("check-manifest") session.run( f"{session.bin}/python{session.python}", "-Im", "check_manifest", "--version" ) session.run( f"{session.bin}/python{session.python}", "-Im", "check_manifest", "--verbose" ) clean() @nox.session(python=["3.13"], tags=["packaging"]) def package_pyroma(session: nox.Session) -> None: """ Check package quality with pyroma. """ session.install("pyroma") session.run( f"{session.bin}/python{session.python}", "-c", 'from importlib.metadata import version; print(version("pyroma"))', ) session.run(f"{session.bin}/python{session.python}", "-Im", "pyroma", ".") clean() @nox.session(python=["3.13"], tags=["packaging"]) def package_wheel(session: nox.Session) -> None: """ Check the built wheel package for common errors. """ package_dir = session.create_tmp() session.install("build", "check-wheel-contents") session.run(f"{session.bin}/python{session.python}", "-Im", "build", "--version") session.run( f"{session.bin}/python{session.python}", "-Im", "check_wheel_contents", "--version", ) session.run( f"{session.bin}/python{session.python}", "-Im", "build", "--wheel", "--outdir", f"{package_dir}/build", ) session.run( f"{session.bin}/python{session.python}", "-Im", "check_wheel_contents", f"{package_dir}/build", ) clean() django-contact-form-5.2.0/pdm.lock000066400000000000000000000302261477540643200170230ustar00rootroot00000000000000# This file is @generated by PDM. # It is not intended for manual editing. [metadata] groups = ["default", "akismet", "tests"] strategy = ["inherit_metadata"] lock_version = "4.5.0" content_hash = "sha256:57c8245b627669fb907bfd09a56c5aeae244c52ee94a3b5a91bbbc5c086697d6" [[metadata.targets]] requires_python = ">=3.9,<3.10" [[metadata.targets]] requires_python = ">=3.10" [[package]] name = "akismet" version = "24.5.1" requires_python = ">=3.8" summary = "A Python interface to the Akismet spam-filtering service." groups = ["akismet"] dependencies = [ "httpx", ] files = [ {file = "akismet-24.5.1-py3-none-any.whl", hash = "sha256:92c345d61a249be74ef782217276ea752cd1dff274080bdbaf1bd48609111a92"}, {file = "akismet-24.5.1.tar.gz", hash = "sha256:7210f20e5a732ccf91e51424e49c9ec500b285ae920ad510cd3331e8309b6359"}, ] [[package]] name = "anyio" version = "4.6.2.post1" requires_python = ">=3.9" summary = "High level compatibility layer for multiple asynchronous event loop implementations" groups = ["akismet"] dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", "sniffio>=1.1", "typing-extensions>=4.1; python_version < \"3.11\"", ] files = [ {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [[package]] name = "argcomplete" version = "3.5.1" requires_python = ">=3.8" summary = "Bash tab completion for argparse" groups = ["tests"] files = [ {file = "argcomplete-3.5.1-py3-none-any.whl", hash = "sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363"}, {file = "argcomplete-3.5.1.tar.gz", hash = "sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4"}, ] [[package]] name = "asgiref" version = "3.8.1" requires_python = ">=3.8" summary = "ASGI specs, helper code, and adapters" groups = ["default"] dependencies = [ "typing-extensions>=4; python_version < \"3.11\"", ] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [[package]] name = "certifi" version = "2024.8.30" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["akismet"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "colorama" version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["tests"] marker = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "colorlog" version = "6.9.0" requires_python = ">=3.6" summary = "Add colours to the output of Python's logging module." groups = ["tests"] dependencies = [ "colorama; sys_platform == \"win32\"", ] files = [ {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, ] [[package]] name = "distlib" version = "0.3.9" summary = "Distribution utilities" groups = ["tests"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] name = "django" version = "4.2.16" requires_python = ">=3.8" summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." groups = ["default"] marker = "python_version < \"3.10\" and python_version >= \"3.9\"" dependencies = [ "asgiref<4,>=3.6.0", "backports-zoneinfo; python_version < \"3.9\"", "sqlparse>=0.3.1", "tzdata; sys_platform == \"win32\"", ] files = [ {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, ] [[package]] name = "django" version = "5.1.3" requires_python = ">=3.10" summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." groups = ["default"] marker = "python_version >= \"3.10\"" dependencies = [ "asgiref<4,>=3.8.1", "sqlparse>=0.3.1", "tzdata; sys_platform == \"win32\"", ] files = [ {file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"}, {file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"}, ] [[package]] name = "exceptiongroup" version = "1.2.2" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" groups = ["akismet"] marker = "python_version < \"3.11\" and python_version >= \"3.9\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [[package]] name = "filelock" version = "3.16.1" requires_python = ">=3.8" summary = "A platform independent file lock." groups = ["tests"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [[package]] name = "h11" version = "0.14.0" requires_python = ">=3.7" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" groups = ["akismet"] dependencies = [ "typing-extensions; python_version < \"3.8\"", ] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] [[package]] name = "httpcore" version = "1.0.6" requires_python = ">=3.8" summary = "A minimal low-level HTTP client." groups = ["akismet"] dependencies = [ "certifi", "h11<0.15,>=0.13", ] files = [ {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, ] [[package]] name = "httpx" version = "0.27.2" requires_python = ">=3.8" summary = "The next generation HTTP client." groups = ["akismet"] dependencies = [ "anyio", "certifi", "httpcore==1.*", "idna", "sniffio", ] files = [ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [[package]] name = "idna" version = "3.10" requires_python = ">=3.6" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["akismet"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [[package]] name = "nox" version = "2024.10.9" requires_python = ">=3.8" summary = "Flexible test automation." groups = ["tests"] dependencies = [ "argcomplete<4,>=1.9.4", "colorlog<7,>=2.6.1", "packaging>=20.9", "tomli>=1; python_version < \"3.11\"", "virtualenv>=20.14.1", ] files = [ {file = "nox-2024.10.9-py3-none-any.whl", hash = "sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab"}, {file = "nox-2024.10.9.tar.gz", hash = "sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95"}, ] [[package]] name = "packaging" version = "24.2" requires_python = ">=3.8" summary = "Core utilities for Python packages" groups = ["tests"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "platformdirs" version = "4.3.6" requires_python = ">=3.8" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." groups = ["tests"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [[package]] name = "sniffio" version = "1.3.1" requires_python = ">=3.7" summary = "Sniff out which async library your code is running under" groups = ["akismet"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlparse" version = "0.5.1" requires_python = ">=3.8" summary = "A non-validating SQL parser." groups = ["default"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, ] [[package]] name = "tomli" version = "2.0.2" requires_python = ">=3.8" summary = "A lil' TOML parser" groups = ["tests"] marker = "python_version < \"3.11\" and python_version >= \"3.9\"" files = [ {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] name = "typing-extensions" version = "4.12.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" groups = ["default", "akismet"] marker = "python_version < \"3.11\" and python_version >= \"3.9\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "tzdata" version = "2024.2" requires_python = ">=2" summary = "Provider of IANA time zone data" groups = ["default"] marker = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] name = "virtualenv" version = "20.27.1" requires_python = ">=3.8" summary = "Virtual Python Environment builder" groups = ["tests"] dependencies = [ "distlib<1,>=0.3.7", "filelock<4,>=3.12.2", "importlib-metadata>=6.6; python_version < \"3.8\"", "platformdirs<5,>=3.9.1", ] files = [ {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] django-contact-form-5.2.0/pyproject.toml000066400000000000000000000044601477540643200203060ustar00rootroot00000000000000[build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [project] authors = [ {name = "James Bennett"}, ] maintainers = [ {name = "James Bennett"} ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Utilities", ] name = "django-contact-form" description = "A generic contact-form application for Django." dependencies = [ "Django>=4.2,!=5.0.*", ] keywords = ["django", "email", "contact-form"] license = {text = "BSD-3-Clause"} readme = "README.rst" requires-python = ">=3.9" version = "5.2.0" [project.urls] "Documentation" = "https://django-contact-form.readthedocs.io/" "Source Code" = "https://github.com/ubernostrum/django-contact-form" [project.optional-dependencies] akismet = [ "akismet>=24.5.0", ] [dependency-groups] tests = ["nox"] [tool.bandit] skips = ["B101"] [tool.black] target-version = ["py39", "py310", "py311", "py312", "py313"] [tool.check-manifest] ignore-bad-ideas = ["*.mo"] [tool.check-wheel-contents] ignore = ["W004"] [tool.coverage.paths] source = ["src", ".nox/tests_with_coverage*/**/site-packages"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __str__", "raise NotImplementedError", ] fail_under = 100 [tool.coverage.run] branch = true parallel = true source = ["django-contact-form"] [tool.interrogate] fail-under = 100 ignore-init-method = true ignore-init-module = true ignore-magic = true [tool.isort] profile = "black" [tool.pdm] distribution = true [tool.pdm.build] source-includes = [ ".editorconfig", ".flake8", ".pre-commit-config.yaml", ".readthedocs.yaml", "AUTHORS", "CONTRIBUTING.rst", "docs/", "noxfile.py", "pdm.lock", "runtests.py", "tests/", ] [tool.pylint] disable = ["duplicate-code"] django-contact-form-5.2.0/runtests.py000066400000000000000000000010431477540643200176250ustar00rootroot00000000000000""" A standalone test runner script, since we don't ship a Django manage.py file. """ # SPDX-License-Identifier: BSD-3-Clause import os import sys import django from django.conf import settings from django.test.utils import get_runner if __name__ == "__main__": sys.path.append("tests") os.environ["DJANGO_SETTINGS_MODULE"] = "test_settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner(exclude_tags=["end-to-end"]) failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) django-contact-form-5.2.0/src/000077500000000000000000000000001477540643200161555ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/000077500000000000000000000000001477540643200221555ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/__init__.py000066400000000000000000000001411477540643200242620ustar00rootroot00000000000000""" A generic contact-form application for Django. """ # SPDX-License-Identifier: BSD-3-Clause django-contact-form-5.2.0/src/django_contact_form/_akismet.py000066400000000000000000000056661477540643200243400ustar00rootroot00000000000000""" Helper code for obtaining an Akismet API client. """ # SPDX-License-Identifier: BSD-3-Clause import textwrap import warnings import akismet from django.conf import settings from django.core.exceptions import ImproperlyConfigured _AKISMET_CLIENT = None def _client_from_settings(client_class): """ Attempt to obtain an Akismet client from legacy configuration in Django settings. """ key = getattr(settings, "AKISMET_API_KEY", None) # noqa: B009 url = getattr(settings, "AKISMET_BLOG_URL", None) # noqa: B009 if not all([key, url]): return None warnings.warn( textwrap.dedent( """ Specifying Akismet configuration via the Django settings AKISMET_API_KEY and AKISMET_BLOG_URL is deprecated and support for it will be removed in a future version of django-contact-form. Please migrate to specifying the configuration in the environment variables PYTHON_AKISMET_API_KEY and PYTHON_AKISMET_BLOG_URL. Or, if you cannot configure via environment variables, write a subclass of `AkismetContactForm` and override its `get_akismet_client()` method to construct your Akismet client. """ ), DeprecationWarning, stacklevel=2, ) client = client_class(config=akismet.Config(key, url)) if client.verify_key(key, url): return client raise ImproperlyConfigured( "The Akismet configuration specified in your Django settings is invalid." ) def _client_from_environment(client_class): """ Attempt to obtain an Akismet client from configuration in environment variables. """ try: return client_class.validated_client() except akismet.ConfigurationError as exc: raise ImproperlyConfigured( "The Akismet configuration specified in your environment variables is " "missing or invalid." ) from exc def _try_get_akismet_client( # pylint: disable=inconsistent-return-statements client_class=akismet.SyncClient, ): """ Attempt to obtain and return an instance of the given Akismet API client class.. :raises django.core.exceptions.ImproperlyConfigured: When the Akismet client configuration is missing or invalid. """ global _AKISMET_CLIENT # pylint: disable=global-statement if _AKISMET_CLIENT is None: # pragma: no branch for attempt in [ # pragma: no branch _client_from_settings, _client_from_environment, ]: _AKISMET_CLIENT = attempt(client_class) if _AKISMET_CLIENT is not None: return _AKISMET_CLIENT return _AKISMET_CLIENT def _clear_cached_instance(): """ Clear the cached Akismet API client instance, so that it will be re-created the next time it is requested. """ global _AKISMET_CLIENT # pylint: disable=global-statement _AKISMET_CLIENT = None django-contact-form-5.2.0/src/django_contact_form/akismet_urls.py000066400000000000000000000013551477540643200252350ustar00rootroot00000000000000""" Example URLConf for a contact form with Akismet spam filtering. If all you want is the basic contact-form plus spam filtering, include this URLConf somewhere in your URL hierarchy (for example, at ``/contact/``). """ # SPDX-License-Identifier: BSD-3-Clause from django.urls import path from django.views.generic import TemplateView from .forms import AkismetContactForm from .views import ContactFormView urlpatterns = [ path( "", ContactFormView.as_view(form_class=AkismetContactForm), name="django_contact_form", ), path( "sent/", TemplateView.as_view( template_name="django_contact_form/contact_form_sent.html" ), name="django_contact_form_sent", ), ] django-contact-form-5.2.0/src/django_contact_form/forms.py000066400000000000000000000317241477540643200236640ustar00rootroot00000000000000""" A base contact form for allowing users to send email messages through a web interface. """ # SPDX-License-Identifier: BSD-3-Clause from django import forms from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from django.core.mail import send_mail from django.template import loader from django.utils.translation import gettext_lazy as _ class ContactForm(forms.Form): """ The base contact form class from which all contact form classes should inherit. If you don't need any customization, you can use this form to provide basic contact-form functionality; it will collect name, email address and message. The :class:`~django_contact_form.views.ContactFormView` included in this application knows how to work with this form and can handle many types of subclasses as well (see below for a discussion of the important points), so in many cases it will be all that you need. If you'd like to use this form or a subclass of it from one of your own views, here's how: 1. When you instantiate the form, pass the current :class:`~django.http.HttpRequest` object as the keyword argument ``request``; this is used internally by the base implementation, and also made available so that subclasses can add functionality which relies on inspecting the request (such as spam filtering). 2. To send the message, call the form's :meth:`save` method, which accepts the keyword argument ``fail_silently`` and defaults it to :data:`False`. This argument is passed directly to Django's :func:`~django.core.mail.send_mail` function, and allows you to suppress or raise exceptions as needed for debugging. The :meth:`save` method has no return value. Other than that, treat it like any other form; validity checks and validated data are handled normally, through the :meth:`~django.forms.Form.is_valid` method and the :attr:`~django.forms.Form.cleaned_data` dictionary. Under the hood, this form uses a somewhat abstracted interface in order to make it easier to subclass and add functionality. Customizing behavior in subclasses `````````````````````````````````` The following attributes play a role in determining behavior, and any of them can be implemented as an attribute or as a method (for example, if you wish to have :attr:`from_email` be dynamic, you can implement a method named :meth:`from_email` instead of setting the attribute :attr:`from_email`). .. attribute:: from_email The email address (:class:`str`) to use in the ``From:`` header of the message. By default, this is the value of the Django setting ```DEFAULT_FROM_EMAIL``. .. attribute:: recipient_list A :class:`list` of recipients for the message. By default, this is the email addresses specified in the Django setting ``MANAGERS``. .. attribute:: subject_template_name A :class:`str`, the name of the template to use when rendering the subject line of the message. By default, this is ``"django_contact_form/contact_form_subject.txt"``. .. attribute:: template_name A :class:`str`, the name of the template to use when rendering the body of the message. By default, this is ``"django_contact_form/contact_form.txt"``. And two methods are involved in producing the contents of the message to send: .. automethod:: message .. automethod:: subject Finally, the message itself is generated by the following two methods: .. automethod:: get_message_dict .. automethod:: get_message_context Other attributes/methods ```````````````````````` Meanwhile, the following attributes/methods generally should not be overridden; doing so may interfere with functionality, may not accomplish what you want, and generally any desired customization can be accomplished in a more straightforward way through overriding one of the attributes/methods listed above. .. attribute:: request The :class:`~django.http.HttpRequest` object representing the current request. This is set automatically in ``__init__()``, and is used both to generate a :class:`~django.template.RequestContext` for the templates and to allow subclasses to engage in request-specific behavior. .. automethod:: save Note that subclasses which override ``__init__`` or :meth:`save` need to accept ``*args`` and ``**kwargs``, and pass them via :func:`super`, in order to preserve behavior (each of those methods accepts at least one additional argument, and this application expects and requires them to do so). """ name = forms.CharField(max_length=100, label=_("Your name")) email = forms.EmailField(max_length=200, label=_("Your email address")) body = forms.CharField(widget=forms.Textarea, label=_("Your message")) from_email = settings.DEFAULT_FROM_EMAIL recipient_list = [mail_tuple[1] for mail_tuple in settings.MANAGERS] subject_template_name = "django_contact_form/contact_form_subject.txt" template_name = "django_contact_form/contact_form.txt" def __init__( self, *args, data=None, files=None, request=None, recipient_list=None, **kwargs ): if request is None: raise TypeError("Keyword argument 'request' must be supplied") self.request = request if recipient_list is not None: self.recipient_list = recipient_list super().__init__(data=data, files=files, *args, **kwargs) # noqa: B026 def message(self) -> str: """ Return the body of the message to send. By default, this is accomplished by rendering the template name specified in :attr:`template_name`. """ template_name = ( self.template_name() # pylint: disable=not-callable if callable(self.template_name) else self.template_name ) return loader.render_to_string( template_name, self.get_message_context(), request=self.request ) def subject(self) -> str: """ Return the subject line of the message to send. By default, this is accomplished by rendering the template name specified in :attr:`subject_template_name`. .. warning:: **Subject must be a single line** The subject of an email is sent in a header (named ``Subject:``). Because email uses newlines as a separator between headers, newlines in the subject can cause it to be interpreted as multiple headers; this is the `header injection attack `_. To prevent this, :meth:`subject` will always force the subject to a single line of text, stripping all newline characters. If you override :meth:`subject`, be sure to either do this manually, or use :class:`super` to call the parent implementation. """ template_name = ( self.subject_template_name() # pylint: disable=not-callable if callable(self.subject_template_name) else self.subject_template_name ) subject = loader.render_to_string( template_name, self.get_message_context(), request=self.request ) return "".join(subject.splitlines()) def get_message_context(self) -> dict: """ Return the context used to render the templates for the email subject and body. The default context will be a :class:`~django.template.RequestContext` (using the current HTTP request, so user information is available), plus the contents of the form's :attr:`~django.forms.Form.cleaned_data` dictionary, and one additional variable: ``site`` If ``django.contrib.sites`` is installed, the currently-active :class:`~django.contrib.sites.models.Site` object. Otherwise, a :class:`~django.contrib.sites.requests.RequestSite` object generated from the request. """ if not self.is_valid(): raise ValueError("Cannot generate Context from invalid contact form") return dict(self.cleaned_data, site=get_current_site(self.request)) def get_message_dict(self) -> dict: """ Generate the parts of the message and return them in a dictionary suitable for passing as keyword arguments to Django's :func:`~django.core.mail.send_mail`. By default, this method will collect and return :attr:`from_email`, :attr:`recipient_list`, :meth:`message` and :meth:`subject`. Overriding this allows essentially unlimited customization of how the message is generated. Note that for compatibility, implementations which override this should support callables for the values of :attr:`from_email` and :attr:`recipient_list`. """ if not self.is_valid(): raise ValueError("Message cannot be sent from invalid contact form") message_dict = {} for message_part in ("from_email", "message", "recipient_list", "subject"): attr = getattr(self, message_part) message_dict[message_part] = attr() if callable(attr) else attr return message_dict def save(self, fail_silently=False): """ If the form has data and is valid, construct and send the email. By default, this is done by obtaining the parts of the email from :meth:`get_message_dict` and passing the result to Django's :func:`~django.core.mail.send_mail` function. """ send_mail(fail_silently=fail_silently, **self.get_message_dict()) class AkismetContactForm(ContactForm): """ A subclass of :class:`ContactForm` which adds spam filtering, via `the Akismet spam-detection service `_. Use of this class requires you to provide configuration for the Akismet web service. You'll need to obtain an Akismet API key, and you'll need to associate it with the site you'll use the contact form on. You can do this at . Once you have, put your Akismet API key in the environment variable ``PYTHON_AKISMET_API_KEY``, and the URL it's associated with in the environment variable ``PYTHON_AKISMET_BLOG_URL``. You will also need `the Python Akismet module `_ to communicate with the Akismet web service. You can install it manually (if you do, install at least version 24.5.0), or ``django-contact-form`` can install it automatically for you if you tell ``pip`` to install ``"django-contact-form[akismet]"``. Once you have an Akismet API key and URL configured, and the ``akismet`` module installed, you can drop in :class:`AkismetContactForm` anywhere you would have used :class:`ContactForm`. A URLconf is also provided in django-contact-form, at ``django_contact_form.akismet_urls``, which will set up :class:`AkismetContactForm` for you in place of the base contact form class. If you want to customize the spam-filtering behavior, there are two methods you can override: .. automethod:: get_akismet_check_arguments .. automethod:: get_akismet_client """ SPAM_MESSAGE = _("Your message was classified as spam.") def get_akismet_check_arguments(self) -> dict: """ Return the arguments which will be passed to the Akismet spam check. If your form contains additional fields which need to have their contents passed to Akismet, override this to ensure those arguments are correctly set. """ return { "user_ip": self.request.META["REMOTE_ADDR"], "user_agent": self.request.META.get("HTTP_USER_AGENT"), "comment_author": self.cleaned_data.get("name"), "comment_author_email": self.cleaned_data.get("email"), "comment_content": self.cleaned_data["body"], "comment_type": "contact-form", } def get_akismet_client(self) -> "akismet.SyncClient": # noqa: F821 """ Obtain and return an Akismet API client. By default, this will create a single API client instance and keep it resident in memory for the life of the Python process. If you need to customize the Akismet client creation (for example, to pass custom arguments to the Akismet API client), override this method. *Note:* Only synchronous Akismet clients (:class:`akismet.SyncClient`) are supported here; async clients (:class:`akismet.AsyncClient`) are not. """ from ._akismet import ( # pylint: disable=import-outside-toplevel _try_get_akismet_client, ) return _try_get_akismet_client() def clean_body(self): """ Apply Akismet spam filtering to the submission. """ akismet_client = self.get_akismet_client() if akismet_client.comment_check(**self.get_akismet_check_arguments()): raise forms.ValidationError(self.SPAM_MESSAGE) return self.cleaned_data["body"] django-contact-form-5.2.0/src/django_contact_form/locale/000077500000000000000000000000001477540643200234145ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/da/000077500000000000000000000000001477540643200240005ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/da/LC_MESSAGES/000077500000000000000000000000001477540643200255655ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/da/LC_MESSAGES/django.mo000066400000000000000000000012271477540643200273660ustar00rootroot00000000000000Dl $ rK \&gYour email addressYour messageYour message was classified as spam.Your nameProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2020-01-23 18:00+0050 Last-Translator: b'Anonymous User ' Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Translated-Using: django-rosetta 0.9.0 Din emailadresseDin beskedDin besked var klassifiseret som spam.Dit navndjango-contact-form-5.2.0/src/django_contact_form/locale/da/LC_MESSAGES/django.po000066400000000000000000000017731477540643200273770ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-01-23 18:03+0100\n" "PO-Revision-Date: 2020-01-23 18:00+0050\n" "Last-Translator: b'Anonymous User '\n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Translated-Using: django-rosetta 0.9.0\n" #: contact_form/forms.py:27 msgid "Your name" msgstr "Dit navn" #: contact_form/forms.py:28 msgid "Your email address" msgstr "Din emailadresse" #: contact_form/forms.py:29 msgid "Your message" msgstr "Din besked" #: contact_form/forms.py:148 msgid "Your message was classified as spam." msgstr "Din besked var klassifiseret som spam." django-contact-form-5.2.0/src/django_contact_form/locale/de/000077500000000000000000000000001477540643200240045ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/de/LC_MESSAGES/000077500000000000000000000000001477540643200255715ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/de/LC_MESSAGES/django.mo000066400000000000000000000011301477540643200273630ustar00rootroot00000000000000Dl $ "-  NYour email addressYour messageYour message was classified as spam.Your nameProject-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2016-09-20 08:30+0200 Last-Translator: Language-Team: Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.8.8 Deine E-Mail-AdresseDeine NachrichtDeine Nachricht wurde als Spam klassifiziert.Dein Namedjango-contact-form-5.2.0/src/django_contact_form/locale/de/LC_MESSAGES/django.po000066400000000000000000000015771477540643200274050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-09 22:34-0700\n" "PO-Revision-Date: 2016-09-20 08:30+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.8\n" #: forms.py:21 msgid "Your name" msgstr "Dein Name" #: forms.py:23 msgid "Your email address" msgstr "Deine E-Mail-Adresse" #: forms.py:25 msgid "Your message" msgstr "Deine Nachricht" #: forms.py:140 msgid "Your message was classified as spam." msgstr "Deine Nachricht wurde als Spam klassifiziert." django-contact-form-5.2.0/src/django_contact_form/locale/en/000077500000000000000000000000001477540643200240165ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/en/LC_MESSAGES/000077500000000000000000000000001477540643200256035ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/en/LC_MESSAGES/django.mo000066400000000000000000000005721477540643200274060ustar00rootroot00000000000000$,8@9Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-09-09 22:34-0700 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit django-contact-form-5.2.0/src/django_contact_form/locale/en/LC_MESSAGES/django.po000066400000000000000000000014341477540643200274070ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-09 22:34-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: forms.py:21 msgid "Your name" msgstr "" #: forms.py:23 msgid "Your email address" msgstr "" #: forms.py:25 msgid "Your message" msgstr "" #: forms.py:140 msgid "Your message was classified as spam." msgstr "" django-contact-form-5.2.0/src/django_contact_form/locale/es/000077500000000000000000000000001477540643200240235ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/es/LC_MESSAGES/000077500000000000000000000000001477540643200256105ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/es/LC_MESSAGES/django.mo000066400000000000000000000010601477540643200274040ustar00rootroot00000000000000<\pq v  &Your email addressYour messageYour nameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-09-09 22:34-0700 PO-Revision-Date: 2016-04-13 09:34+0200 Last-Translator: Urtzi Odriozola Language-Team: Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.8.4 Tu emailTu mensajeTu nombredjango-contact-form-5.2.0/src/django_contact_form/locale/es/LC_MESSAGES/django.po000066400000000000000000000015731477540643200274200ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Urtzi Odriozola , 2016. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-09 22:34-0700\n" "PO-Revision-Date: 2016-04-13 09:34+0200\n" "Last-Translator: Urtzi Odriozola \n" "Language-Team: \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.4\n" #: forms.py:21 msgid "Your name" msgstr "Tu nombre" #: forms.py:23 msgid "Your email address" msgstr "Tu email" #: forms.py:25 msgid "Your message" msgstr "Tu mensaje" #: forms.py:140 msgid "Your message was classified as spam." msgstr "" django-contact-form-5.2.0/src/django_contact_form/locale/eu/000077500000000000000000000000001477540643200240255ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/eu/LC_MESSAGES/000077500000000000000000000000001477540643200256125ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/eu/LC_MESSAGES/django.mo000066400000000000000000000010641477540643200274120ustar00rootroot00000000000000<\pq v   )Your email addressYour messageYour nameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-09-09 22:34-0700 PO-Revision-Date: 2016-04-13 09:32+0200 Last-Translator: Urtzi Odriozola Language-Team: Language: eu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.8.4 Zure epostaZure mezuaZure izenadjango-contact-form-5.2.0/src/django_contact_form/locale/eu/LC_MESSAGES/django.po000066400000000000000000000015771477540643200274260ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Urtzi Odriozola , 2016. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-09 22:34-0700\n" "PO-Revision-Date: 2016-04-13 09:32+0200\n" "Last-Translator: Urtzi Odriozola \n" "Language-Team: \n" "Language: eu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.4\n" #: forms.py:21 msgid "Your name" msgstr "Zure izena" #: forms.py:23 msgid "Your email address" msgstr "Zure eposta" #: forms.py:25 msgid "Your message" msgstr "Zure mezua" #: forms.py:140 msgid "Your message was classified as spam." msgstr "" django-contact-form-5.2.0/src/django_contact_form/locale/fr/000077500000000000000000000000001477540643200240235ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/fr/LC_MESSAGES/000077500000000000000000000000001477540643200256105ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000010221477540643200274020ustar00rootroot00000000000000<\pq J  Your email addressYour messageYour nameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-09-09 22:34-0700 PO-Revision-Date: 2016-09-20 08:29+0200 Last-Translator: Language-Team: Language: fr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); X-Generator: Poedit 1.8.8 Votre adresse emailVotre messageVotre nomdjango-contact-form-5.2.0/src/django_contact_form/locale/fr/LC_MESSAGES/django.po000066400000000000000000000015671477540643200274230ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-09 22:34-0700\n" "PO-Revision-Date: 2016-09-20 08:29+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 1.8.8\n" #: forms.py:21 msgid "Your name" msgstr "Votre nom" #: forms.py:23 msgid "Your email address" msgstr "Votre adresse email" #: forms.py:25 msgid "Your message" msgstr "Votre message" #: forms.py:140 msgid "Your message was classified as spam." msgstr "Votre message a été classé comme spam." django-contact-form-5.2.0/src/django_contact_form/locale/it/000077500000000000000000000000001477540643200240305ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/it/LC_MESSAGES/000077500000000000000000000000001477540643200256155ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/it/LC_MESSAGES/django.mo000066400000000000000000000006701477540643200274170ustar00rootroot00000000000000<\pq  Your email addressYour messageYour nameProject-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2018-07-24 04:14+0200 Last-Translator: GbP Language-Team: Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La tua emailIl tuo messaggioIl tuo nomedjango-contact-form-5.2.0/src/django_contact_form/locale/it/LC_MESSAGES/django.po000066400000000000000000000013261477540643200274210ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-09-20 08:29+0200\n" "PO-Revision-Date: 2018-07-24 04:14+0200\n" "Last-Translator: GbP \n" "Language-Team: \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: forms.py:23 msgid "Your name" msgstr "Il tuo nome" #: forms.py:25 msgid "Your email address" msgstr "La tua email" #: forms.py:27 msgid "Your message" msgstr "Il tuo messaggio" django-contact-form-5.2.0/src/django_contact_form/locale/nl/000077500000000000000000000000001477540643200240255ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/nl/LC_MESSAGES/000077500000000000000000000000001477540643200256125ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/nl/LC_MESSAGES/django.mo000066400000000000000000000012131477540643200274060ustar00rootroot00000000000000Dl $ jC R%]Your email addressYour messageYour message was classified as spam.Your nameProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: 2020-05-31 09:35+0200 Last-Translator: Jean-Paul Ladage Language-Team: Zest vertalers Language: nl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); Uw e-mailadresUw berichtUw bericht wordt als spam aangemerkt.Uw naamdjango-contact-form-5.2.0/src/django_contact_form/locale/nl/LC_MESSAGES/django.po000066400000000000000000000020011477540643200274050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Jean-Paul Ladage , 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-05-31 08:35+0200\n" "PO-Revision-Date: 2020-05-31 09:35+0200\n" "Last-Translator: Jean-Paul Ladage \n" "Language-Team: Zest vertalers \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: src/contact_form/forms.py:40 msgid "Your name" msgstr "Uw naam" #: src/contact_form/forms.py:41 msgid "Your email address" msgstr "Uw e-mailadres" #: src/contact_form/forms.py:42 msgid "Your message" msgstr "Uw bericht" #: src/contact_form/forms.py:161 msgid "Your message was classified as spam." msgstr "Uw bericht wordt als spam aangemerkt." django-contact-form-5.2.0/src/django_contact_form/locale/uk/000077500000000000000000000000001477540643200240335ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/uk/LC_MESSAGES/000077500000000000000000000000001477540643200256205ustar00rootroot00000000000000django-contact-form-5.2.0/src/django_contact_form/locale/uk/LC_MESSAGES/django.mo000066400000000000000000000012541477540643200274210ustar00rootroot00000000000000Dl $ *#$KHYour email addressYour messageYour message was classified as spam.Your nameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-09-09 22:34-0700 PO-Revision-Date: 2019-12-04 00:33+0200 Language: uk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last-Translator: Language-Team: X-Generator: Poedit 2.2.4 Ваша електронна адресаТекст повідомленняВаше повідомлення класифіковано як спам.Як вас звати?django-contact-form-5.2.0/src/django_contact_form/locale/uk/LC_MESSAGES/django.po000066400000000000000000000016461477540643200274310ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-09-09 22:34-0700\n" "PO-Revision-Date: 2019-12-04 00:33+0200\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: \n" "Language-Team: \n" "X-Generator: Poedit 2.2.4\n" #: forms.py:21 msgid "Your name" msgstr "Як вас звати?" #: forms.py:23 msgid "Your email address" msgstr "Ваша електронна адреса" #: forms.py:25 msgid "Your message" msgstr "Текст повідомлення" #: forms.py:140 msgid "Your message was classified as spam." msgstr "Ваше повідомлення класифіковано як спам." django-contact-form-5.2.0/src/django_contact_form/urls.py000066400000000000000000000012021477540643200235070ustar00rootroot00000000000000""" Example URLConf for a contact form. If all you want is the basic ContactForm with default behavior, include this URLConf somewhere in your URL hierarchy (for example, at ``/contact/``) """ # SPDX-License-Identifier: BSD-3-Clause from django.urls import path from django.views.generic import TemplateView from django_contact_form.views import ContactFormView urlpatterns = [ path("", ContactFormView.as_view(), name="django_contact_form"), path( "sent/", TemplateView.as_view( template_name="django_contact_form/contact_form_sent.html" ), name="django_contact_form_sent", ), ] django-contact-form-5.2.0/src/django_contact_form/views.py000066400000000000000000000101551477540643200236660ustar00rootroot00000000000000""" View which can render and send email from a contact form. """ # SPDX-License-Identifier: BSD-3-Clause from django.urls import reverse_lazy from django.views.generic.edit import FormView from .forms import ContactForm class ContactFormView(FormView): """ The base view class from which most custom contact-form views should inherit. If you don't need any custom functionality, and are content with the default :class:`~django_contact_form.forms.ContactForm` class, you can also use it as-is (and the provided URLConf, ``django_contact_form.urls``, does exactly this). This is a subclass of Django's :class:`~django.views.generic.edit.FormView`, so refer to the Django documentation for a list of attributes/methods which can be overridden to customize behavior. One non-standard attribute is defined here: .. attribute:: recipient_list The list of email addresses to send mail to. If not specified, defaults to the :attr:`~django_contact_form.forms.ContactForm.recipient_list` of the form. Additionally, the following standard (from :class:`~django.views.generic.edit.FormView`) attributes are commonly useful to override (all attributes below can also be passed to :meth:`~django.views.generic.base.View.as_view()` in the URLconf, permitting customization without the need to write a full custom subclass of :class:`ContactFormView`). Each of these can be supplied as an attribute, or as a method with the name prefixed with ``get_`` (for example, a ``get_form_class()`` method instead of a ``form_class`` attribute): .. attribute:: form_class The form class to use. By default, will be :class:`~django_contact_form.forms.ContactForm`. .. attribute:: template_name A :class:`str`, the template to use when rendering the form. By default, will be ``django_contact_form/contact_form.html``. .. attribute:: success_url The URL to redirect to after successful form submission. Can be a hard-coded string, the string resulting from calling Django's :func:`~django.urls.reverse` helper, or the lazy object produced by Django's :func:`~django.urls.reverse_lazy` helper. Default value is the result of calling :func:`~django.urls.reverse_lazy` with the URL name ``"django_contact_form_sent"``. You can also override the following method for full customization of the form instance construction: .. automethod:: get_form_kwargs """ form_class = ContactForm recipient_list = None success_url = reverse_lazy("django_contact_form_sent") template_name = "django_contact_form/contact_form.html" def form_valid(self, form): """ Handle a valid form by sending the email. """ form.save() return super().form_valid(form) def get_form_kwargs(self) -> dict: """ Return additional keyword arguments (as a dictionary) to pass to the form class on initialization. By default, this will return a dictionary containing the current :class:`~django.http.HttpRequest` (as the key ``request``) and, if :attr:`~ContactFormView.recipient_list` was defined, its value (as the key ``recipient_list``). .. warning:: **Request is a required argument** If you override :meth:`get_form_kwargs`, you **must** ensure that, at the very least, the keyword argument ``request`` is still provided, or :class:`~django_contact_form.forms.ContactForm` initialization will raise :exc:`TypeError`. The easiest approach is to use :class:`super` to call the base implementation in :class:`ContactFormView`, and modify the dictionary it returns. """ # ContactForm instances require instantiation with an # HttpRequest. kwargs = super().get_form_kwargs() kwargs.update({"request": self.request}) # We may also have been given a recipient list when # instantiated. if self.recipient_list is not None: kwargs.update({"recipient_list": self.recipient_list}) return kwargs django-contact-form-5.2.0/tests/000077500000000000000000000000001477540643200165305ustar00rootroot00000000000000django-contact-form-5.2.0/tests/__init__.py000066400000000000000000000000001477540643200206270ustar00rootroot00000000000000django-contact-form-5.2.0/tests/templates/000077500000000000000000000000001477540643200205265ustar00rootroot00000000000000django-contact-form-5.2.0/tests/templates/django_contact_form/000077500000000000000000000000001477540643200245265ustar00rootroot00000000000000django-contact-form-5.2.0/tests/templates/django_contact_form/contact_form.html000066400000000000000000000000131477540643200300640ustar00rootroot00000000000000{{ body }} django-contact-form-5.2.0/tests/templates/django_contact_form/contact_form.txt000066400000000000000000000000121477540643200277360ustar00rootroot00000000000000{{ body }}django-contact-form-5.2.0/tests/templates/django_contact_form/contact_form_sent.html000066400000000000000000000000251477540643200311200ustar00rootroot00000000000000

Message sent!

django-contact-form-5.2.0/tests/templates/django_contact_form/contact_form_subject.txt000066400000000000000000000000241477540643200314600ustar00rootroot00000000000000Contact form messagedjango-contact-form-5.2.0/tests/templates/django_contact_form/test_callable_template_name.html000066400000000000000000000000351477540643200331030ustar00rootroot00000000000000Callable template_name used. django-contact-form-5.2.0/tests/test_akismet_integration.py000066400000000000000000000167771477540643200242230ustar00rootroot00000000000000""" Tests for the Akismet spam-filtering integration. """ # SPDX-License-Identifier: BSD-3-Clause import os from http import HTTPStatus from unittest import mock import akismet from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.urls import reverse from django_contact_form._akismet import _clear_cached_instance, _try_get_akismet_client from django_contact_form.forms import AkismetContactForm class AlwaysSpamClient(akismet.TestSyncClient): """ Akismet client which always marks content as spam. """ comment_check_response = akismet.CheckResponse.SPAM class NeverSpamClient(akismet.TestSyncClient): """ Akismet client which always marks content as non-spam. """ comment_check_response = akismet.CheckResponse.HAM class ValidConfigClient(akismet.TestSyncClient): """ Akismet client which marks its configuration as valid. """ verify_key_response = True class InvalidConfigClient(akismet.TestSyncClient): """ Akismet client which marks its configuration as invalid. """ verify_key_response = False @override_settings(ROOT_URLCONF="django_contact_form.akismet_urls") class AkismetContactFormTests(TestCase): """ Tests for the Akismet contact form. """ akismet_config = akismet.Config(key="test-key", url="http://example.com") payload = { "name": "Test Name", "email": "test@example.com", "body": "Test message.", } def setUp(self): """ Ensure the Akismet client instance is not cached between tests. """ _clear_cached_instance() def request(self): """ Construct and return an HttpRequest object for test use. """ return RequestFactory().request() def test_akismet_form_spam(self): """ The Akismet contact form correctly rejects spam. """ with ( AlwaysSpamClient(config=self.akismet_config) as akismet_client, mock.patch( "django_contact_form._akismet._try_get_akismet_client", new=mock.Mock(return_value=akismet_client), ), ): form = AkismetContactForm(request=self.request(), data=self.payload) assert not form.is_valid() assert str(form.SPAM_MESSAGE) in form.errors["body"] def test_akismet_form_ham(self): """ The Akismet contact form correctly accepts non-spam. """ with ( NeverSpamClient(config=self.akismet_config) as akismet_client, mock.patch( "django_contact_form._akismet._try_get_akismet_client", new=mock.Mock(return_value=akismet_client), ), ): form = AkismetContactForm(request=self.request(), data=self.payload) assert form.is_valid() def test_akismet_form_no_body(self): """ The Akismet contact form correctly skips validation when no email body is provided. """ data = {"name": "Test", "email": "email@example.com"} with ( NeverSpamClient(config=self.akismet_config) as akismet_client, mock.patch( "django_contact_form._akismet._try_get_akismet_client", new=mock.Mock(return_value=akismet_client), ), ): form = AkismetContactForm(request=self.request(), data=data) assert not form.is_valid() def test_akismet_django_settings_invalid(self): """ When the Django settings are present and invalid, ImproperlyConfigured is raised. """ with ( self.settings( AKISMET_API_KEY=self.akismet_config.key, AKISMET_BLOG_URL=self.akismet_config.url, ), self.assertRaises(ImproperlyConfigured), ): _try_get_akismet_client(InvalidConfigClient) def test_akismet_env_invalid(self): """ When the environment variables are present and invalid, ImproperlyConfigured is raised. """ try: os.environ["PYTHON_AKISMET_API_KEY"] = self.akismet_config.key os.environ["PYTHON_AKISMET_BLOG_URL"] = self.akismet_config.url with self.assertRaises(ImproperlyConfigured): _try_get_akismet_client(InvalidConfigClient) finally: del os.environ["PYTHON_AKISMET_API_KEY"] del os.environ["PYTHON_AKISMET_BLOG_URL"] def test_akismet_env_missing(self): """ When the environment variables are missing, ImproperlyConfigured is raised. """ for key in ("PYTHON_AKISMET_API_KEY", "PYTHON_AKISMET_BLOG_URL"): if key in os.environ: del os.environ[key] with self.assertRaises(ImproperlyConfigured): _try_get_akismet_client(InvalidConfigClient) def test_akismet_django_settings_valid(self): """ When the Django settings are present and valid, an Akismet client is returned using them. """ with self.settings( AKISMET_API_KEY=self.akismet_config.key, AKISMET_BLOG_URL=self.akismet_config.url, ): client = _try_get_akismet_client(ValidConfigClient) assert client is not None assert isinstance(client, ValidConfigClient) def test_akismet_env_valid(self): """ When the environment variables are present and valid, an Akismet client is returned using them. """ try: os.environ["PYTHON_AKISMET_API_KEY"] = self.akismet_config.key os.environ["PYTHON_AKISMET_BLOG_URL"] = self.akismet_config.url client = _try_get_akismet_client(ValidConfigClient) assert client is not None assert isinstance(client, ValidConfigClient) finally: del os.environ["PYTHON_AKISMET_API_KEY"] del os.environ["PYTHON_AKISMET_BLOG_URL"] def test_akismet_django_settings_persist(self): """ A client created from Django settings is correctly persistsed and returned on later calls. """ with self.settings( AKISMET_API_KEY=self.akismet_config.key, AKISMET_BLOG_URL=self.akismet_config.url, ): first = _try_get_akismet_client(ValidConfigClient) second = _try_get_akismet_client(ValidConfigClient) assert first is second def test_akismet_env_persist(self): """ A client cretaed from environment variables is correctly persisted and returned on later calls. """ try: os.environ["PYTHON_AKISMET_API_KEY"] = self.akismet_config.key os.environ["PYTHON_AKISMET_BLOG_URL"] = self.akismet_config.url first = _try_get_akismet_client(ValidConfigClient) second = _try_get_akismet_client(ValidConfigClient) assert first is second finally: del os.environ["PYTHON_AKISMET_API_KEY"] del os.environ["PYTHON_AKISMET_BLOG_URL"] @override_settings(ROOT_URLCONF="django_contact_form.akismet_urls") def test_akismet_view(self): """ The Akismet contact form URL uses a spam-filtering AkismetContactForm instance. """ response = self.client.get(reverse("django_contact_form")) assert response.status_code == HTTPStatus.OK assert isinstance(response.context["form"], AkismetContactForm) django-contact-form-5.2.0/tests/test_forms.py000066400000000000000000000114731477540643200212750ustar00rootroot00000000000000""" Tests for the built-in form classes. """ # SPDX-License-Identifier: BSD-3-Clause from django.conf import settings from django.core import mail from django.test import RequestFactory, TestCase from django_contact_form.forms import ContactForm class ContactFormTests(TestCase): """ Tests for the base ContactForm. """ valid_data = {"name": "Test", "email": "test@example.com", "body": "Test message"} def request(self): """ Construct and return an HttpRequest object for test use. """ return RequestFactory().request() def test_request_required(self): """ Can't instantiate without an HttpRequest. """ self.assertRaises(TypeError, ContactForm) def test_valid_data_required(self): """ Can't try to build the message dict unless data is valid. """ data = {"name": "Test", "body": "Test message"} form = ContactForm(request=self.request(), data=data) self.assertRaises(ValueError, form.get_message_dict) self.assertRaises(ValueError, form.get_message_context) def test_send(self): """ Valid form can and does in fact send email. """ form = ContactForm(request=self.request(), data=self.valid_data) assert form.is_valid() form.save() assert 1 == len(mail.outbox) message = mail.outbox[0] assert self.valid_data["body"] in message.body assert settings.DEFAULT_FROM_EMAIL == message.from_email assert form.recipient_list == message.recipients() def test_no_sites(self): """ Sites integration works with or without installed contrib.sites. """ with self.modify_settings(INSTALLED_APPS={"remove": ["django.contrib.sites"]}): form = ContactForm(request=self.request(), data=self.valid_data) assert form.is_valid() form.save() assert 1 == len(mail.outbox) def test_recipient_list(self): """ Passing recipient_list when instantiating ContactForm properly overrides the list of recipients. """ recipient_list = ["recipient_list@example.com"] form = ContactForm( request=self.request(), data=self.valid_data, recipient_list=recipient_list ) assert form.is_valid() form.save() assert 1 == len(mail.outbox) message = mail.outbox[0] assert recipient_list == message.recipients() def test_callable_template_name(self): """ When a template_name() method is defined, it is used and preferred over a 'template_name' attribute. """ class CallableTemplateName(ContactForm): """ Form with a template_name() method instead of a template_name attribute. """ # pylint: disable=invalid-overridden-method def template_name(self): """ Return the template name as a method rather than an attribute. """ return "django_contact_form/test_callable_template_name.html" form = CallableTemplateName(request=self.request(), data=self.valid_data) assert form.is_valid() form.save() assert 1 == len(mail.outbox) message = mail.outbox[0] assert "Callable template_name used." in message.body def test_callable_message_parts(self): """ Message parts implemented as methods are called and preferred over attributes. """ overridden_data = { "from_email": "override@example.com", "message": "Overridden message.", "recipient_list": ["override_recpt@example.com"], "subject": "Overridden subject", } class CallableMessageParts(ContactForm): """ Form with the message parts as methods rather than attributes. """ def from_email(self): """ Method returning the from_email. """ return overridden_data["from_email"] def message(self): """ Method returning the message. """ return overridden_data["message"] def recipient_list(self): # pylint: disable=method-hidden """ Method returning the recipient_list. """ return overridden_data["recipient_list"] def subject(self): """ Method returning the subject. """ return overridden_data["subject"] form = CallableMessageParts(request=self.request(), data=self.valid_data) assert form.is_valid() assert overridden_data == form.get_message_dict() django-contact-form-5.2.0/tests/test_settings.py000066400000000000000000000026411477540643200220040ustar00rootroot00000000000000""" Minimal Django settings file for test runs. """ # SPDX-License-Identifier: BSD-3-Clause import pathlib from django.utils.crypto import get_random_string APP_DIR = pathlib.Path(__file__).parents[0] DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_FROM_EMAIL = "noreply@example.com" DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sites", "django_contact_form", "tests", ) MANAGERS = [("Manager", "noreply@example.com")] MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", ) ROOT_URLCONF = "django_contact_form.urls" SECRET_KEY = get_random_string(12) SITE_ID = 1 TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [APP_DIR / "templates"], "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", ] }, } ] django-contact-form-5.2.0/tests/test_urls.py000066400000000000000000000012261477540643200211270ustar00rootroot00000000000000""" URLConf for testing django-contact-form. """ # SPDX-License-Identifier: BSD-3-Clause from django.urls import path from django.views.generic import TemplateView from django_contact_form.views import ContactFormView urlpatterns = [ path("", ContactFormView.as_view(), name="django_contact_form"), path( "sent/", TemplateView.as_view( template_name="django_contact_form/contact_form_sent.html" ), name="django_contact_form_sent", ), path( "test_recipient_list/", ContactFormView.as_view(recipient_list=["recipient_list@example.com"]), name="test_recipient_list", ), ] django-contact-form-5.2.0/tests/test_views.py000066400000000000000000000066341477540643200213070ustar00rootroot00000000000000""" Tests for the built-in views. """ # SPDX-License-Identifier: BSD-3-Clause from http import HTTPStatus import django from django.conf import settings from django.core import mail from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.urls import reverse from django_contact_form.forms import AkismetContactForm, ContactForm @override_settings(ROOT_URLCONF="tests.test_urls") class ContactFormViewTests(TestCase): """ Tests for the built-in ContactFormView. """ def test_get(self): """ HTTP GET on the form view just shows the form. """ contact_url = reverse("django_contact_form") response = self.client.get(contact_url) assert HTTPStatus.OK == response.status_code self.assertTemplateUsed(response, "django_contact_form/contact_form.html") def test_send(self): """ Valid data through the view results in a successful send. """ contact_url = reverse("django_contact_form") data = {"name": "Test", "email": "test@example.com", "body": "Test message"} response = self.client.post(contact_url, data=data) self.assertRedirects(response, reverse("django_contact_form_sent")) assert 1 == len(mail.outbox) message = mail.outbox[0] assert data["body"] in message.body assert settings.DEFAULT_FROM_EMAIL == message.from_email form = ContactForm(request=RequestFactory().request) assert form.recipient_list == message.recipients() def test_invalid(self): """ Invalid data doesn't work. """ contact_url = reverse("django_contact_form") data = {"name": "Test", "body": "Test message"} response = self.client.post(contact_url, data=data) assert HTTPStatus.OK == response.status_code # The argument signature of assertFormError() changed beginning in Django 4.1 -- # prior to that the first argument was a response object, and after the # deprecation cycle completed in 5.0 the first argument is now the form # instance. if django.get_version() < "4.2": self.assertFormError(response, "form", "email", "This field is required.") else: self.assertFormError( response.context["form"], "email", "This field is required." ) self.assertEqual(0, len(mail.outbox)) def test_recipient_list(self): """ Passing recipient_list when instantiating ContactFormView properly overrides the list of recipients. """ contact_url = reverse("test_recipient_list") data = {"name": "Test", "email": "test@example.com", "body": "Test message"} response = self.client.post(contact_url, data=data) self.assertRedirects(response, reverse("django_contact_form_sent")) assert 1 == len(mail.outbox) message = mail.outbox[0] assert ["recipient_list@example.com"] == message.recipients() @override_settings(ROOT_URLCONF="django_contact_form.akismet_urls") def test_akismet_view(self): """ The Akismet contact form URL uses a spam-filtering AkismetContactForm instance. """ response = self.client.get(reverse("django_contact_form")) assert response.status_code == HTTPStatus.OK assert isinstance(response.context["form"], AkismetContactForm)