├── tests
└── __init__.py
├── django_docutils
├── __init__.py
├── lib
│ ├── __init__.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_writers.py
│ │ └── test_utils.py
│ ├── metadata
│ │ ├── __init__.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_extract.py
│ │ │ └── test_process.py
│ │ ├── process.py
│ │ ├── processors.py
│ │ └── extract.py
│ ├── transforms
│ │ ├── __init__.py
│ │ ├── font_awesome.py
│ │ ├── toc.py
│ │ ├── ads.py
│ │ └── code.py
│ ├── fixtures
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── test_fixtures.py
│ │ │ ├── test_utils.py
│ │ │ ├── conftest.py
│ │ │ └── test_load.py
│ │ ├── directory
│ │ │ ├── tests
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_utils.py
│ │ │ │ ├── test_extract.py
│ │ │ │ ├── test_multifile.py
│ │ │ │ └── conftest.py
│ │ │ ├── __init__.py
│ │ │ ├── extract.py
│ │ │ └── utils.py
│ │ ├── __init__.py
│ │ └── utils.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── rst.py
│ ├── templates
│ │ └── rst
│ │ │ ├── base.html
│ │ │ └── raw.html
│ ├── settings.py
│ ├── roles
│ │ ├── kbd.py
│ │ ├── email.py
│ │ ├── pypi.py
│ │ ├── url.py
│ │ ├── twitter.py
│ │ ├── hackernews.py
│ │ ├── wikipedia.py
│ │ ├── leanpub.py
│ │ ├── readthedocs.py
│ │ ├── github.py
│ │ ├── develtech.py
│ │ ├── amazon.py
│ │ ├── file.py
│ │ ├── common.py
│ │ └── __init__.py
│ ├── directives
│ │ ├── __init__.py
│ │ └── code.py
│ ├── text.py
│ ├── views.py
│ └── utils.py
├── pygments
│ └── __init__.py
├── references
│ ├── __init__.py
│ ├── rst
│ │ ├── __init__.py
│ │ ├── transforms
│ │ │ ├── __init__.py
│ │ │ └── xref.py
│ │ ├── utils.py
│ │ └── nodes.py
│ ├── intersphinx
│ │ └── __init__.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── loaddata_intersphinx.py
│ └── models.py
├── favicon
│ ├── rst
│ │ ├── __init__.py
│ │ ├── transforms
│ │ │ ├── __init__.py
│ │ │ └── favicon.py
│ │ └── nodes.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_app
│ │ │ ├── __init__.py
│ │ │ ├── apps.py
│ │ │ └── models.py
│ │ ├── test_store.py
│ │ ├── conftest.py
│ │ ├── test_scrape.py
│ │ └── test_prefetch.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── prefetch_favicons.py
│ ├── __init__.py
│ ├── models.py
│ ├── scrape.py
│ └── prefetch.py
├── templatetags
│ ├── __init__.py
│ └── django_docutils.py
├── rst_post
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── loaddata_grow.py
│ └── models
│ │ ├── tests
│ │ ├── conftest.py
│ │ └── test_models.py
│ │ ├── __init__.py
│ │ ├── post_page.py
│ │ ├── post.py
│ │ ├── utils.py
│ │ └── checks.py
├── models.py
├── exc.py
├── __about__.py
├── engines.py
├── views.py
└── directives.py
├── .python-version
├── docs
├── redirects.txt
├── contributing.rst
├── history.md
├── _static
│ ├── img
│ │ └── icons
│ │ │ ├── logo.png
│ │ │ ├── favicon.ico
│ │ │ ├── apple-icon.png
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── favicon-96x96.png
│ │ │ ├── ms-icon-70x70.png
│ │ │ ├── apple-icon-57x57.png
│ │ │ ├── apple-icon-60x60.png
│ │ │ ├── apple-icon-72x72.png
│ │ │ ├── apple-icon-76x76.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── ms-icon-144x144.png
│ │ │ ├── ms-icon-150x150.png
│ │ │ ├── ms-icon-310x310.png
│ │ │ ├── mstile-150x150.png
│ │ │ ├── android-icon-36x36.png
│ │ │ ├── android-icon-48x48.png
│ │ │ ├── android-icon-72x72.png
│ │ │ ├── android-icon-96x96.png
│ │ │ ├── apple-icon-114x114.png
│ │ │ ├── apple-icon-120x120.png
│ │ │ ├── apple-icon-144x144.png
│ │ │ ├── apple-icon-152x152.png
│ │ │ ├── apple-icon-180x180.png
│ │ │ ├── android-chrome-36x36.png
│ │ │ ├── android-chrome-48x48.png
│ │ │ ├── android-chrome-72x72.png
│ │ │ ├── android-chrome-96x96.png
│ │ │ ├── android-icon-144x144.png
│ │ │ ├── android-icon-192x192.png
│ │ │ ├── android-chrome-144x144.png
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-256x256.png
│ │ │ ├── android-chrome-384x384.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── apple-icon-precomposed.png
│ │ │ └── safari-pinned-tab.svg
│ ├── css
│ │ └── custom.css
│ └── django-docutils.css
├── _templates
│ ├── star.html
│ ├── more.html
│ ├── sidebar
│ │ └── projects.html
│ └── layout.html
├── index.md
├── api.md
├── quickstart.md
├── manifest.json
└── conf.py
├── poetry.toml
├── .tool-versions
├── .tmuxp-before-script.sh
├── .github
├── dependabot.yml
└── workflows
│ ├── tests.yml
│ └── docs.yml
├── MANIFEST.in
├── .readthedocs.yml
├── .codecov.yml
├── .pre-commit-config.yaml
├── .gitignore
├── .tmuxp.yaml
├── setup.cfg
├── LICENSE
├── Makefile
├── CHANGES
├── pyproject.toml
└── CONTRIBUTING.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/pygments/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/references/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/favicon/rst/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/transforms/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/references/rst/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.10.4 3.9.9 3.8.11 3.7.12
2 |
--------------------------------------------------------------------------------
/django_docutils/favicon/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/favicon/rst/transforms/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
--------------------------------------------------------------------------------
/django_docutils/references/intersphinx/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/references/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/references/rst/transforms/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/redirects.txt:
--------------------------------------------------------------------------------
1 | "usage.rst" "quickstart.md"
2 |
--------------------------------------------------------------------------------
/poetry.toml:
--------------------------------------------------------------------------------
1 | [virtualenvs]
2 | in-project = true
3 |
--------------------------------------------------------------------------------
/django_docutils/favicon/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/references/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | poetry 1.1.12
2 | python 3.10.1 3.9.9 3.8.11 3.7.12
3 |
--------------------------------------------------------------------------------
/django_docutils/exc.py:
--------------------------------------------------------------------------------
1 | class BasedException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/docs/history.md:
--------------------------------------------------------------------------------
1 | (history)=
2 |
3 | ```{include} ../CHANGES
4 |
5 | ```
6 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/__init__.py:
--------------------------------------------------------------------------------
1 | """reStructuredText data fixtures for django apps."""
2 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from django_docutils.favicon.tests.conftest import * # NOQA: F4
2 |
--------------------------------------------------------------------------------
/docs/_static/img/icons/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/logo.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/favicon.ico
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/test_app/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = "django_docutils.favicon.tests.test_app.apps.TestAppConfig"
2 |
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/ms-icon-70x70.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/ms-icon-150x150.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/ms-icon-310x310.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-icon-36x36.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-icon-48x48.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-icon-72x72.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-icon-96x96.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/.tmuxp-before-script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | poetry shell --no-ansi --no-interaction &2> /dev/null
3 | poetry install --no-ansi --no-interaction &2> /dev/null
4 |
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-36x36.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-48x48.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-72x72.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-96x96.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-icon-144x144.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-icon-192x192.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-144x144.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-256x256.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-384x384.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/docs/_static/img/icons/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-docutils/HEAD/docs/_static/img/icons/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/django_docutils/favicon/rst/nodes.py:
--------------------------------------------------------------------------------
1 | from docutils import nodes
2 |
3 |
4 | class icon(nodes.Inline, nodes.TextElement):
5 | pass
6 |
7 |
8 | nodes._add_node_class_names("icon")
9 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CONTRIBUTING.rst
2 | include CHANGES
3 | include LICENSE
4 | include README.md
5 | include .tmuxp.yaml .tmuxp-before-script.sh
6 | recursive-include docs *.rst
7 | recursive-include django_docutils *.py
8 |
--------------------------------------------------------------------------------
/django_docutils/lib/templates/rst/base.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content_inner %}
4 |
5 | {{content}}
6 |
7 | {% endblock content_inner %}
8 |
9 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/test_app/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TestAppConfig(AppConfig):
5 | name = "django_docutils.favicon.tests.test_app"
6 | label = "test_app"
7 | verbose_name = "For pytest"
8 |
--------------------------------------------------------------------------------
/django_docutils/lib/settings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | BASED_LIB_RST = getattr(settings, "BASED_LIB_RST", {})
4 |
5 | INJECT_FONT_AWESOME = (
6 | BASED_LIB_RST.get("font_awesome", {}).get("url_patterns") is not None
7 | )
8 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | build:
3 | image: latest
4 | python:
5 | version: 3.6
6 | install:
7 | - method: pip
8 | path: .
9 | extra_requirements:
10 | - docs
11 | - requirements: doc/requirements.txt
12 |
--------------------------------------------------------------------------------
/docs/_templates/star.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: NOQA F4
2 | from .checks import _check_postpage_post_back_relation, _check_root_page
3 | from .post import RSTPostBase, get_post_model, get_post_models
4 | from .post_page import RSTPostPageBase, get_postpage_models
5 |
--------------------------------------------------------------------------------
/django_docutils/references/rst/utils.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from docutils import nodes
4 |
5 |
6 | def set_role_source_info(inliner: Any, lineno: int, node: nodes.Node):
7 | """From sphinx, for intersphinx"""
8 | node.source, node.line = inliner.reporter.get_source_and_line(lineno)
9 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | notify:
3 | require_ci_to_pass: no
4 |
5 | coverage:
6 | precision: 2
7 | round: down
8 | range: "70...100"
9 | status:
10 | project:
11 | default:
12 | target: auto
13 | threshold: 1%
14 | base: auto
15 | patch: off
16 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | (index)=
2 |
3 | ```{include} ../README.md
4 |
5 | ```
6 |
7 | ```{toctree}
8 | :maxdepth: 2
9 | :hidden:
10 |
11 | quickstart
12 | api
13 | ```
14 |
15 | ```{toctree}
16 | :caption: Project
17 | :hidden:
18 |
19 | history
20 | contributing
21 | GitHub
22 |
23 | ```
24 |
--------------------------------------------------------------------------------
/django_docutils/references/rst/nodes.py:
--------------------------------------------------------------------------------
1 | from docutils import nodes
2 |
3 |
4 | class pending_xref(nodes.Inline, nodes.Element):
5 |
6 | """Node for cross-references that cannot be resolved without complete
7 | information about all documents.
8 |
9 | These nodes are resolved before writing output, in
10 | BuildEnvironment.resolve_references.
11 | """
12 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/ambv/black
3 | rev: 21.3.0
4 | hooks:
5 | - id: black
6 | language_version: python3.10
7 | - repo: https://github.com/pycqa/isort
8 | rev: 5.10.1
9 | hooks:
10 | - id: isort
11 | name: isort (python)
12 | - repo: https://gitlab.com/pycqa/flake8
13 | rev: 4.0.1
14 | hooks:
15 | - id: flake8
16 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/kbd.py:
--------------------------------------------------------------------------------
1 | from docutils import nodes
2 |
3 |
4 | def kbd_role(name, rawtext, text, lineno, inliner, options=None, content=None):
5 | html = ""
6 | keys = text.split(",")
7 |
8 | if isinstance(keys, str):
9 | keys = [keys]
10 |
11 | for key in keys:
12 | html += f"{key}"
13 |
14 | return [nodes.raw("", html, format="html")], []
15 |
--------------------------------------------------------------------------------
/django_docutils/lib/templates/rst/raw.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load rst %}
3 | {% load render_entrypoint from webpack_loader %}
4 |
5 | {% block content_inner %}
6 |
7 | {% restructuredtext content inject_ads=inject_ads %}
8 |
9 | {% endblock content %}
10 |
11 | {% block extra_js %}
12 | {% render_entrypoint 'toc_scroller' 'js' %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | """Test reST fixture files in directory format, "dir"."""
2 |
3 | from django_docutils.lib.fixtures.directory.utils import find_rst_dir_projects
4 |
5 |
6 | def test_find_rst_dir_projects(tmpdir):
7 | tmpdir.mkdir("wat")
8 | tmpdir.join("wat/README.rst").write("")
9 | tmpdir.join("wat/manifest.json").write("")
10 |
11 | assert [str(tmpdir.join("wat"))] == find_rst_dir_projects(str(tmpdir))
12 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This format is different:
3 |
4 | 1. It resides in its own directory
5 | 2. Includes a JSON file
6 | 3. Does not store configuration in the RST file itself
7 |
8 | This is intended to ensure these projects can be open source repositories
9 | with collaborators on GitHub.
10 |
11 | Here is what a layout of "dir"-type RST fixtures look like:
12 |
13 | fixtures/
14 | - myproject/
15 | - manifest.json: post content configuration information (e.g. title, taxonomy)
16 | - README.rst: reStructuredText content
17 | """
18 |
--------------------------------------------------------------------------------
/django_docutils/__about__.py:
--------------------------------------------------------------------------------
1 | __title__ = "django-docutils"
2 | __package_name__ = "django_docutils"
3 | __description__ = "Documentation Utilities (Docutils, reStructuredText) for django."
4 | __version__ = "0.6.0"
5 | __author__ = "Tony Narlock"
6 | __github__ = "https://github.com/tony/django-docutils"
7 | __pypi__ = "https://pypi.org/project/django-docutils/"
8 | __docs__ = "https://django-docutils.git-pull.com"
9 | __tracker__ = "https://github.com/tony/django-docutils/issues"
10 | __email__ = "tony@git-pull.com"
11 | __license__ = "MIT"
12 | __copyright__ = "Copyright 2013- Tony Narlock"
13 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/email.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def email_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to email articles.
6 |
7 | :email:`me@localhost` ->
8 | link: mailto:me@localhost
9 | text: me@localhost
10 |
11 | :email:`E-mail me ` ->
12 | link: mailto:me@localhost
13 | text: E-mail me
14 |
15 | """
16 |
17 | def url_handler(target):
18 | return f"mailto:{target}"
19 |
20 | return generic_url_role(name, text, url_handler)
21 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/test_store.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django.core.files.uploadedfile import SimpleUploadedFile
4 |
5 |
6 | @pytest.mark.django_db(transaction=True)
7 | def test_stored_favicon(Favicon):
8 | favicon = Favicon()
9 |
10 | favicon_content = b"imgcontent"
11 |
12 | favicon.domain = "test.com"
13 | favicon.favicon = SimpleUploadedFile(
14 | name="test_image.jpg", content=favicon_content, content_type="image/ico"
15 | )
16 |
17 | favicon.save()
18 |
19 | f = Favicon.objects.first()
20 | assert f.favicon.read() == favicon_content
21 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/extract.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 |
5 | def extract_dir_config(path):
6 | """Return config metadata for dir RST project.
7 |
8 | Automatically lowercases keys for a parity with docutils' docinfo behavior.
9 |
10 | :param path: path to project (directory)
11 | :type path: string
12 | :returns: metadata from json file
13 | :rtype: dict
14 | """
15 | config_file = os.path.join(path, "manifest.json")
16 | config_dict = json.loads(open(config_file).read())
17 | return {k.lower(): v for k, v in config_dict.items()}
18 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/pypi.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def pypi_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to pypi page.
6 |
7 | :pypi:`libsass` ->
8 | link: https://pypi.python.org/pypi/libsass
9 | text: libsass
10 |
11 |
12 | :pypi:`a pypi package ` ->
13 | link: https://pypi.python.org/pypi/libsass
14 | text: a pypi package
15 | """
16 |
17 | def url_handler(target):
18 | return f"https://pypi.python.org/pypi/{target}"
19 |
20 | return generic_url_role(name, text, url_handler)
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | pip-wheel-metadata
18 | .installed.cfg
19 | lib64
20 |
21 | # Installer logs
22 | pip-log.txt
23 |
24 | # Unit test / coverage reports
25 | .coverage
26 | .tox
27 | nosetests.xml
28 |
29 | # Translations
30 | *.mo
31 |
32 | # Mr Developer
33 | .mr.developer.cfg
34 | .project
35 | .pydevproject
36 |
37 | # Complexity
38 | output/*.html
39 | output/*/index.html
40 |
41 | # Sphinx
42 | docs/_build
43 |
44 | .eggs/
45 | .cache/
46 | .mypy_cache/
47 | .pytest_cache/
48 | .*env*/
49 |
--------------------------------------------------------------------------------
/.tmuxp.yaml:
--------------------------------------------------------------------------------
1 | session_name: django-docutils
2 | start_directory: ./ # load session relative to config location (project root).
3 | before_script: ./.tmuxp-before-script.sh
4 | shell_command_before:
5 | - '[ -f .venv/bin/activate ] && source .venv/bin/activate && reset'
6 | windows:
7 | - window_name: django-docutils
8 | focus: True
9 | layout: main-horizontal
10 | options:
11 | main-pane-height: 35
12 | panes:
13 | - focus: true
14 | - pane
15 | - make watch_test
16 | - window_name: docs
17 | layout: main-horizontal
18 | options:
19 | main-pane-height: 35
20 | start_directory: docs/
21 | panes:
22 | - focus: true
23 | - pane
24 | - pane
25 | - make start
26 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/url.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def url_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to url articles.
6 |
7 | :url:`https://google.com` ->
8 | link: https://google.com
9 | text: https://google.com
10 |
11 | :url:`Google ` ->
12 | link: https://google.com
13 | text: Google
14 |
15 | :url:`*Google* ` ->
16 | link: https://google.com
17 | text (html): Google
18 |
19 | """
20 |
21 | def url_handler(target):
22 | return target
23 |
24 | return generic_url_role(name, text, url_handler)
25 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/twitter.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def twitter_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to twitter articles.
6 |
7 | :twitter:`@username` ->
8 | link: https://twitter.com/username
9 | text: @username
10 |
11 | :twitter:`follow me on twitter <@username>` ->
12 | link: https://twitter.com/username
13 | text: follow on me on twitter
14 | """
15 |
16 | def url_handler(target):
17 | if "@" in target:
18 | target = target.replace("@", "")
19 |
20 | return f"https://twitter.com/{target}"
21 |
22 | return generic_url_role(name, text, url_handler)
23 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
3 |
4 | [flake8]
5 | exclude = .*/,.tox,
6 | *.egg,
7 | django_docutils/_compat.py,
8 | django_docutils/__*__.py,
9 | */migrations
10 | select = E,W,F,N
11 | max-line-length = 88
12 | # Stuff we ignore thanks to black: https://github.com/ambv/black/issues/429
13 | ignore = E203,W503
14 |
15 | [isort]
16 | profile = black
17 | combine_as_imports= true
18 | default_section = THIRDPARTY
19 | include_trailing_comma = true
20 | multi_line_output = 3
21 | known_pytest = pytest,py
22 | known_first_party = django_docutils
23 | sections = FUTURE,STDLIB,PYTEST,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
24 | line_length = 88
25 |
26 | [tool:pytest]
27 | filterwarnings =
28 | ignore::PendingDeprecationWarning
29 |
--------------------------------------------------------------------------------
/django_docutils/favicon/__init__.py:
--------------------------------------------------------------------------------
1 | """Favicons
2 |
3 | Short term:
4 |
5 | In the short term, we can use something similar the XRef transform, ran after
6 | that, which downloads and caches favicons.
7 |
8 |
9 | Long term:
10 |
11 | In the future, for this to work at scale, favicons will need to be traversed
12 | ahead of time.
13 |
14 | First, there will to be a way to iterate the content of all Node model
15 | object's content. This can be done via::
16 |
17 | for content in PostPage.objects.all().values_list('body'):
18 | # get the doctree of content
19 | # traverse content nodes.reference
20 | # collect all domains/subdomains
21 | # pull favicons for them and store in Favicon model
22 |
23 | """
24 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | (api)=
2 |
3 | # API Reference
4 |
5 | :::{seealso}
6 |
7 | {ref}`Quickstart `.
8 |
9 | :::
10 |
11 | ## Internals
12 |
13 | ## Directives
14 |
15 | ```{eval-rst}
16 | .. autofunction:: django_docutils.directives.register_pygments_directive
17 | ```
18 |
19 | ```{eval-rst}
20 | .. autodata:: django_docutils.directives.DEFAULT
21 | ```
22 |
23 | ```{eval-rst}
24 | .. autodata:: django_docutils.directives.VARIANTS
25 | ```
26 |
27 | ## Code block
28 |
29 | ```{eval-rst}
30 | .. autoclass:: django_docutils.directives.CodeBlock :members: :inherited-members: :private-members:
31 | :show-inheritance: :member-order: bysource
32 | ```
33 |
34 | ## Exceptions
35 |
36 | ```{eval-rst}
37 | .. autoexception:: django_docutils.exc.BasedException
38 | ```
39 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/hackernews.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote
2 |
3 | from .common import generic_url_role
4 |
5 |
6 | def hackernews_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
7 | """Role for linking to hackernews articles.
8 |
9 | :hn:`15610489` ->
10 | link: https://news.ycombinator.com/item?id=15610489
11 | text: 15610489
12 |
13 |
14 | :hn:`this hackernews article <15610489>` ->
15 | link: https://news.ycombinator.com/item?id=15610489
16 | text: this hackernews article
17 | """
18 |
19 | def url_handler(target):
20 | target = quote(target.replace(" ", "_"))
21 | return f"https://news.ycombinator.com/item?id={target}"
22 |
23 | return generic_url_role(name, text, url_handler)
24 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/wikipedia.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote
2 |
3 | from .common import generic_url_role
4 |
5 |
6 | def wikipedia_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
7 | """Role for linking to Wikipedia articles.
8 |
9 | :wikipedia:`Don't repeat yourself` ->
10 | link: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
11 | text: Don't repeat yourself
12 |
13 |
14 | :wikipedia:`this wikipedia article ` ->
15 | link: https://github.com/vim-airline
16 | text: this wikipedia article
17 | """
18 |
19 | def url_handler(target):
20 | target = quote(target.replace(" ", "_"))
21 | return f"https://en.wikipedia.org/wiki/{target}"
22 |
23 | return generic_url_role(name, text, url_handler)
24 |
--------------------------------------------------------------------------------
/docs/_templates/more.html:
--------------------------------------------------------------------------------
1 | Other Projects
2 |
3 |
4 |
5 | More open source projects from Tony Narlock:
6 |
7 |
13 |
14 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/tests/test_fixtures.py:
--------------------------------------------------------------------------------
1 | import py
2 | import pytest
3 |
4 | from django.apps import AppConfig
5 |
6 | from django_docutils.lib.fixtures.load import load_app_rst_fixtures
7 |
8 |
9 | @pytest.mark.django_db(transaction=True)
10 | def test_load_app_rst_fixtures_bare(bare_app_config, RSTPost):
11 | """Verifies the fixture acts as expected."""
12 | assert isinstance(bare_app_config, AppConfig)
13 |
14 | bare_app = py.path.local(bare_app_config.path)
15 | fixtures_dir = bare_app.ensure("fixtures", dir=True)
16 | test_file = fixtures_dir.join("test.rst")
17 | test_file.write(
18 | """
19 | ===
20 | moo
21 | ===
22 |
23 | :Author: anonymous
24 | :Slug_Id: tEst
25 |
26 | foo
27 | """.strip()
28 | )
29 |
30 | posts = load_app_rst_fixtures(bare_app_config, model=RSTPost)
31 | assert len(posts) == 1
32 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/test_app/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from django_docutils.favicon.models import FaviconBase
4 | from django_docutils.references.models import ReferenceBase
5 | from django_docutils.rst_post.models import RSTPostBase, RSTPostPageBase
6 |
7 |
8 | class EmptyModel(models.Model):
9 | pass
10 |
11 |
12 | class Reference(ReferenceBase):
13 | pass
14 |
15 |
16 | class Favicon(FaviconBase):
17 | pass
18 |
19 |
20 | class RSTPost(RSTPostBase):
21 | root_page = models.ForeignKey(
22 | "RSTPostPage", null=True, on_delete=models.SET_NULL, related_name="+"
23 | )
24 |
25 |
26 | class RSTPostSubclass(RSTPost):
27 | pass
28 |
29 |
30 | class RSTPostPage(RSTPostPageBase):
31 |
32 | post = models.ForeignKey(
33 | RSTPost, on_delete=models.CASCADE, related_name="pages", null=True
34 | )
35 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/tests/test_extract.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import py
4 |
5 | from django_docutils.lib.fixtures.directory.extract import extract_dir_config
6 | from django_docutils.lib.metadata.process import process_metadata
7 |
8 |
9 | def test_extract_dir_config(sample_dir_app_config):
10 | sample_dir_app_dir = py.path.local(sample_dir_app_config.path)
11 | project_dir = sample_dir_app_dir.join("fixtures").join("sample_project")
12 |
13 | raw_metadata = extract_dir_config(str(project_dir))
14 | analyzed_metadata = process_metadata(raw_metadata.copy())
15 |
16 | assert isinstance(analyzed_metadata["created"], datetime.date)
17 |
18 | assert process_metadata(raw_metadata) != {
19 | "programming_languages": ["python"],
20 | "topics": ["Web Frameworks"],
21 | "created": "2017-09-12",
22 | "author": "tony",
23 | }
24 |
--------------------------------------------------------------------------------
/docs/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | h2 {
2 | margin-bottom: 1.25rem;
3 | margin-top: 1.25rem;
4 | scroll-margin-top: 0.5rem;
5 | }
6 |
7 | h3 {
8 | margin-bottom: 1.25rem;
9 | margin-top: 1.25rem;
10 | scroll-margin-top: 0.5rem;
11 | }
12 |
13 | h4 {
14 | margin-bottom: 1.25rem;
15 | scroll-margin-top: 0.5rem;
16 | }
17 |
18 | .sidebar-tree p.indented-block {
19 | padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0
20 | var(--sidebar-item-spacing-horizontal);
21 | margin-bottom: 0;
22 | }
23 |
24 | .sidebar-tree p.indented-block span.indent {
25 | margin-left: var(--sidebar-item-spacing-horizontal);
26 | display: block;
27 | }
28 |
29 | .sidebar-tree p.indented-block .project-name {
30 | font-size: var(--sidebar-item-font-size);
31 | font-weight: bold;
32 | margin-right: calc(var(--sidebar-item-spacing-horizontal) / 2.5);
33 | }
34 |
35 | .sidebar-tree .active {
36 | font-weight: bold;
37 | }
38 |
--------------------------------------------------------------------------------
/docs/_static/img/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
19 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/leanpub.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def leanpub_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to leanpub page.
6 |
7 | :leanpub:`the-tao-of-tmux` ->
8 | link: https://leanpub.com/the-tao-of-tmux
9 | text: the-tao-of-tmux
10 |
11 | :leanpub:`my book ` ->
12 | link: https://leanpub.com/the-tao-of-tmux
13 | text: my book
14 |
15 | :leanpub:`The Tao of tmux ` ->
16 | link: https://leanpub.com/the-tao-of-tmux/read
17 | text: The Tao of tmux
18 | """
19 |
20 | def url_handler(target):
21 | if ":" in target:
22 | project, path = target.split(":")
23 | return "https://leanpub.com/{project}/{path}".format(
24 | project=project, path=path
25 | )
26 | return f"https://leanpub.com/{target}"
27 |
28 | return generic_url_role(name, text, url_handler)
29 |
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | (quickstart)=
2 |
3 | # Quickstart
4 |
5 | ## Installation
6 |
7 | For latest official version:
8 |
9 | ```console
10 | $ pip install django-docutils
11 | ```
12 |
13 | Upgrading:
14 |
15 | ```console
16 | $ pip install --upgrade django-docutils
17 | ```
18 |
19 | (developmental-releases)=
20 |
21 | ### Developmental releases
22 |
23 | New versions of django-docutils are published to PyPI as alpha, beta, or release candidates. In
24 | their versions you will see notfication like `a1`, `b1`, and `rc1`, respectively. `1.10.0b4` would
25 | mean the 4th beta release of `1.10.0` before general availability.
26 |
27 | - [pip]\:
28 |
29 | ```console
30 | $ pip install --upgrade --pre django-docutils
31 | ```
32 |
33 | via trunk (can break easily):
34 |
35 | - [pip]\:
36 |
37 | ```console
38 | $ pip install -e git+https://github.com/tony/django-docutils.git#egg=django-docutils
39 | ```
40 |
41 | [pip]: https://pip.pypa.io/en/stable/
42 |
43 | ## Usage
44 |
45 | ```python
46 | import django_docutils
47 | ```
48 |
49 | See {ref}`Home page `.
50 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/tests/test_multifile.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django_docutils.exc import BasedException
4 | from django_docutils.lib.fixtures.directory.extract import extract_dir_config
5 | from django_docutils.lib.fixtures.directory.utils import (
6 | find_rst_dirs_in_app,
7 | find_series_files,
8 | )
9 |
10 |
11 | def test_dir_with_series(sample_dir_series_app_config):
12 | project_paths = find_rst_dirs_in_app(sample_dir_series_app_config)
13 |
14 | assert len(project_paths) == 1
15 |
16 | project_path = project_paths[0] # pick out the project
17 |
18 | config = extract_dir_config(project_path)
19 |
20 | # this function automatically checks that series files exist
21 | find_series_files(config, project_path)
22 |
23 | # raises exception if files not found
24 | with pytest.raises(BasedException, match=r"Files in .*"):
25 | # test it with missing files
26 | missing_file_config = config.copy()
27 | missing_file_config["series"].append("non-existant-file.rst")
28 | find_series_files(missing_file_config, project_path)
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2015- tmuxp contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/django_docutils/templatetags/django_docutils.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 | from django.utils.encoding import force_bytes, force_str
4 | from django.utils.safestring import mark_safe
5 |
6 | register = template.Library()
7 |
8 |
9 | @register.filter(is_safe=True)
10 | def restructuredtext(value):
11 | import warnings
12 |
13 | warnings.warn(
14 | "The restructuredtext filter has been deprecated", category=DeprecationWarning
15 | )
16 | try:
17 | from docutils.core import publish_parts
18 | except ImportError:
19 | if settings.DEBUG:
20 | raise template.TemplateSyntaxError(
21 | "Error in 'restructuredtext' filter: The Python docutils "
22 | "library isn't installed."
23 | )
24 | return force_str(value)
25 | else:
26 | docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {})
27 | parts = publish_parts(
28 | source=force_bytes(value),
29 | writer_name="html5_polyglot",
30 | settings_overrides=docutils_settings,
31 | )
32 | return mark_safe(force_str(parts["fragment"]))
33 |
--------------------------------------------------------------------------------
/django_docutils/favicon/models.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps as django_apps
2 | from django.conf import settings
3 | from django.core.exceptions import ImproperlyConfigured
4 | from django.db import models
5 | from django.utils.translation import gettext_lazy as _
6 |
7 |
8 | def get_favicon_model():
9 | """
10 | Return the Favicon model that is active in this project.
11 | """
12 | try:
13 | return django_apps.get_model(settings.BASED_FAVICON_MODEL, require_ready=False)
14 | except ValueError:
15 | raise ImproperlyConfigured(
16 | "BASED_FAVICON_MODEL must be of the form 'app_label.model_name'"
17 | )
18 | except LookupError:
19 | raise ImproperlyConfigured(
20 | "BASED_FAVICON_MODEL refers to model '%s' that has not been installed"
21 | % settings.BASED_FAVICON_MODEL
22 | )
23 |
24 |
25 | class FaviconBase(models.Model):
26 | domain = models.URLField(verbose_name=_("Domain or subdomain"), unique=True)
27 | favicon = models.ImageField(
28 | verbose_name=("Path to icon in static files"),
29 | upload_to="favicons",
30 | blank=True,
31 | null=True,
32 | )
33 |
34 | class Meta:
35 | abstract = True
36 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/readthedocs.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def readthedocs_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to readthedocs.org page.
6 |
7 | :rtd:`django-pipeline` ->
8 | link: https://django-pipeline.readthedocs.io/
9 | text: django-pipeline
10 |
11 | :rtd:`a rtd site ` ->
12 | link: https://django-pipeline.readthedocs.io/
13 | text: a rtd site
14 |
15 | :rtd:`python-guide:dev/virtualenvs` ->
16 | link: https://python-guide.readthedocs.io/en/latest/dev/virtualenvs/
17 | text: python-guide:dev/virtualenvs
18 |
19 | :rtd:`about virtualenvs ` ->
20 | link: https://python-guide.readthedocs.io/en/latest/dev/virtualenvs/
21 | text: about virtalenvs
22 | """
23 |
24 | def url_handler(target):
25 | if ":" in target:
26 | project, path = target.split(":")
27 | return "https://{project}.readthedocs.io/en/latest/{path}".format(
28 | project=project, path=path
29 | )
30 | return f"https://{target}.readthedocs.io"
31 |
32 | return generic_url_role(name, text, url_handler)
33 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # flake8: NOQA: F401
2 |
3 | import pytest
4 |
5 | from django.apps import apps
6 |
7 |
8 | @pytest.fixture
9 | def EmptyModel(favicon_app):
10 | return favicon_app.get_model("EmptyModel")
11 |
12 |
13 | @pytest.fixture
14 | def Favicon(favicon_app):
15 | return favicon_app.get_model("Favicon")
16 |
17 |
18 | @pytest.fixture
19 | def RSTPost(favicon_app):
20 | return favicon_app.get_model("RSTPost")
21 |
22 |
23 | @pytest.fixture
24 | def RSTPostPage(favicon_app):
25 | return favicon_app.get_model("RSTPostPage")
26 |
27 |
28 | @pytest.fixture
29 | def favicon_app(settings, request):
30 | app_name = "test_app"
31 | app_import_string = f"django_docutils.favicon.tests.{app_name}"
32 |
33 | if app_import_string not in settings.INSTALLED_APPS:
34 | settings.INSTALLED_APPS = settings.INSTALLED_APPS + (app_import_string,)
35 |
36 | def resource_a_teardown():
37 | print("\nresources_a_teardown()")
38 | settings.INSTALLED_APPS = (
39 | s for s in settings.INSTALLED_APPS if s != app_import_string
40 | )
41 |
42 | assert app_import_string not in settings.INSTALLED_APPS
43 |
44 | request.addfinalizer(resource_a_teardown)
45 |
46 | return apps.get_app_config(app_name)
47 |
--------------------------------------------------------------------------------
/django_docutils/lib/directives/__init__.py:
--------------------------------------------------------------------------------
1 | from django.utils.module_loading import import_string
2 | from docutils.parsers.rst import directives
3 |
4 | from ..settings import BASED_LIB_RST
5 |
6 |
7 | def register_based_directives():
8 | """Register all directives, exists to avoid race conditions.
9 |
10 | Sometimes stuff like publish_parts can be ran from command line functions
11 | tests. There's also ways we could avoid this by placing it in __init__
12 | of django_docutils.lib, but that's a bit implicit. Investigate that later.
13 |
14 | In order to make this work across django projects, let's use django
15 | settings to register to them.
16 |
17 | Why? Not all django projects want code highlighting (which requires
18 | pygments). Let's use a TEMPLATES-style django config::
19 |
20 | BASED_LIB_RST = {
21 | 'directives': { #: directive-name: Directive class (import string)
22 | 'code-block': 'django_docutils.lib.directives.pygments.CodeBlock'
23 | }
24 | }
25 | """
26 | if not BASED_LIB_RST:
27 | return
28 |
29 | if "directives" in BASED_LIB_RST:
30 | for dir_name, dir_cls_str in BASED_LIB_RST["directives"].items():
31 | class_ = import_string(dir_cls_str)
32 | directives.register_directive(dir_name, class_)
33 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/github.py:
--------------------------------------------------------------------------------
1 | from .common import generic_url_role
2 |
3 |
4 | def github_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
5 | """Role for linking to GitHub repos and issues.
6 |
7 | :gh:`vim-airline` ->
8 | link: https://github.com/vim-airline
9 | text: vim-airline
10 |
11 |
12 | :gh:`vim-airline's org ` ->
13 | link: https://github.com/vim-airline
14 | text: vim-airline's org
15 |
16 | :gh:`vim-airline/vim-airline` ->
17 | link: https://github.com/vim-airline/vim-airline
18 | text: vim-airline/vim-airline
19 |
20 | :gh:`vim-airline/vim-airline#134` ->
21 | link: https://github.com/vim-airline/vim-airline/issues/134
22 | text: vim-airline/vim-airline#134
23 |
24 | :gh:`this example issue ` ->
25 | link: https://github.com/vim-airline/vim-airline/issues/134
26 | text: this example issue
27 | """
28 |
29 | def url_handler(target):
30 | if "#" in target:
31 | user_n_repo, issue = target.split("#")
32 | if issue.isnumeric():
33 | return f"https://github.com/{user_n_repo}/issues/{issue}"
34 |
35 | return f"https://github.com/{target}"
36 |
37 | return generic_url_role(name, text, url_handler)
38 |
--------------------------------------------------------------------------------
/django_docutils/lib/tests/test_writers.py:
--------------------------------------------------------------------------------
1 | from django.utils.encoding import force_bytes
2 | from docutils.core import publish_doctree
3 | from docutils.writers.html5_polyglot import Writer
4 |
5 | from django_docutils.lib.publisher import publish_parts_from_doctree
6 | from django_docutils.lib.settings import BASED_LIB_RST
7 | from django_docutils.lib.writers import BasedWriter
8 |
9 |
10 | def test_HTMLWriter_hides_docinfo():
11 | docutils_settings = BASED_LIB_RST.get("docutils", {})
12 |
13 | content = """
14 | ===========
15 | Hello world
16 | ===========
17 |
18 | :key1: value
19 | :key2: value
20 |
21 | more text
22 |
23 | first section
24 | -------------
25 |
26 | some content
27 | """.strip()
28 |
29 | doctree = publish_doctree(
30 | source=force_bytes(content), settings_overrides=docutils_settings
31 | )
32 |
33 | # Test that normal writer will show docinfo in HTML
34 | parts = publish_parts_from_doctree(
35 | doctree, writer=Writer(), settings_overrides=docutils_settings
36 | )
37 | assert "key1" in parts["html_body"]
38 |
39 | # Our writer should *not* output docinto
40 | parts = publish_parts_from_doctree(
41 | doctree, writer=BasedWriter(), settings_overrides=docutils_settings
42 | )
43 |
44 | assert "key1" not in parts["html_body"]
45 | assert "first section" in parts["html_body"]
46 |
--------------------------------------------------------------------------------
/django_docutils/references/management/commands/loaddata_intersphinx.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django.db import DEFAULT_DB_ALIAS, connections, transaction
3 |
4 | from django_docutils.references.intersphinx.load import load_mappings
5 |
6 |
7 | class Command(BaseCommand):
8 | help = "Installs / Updates growth rst file(s) in the database."
9 |
10 | def handle(self, **options):
11 | self.using = options["database"]
12 | with transaction.atomic(using=self.using):
13 | self.loaddata()
14 |
15 | # Close the DB connection -- unless we're still in a transaction. This
16 | # is required as a workaround for an edge case in MySQL: if the same
17 | # connection is used to create tables, load data, and query, the query
18 | # can return incorrect results. See Django #7572, MySQL #37735.
19 | if transaction.get_autocommit(self.using):
20 | connections[self.using].close()
21 |
22 | def add_arguments(self, parser):
23 | parser.add_argument(
24 | "--database",
25 | action="store",
26 | dest="database",
27 | default=DEFAULT_DB_ALIAS,
28 | help="Nominates a specific database to load fixtures into. "
29 | 'Defaults to the "default" database.',
30 | )
31 |
32 | def loaddata(self):
33 | load_mappings()
34 |
--------------------------------------------------------------------------------
/django_docutils/lib/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from django_docutils.lib.utils import chop_after_docinfo, chop_after_title
2 |
3 |
4 | def test_chop_after_title():
5 | content = """=============================================
6 | Learn JavaScript for free: The best resources
7 | =============================================
8 |
9 | first section
10 | -------------
11 |
12 | some content
13 | """.strip()
14 |
15 | result = chop_after_title(content)
16 |
17 | expected = """
18 | first section
19 | -------------
20 |
21 | some content""".strip()
22 |
23 | assert result == expected
24 |
25 |
26 | def test_chop_after_docinfo():
27 | before = """
28 | ===========
29 | Content ok!
30 | ===========
31 |
32 | :programming_languages: javascript
33 | :topics: webpack
34 | :Created: 2017-07-30
35 | :Author: tony
36 |
37 | more text
38 |
39 | first section
40 | -------------
41 |
42 | some content
43 | """.strip()
44 |
45 | after = """
46 | more text
47 |
48 | first section
49 | -------------
50 |
51 | some content
52 | """.strip()
53 |
54 | assert chop_after_docinfo(before) == after
55 |
56 | # test docinfo handles spaces in values
57 | assert (
58 | chop_after_docinfo(
59 | source="""
60 | ==============
61 | Document title
62 | ==============
63 | -----------------
64 | Document subtitle
65 | -----------------
66 |
67 | :Title: Overridden Title
68 | :Subtitle: Overridden Subtitle
69 |
70 | Content
71 | -------
72 |
73 | hi
74 | """.strip()
75 | )
76 | == """
77 | Content
78 | -------
79 |
80 | hi""".strip()
81 | )
82 |
--------------------------------------------------------------------------------
/django_docutils/lib/transforms/font_awesome.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from docutils import nodes, utils
4 | from docutils.transforms import Transform
5 |
6 | from django_docutils.lib.settings import BASED_LIB_RST
7 |
8 | url_patterns = BASED_LIB_RST.get("font_awesome", {}).get("url_patterns", {})
9 | permissible_nodes = [nodes.Text]
10 |
11 |
12 | def fa_classes_from_url(url: str) -> str:
13 | for url_pattern, classes in url_patterns.items():
14 | if re.match(url_pattern, url):
15 |
16 | return classes
17 |
18 | return ""
19 |
20 |
21 | def inject_font_awesome_to_ref_node(target: nodes.Node, url: str):
22 | fa_classes = fa_classes_from_url(url=url)
23 | if fa_classes != "":
24 | fa_tag = f''
25 | title = utils.unescape(target[0])
26 | rn = nodes.reference("", "", internal=True, refuri=url)
27 | rn += nodes.raw("", fa_tag, format="html")
28 | rn += target[0].__class__(title, title)
29 | return rn
30 | return None
31 |
32 |
33 | class InjectFontAwesome(Transform):
34 |
35 | default_priority = 680
36 |
37 | def apply(self):
38 | for target in self.document.traverse(nodes.reference):
39 | if target.hasattr("refuri") and any(
40 | isinstance(target[0], node_type) for node_type in permissible_nodes
41 | ):
42 | rn = inject_font_awesome_to_ref_node(
43 | target=target, url=target["refuri"]
44 | )
45 | if rn is not None:
46 | target.replace_self(rn)
47 |
--------------------------------------------------------------------------------
/docs/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-docutils",
3 | "short_name": "django-docutils",
4 | "description": "Helpers for interfacing between django and docutils",
5 | "theme_color": "#2196f3",
6 | "background_color": "#fff",
7 | "display": "browser",
8 | "Scope": "https://django-docutils.git-pull.com/",
9 | "start_url": "https://django-docutils.git-pull.com/",
10 | "icons": [
11 | {
12 | "src": "_static/img/icons/android-chrome-72x72.png",
13 | "sizes": "72x72",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "_static/img/icons/android-chrome-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "_static/img/icons/android-chrome-128x128.png",
23 | "sizes": "128x128",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "_static/img/icons/android-chrome-144x144.png",
28 | "sizes": "144x144",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "_static/img/icons/android-chrome-152x152.png",
33 | "sizes": "152x152",
34 | "type": "image/png"
35 | },
36 | {
37 | "src": "_static/img/icons/android-chrome-192x192.png",
38 | "sizes": "192x192",
39 | "type": "image/png"
40 | },
41 | {
42 | "src": "_static/img/icons/android-chrome-384x384.png",
43 | "sizes": "384x384",
44 | "type": "image/png"
45 | },
46 | {
47 | "src": "_static/img/icons/android-chrome-512x512.png",
48 | "sizes": "512x512",
49 | "type": "image/png"
50 | }
51 | ],
52 | "splash_pages": null
53 | }
54 |
--------------------------------------------------------------------------------
/docs/_static/django-docutils.css:
--------------------------------------------------------------------------------
1 | h1.logo {
2 | font-size: 20px;
3 | }
4 |
5 | div.sidebar {
6 | margin: 0px;
7 | border: 0px;
8 | padding: 0px;
9 | background-color: inherit;
10 | }
11 |
12 | div.sidebar .sidebar-title {
13 | display: none;
14 | }
15 |
16 | form.navbar-form {
17 | padding: 0px 10px;
18 | }
19 |
20 | div#changelog > div.section > ul > li > p:only-child {
21 | margin-bottom: 0;
22 | }
23 |
24 | div#text-based-window-manager {
25 | clear: both;
26 | }
27 |
28 | @media screen and (max-width: 768px) {
29 | #fork-gh img {
30 | display: none;
31 | }
32 | }
33 |
34 | table.docutils {
35 | background-color: #fafbfc;
36 | border: 0;
37 | }
38 |
39 | table.docutils td, table.docutils th {
40 | border: 0;
41 | }
42 |
43 | table.docutils pre {
44 | background-color: rgba(239, 242, 244, .75);
45 | }
46 |
47 | pre {
48 | background-color: #fafbfc;
49 | border-left: 5px solid #558abb;
50 | font-size: 0.75em;
51 | }
52 |
53 | div.seealso, div.note {
54 | background-color: #fafbfc;
55 | border-right: 0;
56 | border-top: 0;
57 | border-bottom: 0;
58 | }
59 |
60 | div.seealso {
61 | border-left: 5px solid #8abb55;
62 | }
63 |
64 | div.note {
65 | border-left: 5px solid #bb5557;
66 | }
67 |
68 | code.literal {
69 | font-size: 85%;
70 | color: #24292e;
71 | box-sizing: border-box;
72 | display: inline-block;
73 | padding: 0;
74 | background: #fafcfc;
75 | border: 1px solid #f0f4f7;
76 | line-height: 20px;
77 | }
78 |
79 | code::before, code::after {
80 | letter-spacing: -0.2em;
81 | content: "\00a0";
82 | }
83 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PY_FILES= find . -type f -not -path '*/\.*' | grep -i '.*[.]py$$' 2> /dev/null
2 | DOC_FILES= find . -type f -not -path '*/\.*' | grep -i '.*[.]rst\$\|.*[.]md\$\|.*[.]css\$\|.*[.]py\$\|mkdocs\.yml\|CHANGES\|TODO\|.*conf\.py' 2> /dev/null
3 | SHELL := /bin/bash
4 |
5 |
6 | entr_warn:
7 | @echo "----------------------------------------------------------"
8 | @echo " ! File watching functionality non-operational ! "
9 | @echo " "
10 | @echo "Install entr(1) to automatically run tasks on file change."
11 | @echo "See http://entrproject.org/ "
12 | @echo "----------------------------------------------------------"
13 |
14 | isort:
15 | poetry run isort `${PY_FILES}`
16 |
17 | black:
18 | poetry run black `${PY_FILES}`
19 |
20 | test:
21 | poetry run py.test $(test)
22 |
23 | start:
24 | $(MAKE) test; poetry run ptw .
25 |
26 | watch_test:
27 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) test; else $(MAKE) test entr_warn; fi
28 |
29 | build_docs:
30 | $(MAKE) -C docs html
31 |
32 | start_docs:
33 | $(MAKE) -C docs start
34 |
35 | design_docs:
36 | $(MAKE) -C docs design
37 |
38 | flake8:
39 | flake8 django_docutils tests
40 |
41 | watch_flake8:
42 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) flake8; else $(MAKE) flake8 entr_warn; fi
43 |
44 | mypy:
45 | poetry run mypy `${PY_FILES}`
46 |
47 | watch_mypy:
48 | if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) mypy; else $(MAKE) mypy entr_warn; fi
49 |
50 | format_markdown:
51 | prettier --parser=markdown -w *.md docs/*.md docs/**/*.md CHANGES
52 |
--------------------------------------------------------------------------------
/django_docutils/lib/transforms/toc.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from docutils import nodes
4 | from docutils.transforms import parts
5 |
6 |
7 | class Contents(parts.Contents):
8 |
9 | """
10 | Changes:
11 | - remove unused autonum
12 | - PEP8
13 | - Removed extra nodes.paragraph wrapping of list_item's
14 | """
15 |
16 | def build_contents(self, node, level=0):
17 | level += 1
18 | sections = [sect for sect in node if isinstance(sect, nodes.section)]
19 | entries = []
20 | depth = self.startnode.details.get("depth", sys.maxsize)
21 |
22 | for section in sections:
23 | title = section[0]
24 | auto = title.get("auto") # May be set by SectNum.
25 | entrytext = self.copy_and_filter(title)
26 | reference = nodes.reference("", "", refid=section["ids"][0], *entrytext)
27 | ref_id = self.document.set_id(reference)
28 | item = nodes.list_item("", reference)
29 | if (
30 | self.backlinks in ("entry", "top")
31 | and title.next_node(nodes.reference) is None
32 | ):
33 | if self.backlinks == "entry":
34 | title["refid"] = ref_id
35 | elif self.backlinks == "top":
36 | title["refid"] = self.toc_id
37 | if level < depth:
38 | subsects = self.build_contents(section, level)
39 | item += subsects
40 | entries.append(item)
41 | if entries:
42 | contents = nodes.bullet_list("", *entries, classes=["menu-list"])
43 |
44 | if auto:
45 | contents["classes"].append("auto-toc")
46 | return contents
47 | else:
48 | return []
49 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/process.py:
--------------------------------------------------------------------------------
1 | """Metadata is a catch-all term for information for an RST document.
2 |
3 | It can be pulled from the RST file itself, such as:
4 |
5 | - The Title/Subtitle of the document
6 | - The docinfo attributes
7 |
8 | In the case of directory-style projects, the manifest.json.
9 |
10 | These optional pipeline functions can be configured to to create, read,
11 | update, and delete metadata from RST projects.
12 |
13 | To set metadata processors, use BASED_LIB_RST['metadata_processors']::
14 |
15 | BASED_LIB_RST = {
16 | 'metadata_processors': [
17 | 'django_docutils.lib.metadata.processors.process_datetime'
18 | ],
19 | }
20 |
21 | The order of the processors will be respected.
22 |
23 | The function accepts one argument, the metadata dictionary, and returns the
24 | same dictionary::
25 |
26 | def process_datetime(metadata):
27 | # create, read, update, delete metadata from the RST document
28 | return metadata
29 |
30 | See *processors.py* for more examples.
31 | """
32 | from django.utils.module_loading import import_string
33 |
34 | from ..settings import BASED_LIB_RST
35 |
36 |
37 | def process_metadata(metadata):
38 | """Return objects from rst metadata pulled from document source.
39 |
40 | This will turn things like string dates into time-zone'd dateutil objects.
41 |
42 | :param metadata: data returned from processing an RST file
43 | :type metadata: dict
44 | :rtype: dict
45 | """
46 |
47 | if not BASED_LIB_RST:
48 | return metadata
49 |
50 | if "metadata_processors" in BASED_LIB_RST:
51 | for processor_str in BASED_LIB_RST["metadata_processors"]:
52 | processor_fn = import_string(processor_str)
53 | metadata = processor_fn(metadata)
54 |
55 | return metadata
56 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/processors.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import pytz
4 | from django.conf import settings
5 |
6 | from django_docutils.lib.fixtures.publisher import M2M_FIELDS
7 |
8 |
9 | def process_datetime(metadata):
10 | timezone_formats = [ # timezone formats to try, most detailed to least
11 | "%Y-%m-%d %I:%M%p",
12 | "%Y-%m-%d",
13 | ]
14 |
15 | for time_key in ["created", "modified"]:
16 | if time_key in metadata:
17 | for _format in timezone_formats:
18 | try:
19 | metadata[time_key] = datetime.datetime.strptime(
20 | metadata[time_key], _format
21 | )
22 | break
23 | except ValueError:
24 | continue
25 | metadata[time_key] = pytz.timezone(settings.TIME_ZONE).localize(
26 | metadata[time_key], is_dst=None
27 | )
28 | return metadata
29 |
30 |
31 | def process_anonymous_user(metadata):
32 | """Corrects name of author "anonymous" to django's anonymous username"""
33 |
34 | if metadata.get("author", None) == "anonymous":
35 | metadata["author"] = settings.ANONYMOUS_USER_NAME
36 |
37 | return metadata
38 |
39 |
40 | def process_m2m_fields(metadata):
41 | """Expand m2m values. When parsed from RST docinfo attributes, they're
42 | separated by commas: e.g. :Topic: web frameworks, django
43 |
44 | Directory-style imports skip this, json imports into list
45 | """
46 | for m2m_field in M2M_FIELDS:
47 | if m2m_field in metadata and isinstance(metadata[m2m_field], str):
48 | metadata[m2m_field] = metadata[m2m_field].split(",")
49 | if isinstance(metadata[m2m_field], str):
50 | metadata[m2m_field] = [metadata[m2m_field]]
51 | return metadata
52 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/tests/test_extract.py:
--------------------------------------------------------------------------------
1 | from django.utils.encoding import force_bytes
2 | from docutils.core import publish_doctree
3 |
4 | from django_docutils.lib.metadata.extract import (
5 | extract_metadata,
6 | extract_subtitle,
7 | extract_title,
8 | )
9 | from django_docutils.lib.settings import BASED_LIB_RST
10 |
11 |
12 | def test_extract_title():
13 | content = """
14 | ===========
15 | Hello world
16 | ===========
17 |
18 | :key1: value
19 | :key2: value
20 |
21 | more text
22 |
23 | first section
24 | -------------
25 |
26 | some content
27 | """.strip()
28 |
29 | doctree = publish_doctree(source=force_bytes(content))
30 |
31 | assert extract_title(doctree) == "Hello world"
32 |
33 |
34 | def test_extract_subtitle():
35 | content = """
36 | ===========
37 | Hello world
38 | ===========
39 | moo
40 | ===
41 |
42 | :key1: value
43 | :key2: value
44 |
45 | more text
46 |
47 | first section
48 | -------------
49 |
50 | some content
51 | """.strip()
52 |
53 | doctree = publish_doctree(source=force_bytes(content))
54 |
55 | assert extract_subtitle(doctree) == "moo"
56 |
57 |
58 | def test_extract_metadata(tmpdir):
59 | docutils_settings = BASED_LIB_RST.get("docutils", {})
60 | content = """
61 | ===========
62 | Content ok!
63 | ===========
64 |
65 | :programming_languages: javascript
66 | :topics: webpack
67 | :Created: 2017-07-30
68 | :Author: tony
69 |
70 | more text
71 |
72 | first section
73 | -------------
74 |
75 | some content
76 | """.strip()
77 |
78 | doctree = publish_doctree(
79 | source=force_bytes(content), settings_overrides=docutils_settings
80 | )
81 |
82 | assert extract_metadata(doctree) == {
83 | "programming_languages": "javascript",
84 | "topics": "webpack",
85 | "created": "2017-07-30",
86 | "author": "tony",
87 | }
88 |
--------------------------------------------------------------------------------
/docs/_templates/sidebar/projects.html:
--------------------------------------------------------------------------------
1 |
32 |
46 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/management/commands/loaddata_grow.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.core.management.base import BaseCommand
3 | from django.db import DEFAULT_DB_ALIAS, connections, transaction
4 |
5 | from django_docutils.lib.directives import register_based_directives
6 | from django_docutils.lib.fixtures.load import load_app_rst_fixtures
7 | from django_docutils.lib.fixtures.utils import find_app_configs_with_fixtures
8 | from django_docutils.lib.roles import register_based_roles
9 |
10 | User = get_user_model()
11 |
12 |
13 | class Command(BaseCommand):
14 | help = "Installs / Updates growth rst file(s) in the database."
15 |
16 | def handle(self, **options):
17 | self.using = options["database"]
18 | with transaction.atomic(using=self.using):
19 | self.loaddata()
20 |
21 | # Close the DB connection -- unless we're still in a transaction. This
22 | # is required as a workaround for an edge case in MySQL: if the same
23 | # connection is used to create tables, load data, and query, the query
24 | # can return incorrect results. See Django #7572, MySQL #37735.
25 | if transaction.get_autocommit(self.using):
26 | connections[self.using].close()
27 |
28 | def add_arguments(self, parser):
29 | parser.add_argument(
30 | "--database",
31 | action="store",
32 | dest="database",
33 | default=DEFAULT_DB_ALIAS,
34 | help="Nominates a specific database to load fixtures into. "
35 | 'Defaults to the "default" database.',
36 | )
37 |
38 | def loaddata(self):
39 | # prepare directives / roles
40 | register_based_directives()
41 | register_based_roles()
42 |
43 | # apps that have growth fixtures
44 | app_configs = find_app_configs_with_fixtures(has_rst_files=True)
45 |
46 | for app_config in app_configs:
47 | load_app_rst_fixtures(app_config=app_config)
48 |
--------------------------------------------------------------------------------
/django_docutils/lib/text.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django.conf import settings
4 | from django.utils.module_loading import import_string
5 |
6 | _word_re = re.compile(r"\w+", re.UNICODE)
7 | _word_beginning_split_re = re.compile(r"([\s\(\{\[\<]+)", re.UNICODE)
8 |
9 |
10 | def is_uncapitalized_word(value):
11 | """Return True if term/word segment is special uncap term (e.g. "django-")
12 |
13 | :param value: string value from template
14 | :type value: string
15 |
16 | Functions can be declared via BASED_TEXT in django settings via string
17 | imports. The filters accept one argument (the word). If you don't want the
18 | word/pattern capitalized, return True. Anything else capitalizes as normal.
19 |
20 | How to create filters:;
21 |
22 | def handle_uncapped_word(value):
23 | if value.startswith('django-'):
24 | return True
25 | if 'vs' in value:
26 | return True
27 | return False
28 |
29 | In your settings::
30 |
31 | BASED_LIB_TEXT = {
32 | 'uncapitalized_word_filters': [
33 | 'develtech.path.to.handle_uncapped_word'
34 | ]
35 | }
36 | """
37 | try:
38 | config = settings.BASED_LIB_TEXT
39 | except AttributeError:
40 | return
41 |
42 | if "uncapitalized_word_filters" in config:
43 | for filter_fn_str in config["uncapitalized_word_filters"]:
44 | filter_ = import_string(filter_fn_str)
45 | if filter_(value):
46 | return True
47 | return False
48 |
49 |
50 | def smart_capfirst(value):
51 | """Capitalize the first character of the value."""
52 |
53 | if is_uncapitalized_word(value):
54 | return value
55 |
56 | return value[0].upper() + value[1:]
57 |
58 |
59 | def smart_title(value):
60 | """Convert a string into titlecase, except for special cases.
61 |
62 | Django can still be capitalized, but it must already be like that.
63 | """
64 |
65 | return "".join(
66 | [smart_capfirst(item) for item in _word_beginning_split_re.split(value) if item]
67 | )
68 |
--------------------------------------------------------------------------------
/django_docutils/favicon/management/commands/prefetch_favicons.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django.db import DEFAULT_DB_ALIAS, connections, transaction
3 |
4 | from django_docutils.lib.favicon.prefetch import prefetch_favicons
5 |
6 | from ...models import get_favicon_model
7 |
8 |
9 | class Command(BaseCommand):
10 | help = "Parse all page/node content for favicons."
11 |
12 | def handle(self, **options):
13 | if options["clear"]:
14 | self.stdout.write("Wiping favicons")
15 | Favicon = get_favicon_model()
16 | for f in Favicon.objects.all():
17 | f.delete()
18 |
19 | self.using = options["database"]
20 | self.url_pattern = options["pattern"]
21 |
22 | with transaction.atomic(using=self.using):
23 | prefetch_favicons(self.url_pattern)
24 |
25 | # Close the DB connection -- unless we're still in a transaction. This
26 | # is required as a workaround for an edge case in MySQL: if the same
27 | # connection is used to create tables, load data, and query, the query
28 | # can return incorrect results. See Django #7572, MySQL #37735.
29 | if transaction.get_autocommit(self.using):
30 | connections[self.using].close()
31 |
32 | def add_arguments(self, parser):
33 | parser.add_argument(
34 | "-c",
35 | "--clear",
36 | action="store_true",
37 | dest="clear",
38 | help="Clear the existing favicons from the database"
39 | "before trying to fetch favicons.",
40 | )
41 |
42 | parser.add_argument(
43 | "--database",
44 | action="store",
45 | dest="database",
46 | default=DEFAULT_DB_ALIAS,
47 | help="Nominates a specific database to load fixtures into. "
48 | 'Defaults to the "default" database.',
49 | )
50 |
51 | parser.add_argument(
52 | "--pattern",
53 | action="store",
54 | dest="pattern",
55 | default=None,
56 | help="Only parse URL's with pattern",
57 | )
58 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/develtech.py:
--------------------------------------------------------------------------------
1 | from django.urls import NoReverseMatch, reverse
2 | from docutils import nodes, utils
3 |
4 | from ..utils import split_explicit_title
5 |
6 |
7 | def site_url_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
8 | name = name.lower()
9 |
10 | has_explicit_title, title, target = split_explicit_title(text)
11 | title = utils.unescape(title)
12 | target = utils.unescape(target)
13 |
14 | if not has_explicit_title:
15 | title = "Page " + utils.unescape(title)
16 | try:
17 | url = reverse(target)
18 | except NoReverseMatch:
19 | msg = inliner.reporter.error("no matching url %s" % target, line=lineno)
20 | prb = inliner.problematic(rawtext, rawtext, msg)
21 | return [prb], [msg]
22 | sn = nodes.Text(title, title)
23 | rn = nodes.reference("", "", internal=True, refuri=url, classes=[name])
24 | rn += sn
25 | return [rn], []
26 |
27 |
28 | def post_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
29 | from django.apps import apps
30 |
31 | from django_docutils.lib.fixtures.utils import get_model_from_post_app
32 |
33 | # split :post:appname:`post_id` into :post:appname:
34 | raw_role = rawtext.split("`")[0]
35 |
36 | app_name_singular = raw_role.split(":")[2]
37 | app_config = apps.get_app_config(f"{app_name_singular}s")
38 | Model = get_model_from_post_app(app_config)
39 |
40 | name = name.lower()
41 | has_explicit_title, title, target = split_explicit_title(text)
42 | title = utils.unescape(title)
43 | target = utils.unescape(target)
44 |
45 | try:
46 | obj = Model.objects.get(slug_id=target)
47 | url = obj.get_absolute_url()
48 | if not has_explicit_title:
49 | title = obj.title
50 | except (Model.DoesNotExist, NoReverseMatch) as e:
51 | msg = inliner.reporter.error(f"{e} ({target})", line=lineno)
52 | prb = inliner.problematic(rawtext, rawtext, msg)
53 | return [prb], [msg]
54 | sn = nodes.Text(title, title)
55 | rn = nodes.reference("", "", internal=True, refuri=url, classes=[name])
56 | rn += sn
57 | return [rn], []
58 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/tests/test_process.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from django.utils.encoding import force_bytes
4 | from docutils.core import publish_doctree
5 |
6 | from django_docutils.lib.metadata.extract import extract_metadata
7 | from django_docutils.lib.metadata.process import process_metadata
8 |
9 |
10 | def test_process_metadata_file():
11 | source = """
12 | ===========
13 | Content ok!
14 | ===========
15 |
16 | :programming_language: javascript
17 | :topic: webpack
18 | :Created: 2017-07-30
19 | :Author: tony
20 |
21 | more text
22 |
23 | first section
24 | -------------
25 |
26 | some content
27 | """.strip()
28 |
29 | doctree = publish_doctree(source=force_bytes(source))
30 |
31 | raw_metadata = extract_metadata(doctree)
32 |
33 | analyzed_metadata = process_metadata(raw_metadata.copy())
34 | assert set(raw_metadata.keys()) == set(analyzed_metadata.keys())
35 |
36 | assert isinstance(analyzed_metadata["created"], datetime.date)
37 |
38 | assert process_metadata(raw_metadata) != {
39 | "programming_languages": "javascript",
40 | "topics": "webpack",
41 | "created": "2017-07-30",
42 | "author": "tony",
43 | }
44 |
45 |
46 | def test_process_metadata_daytime_timezone():
47 | """Verify time of day and timezone (optional) work with dates."""
48 |
49 | source = """
50 | ===========
51 | Content ok!
52 | ===========
53 |
54 | :programming_language: javascript
55 | :topic: webpack
56 | :Created: 2017-07-30 2:30PM
57 | :Author: tony
58 |
59 | more text
60 |
61 | first section
62 | -------------
63 |
64 | some content
65 | """.strip()
66 |
67 | doctree = publish_doctree(source=force_bytes(source))
68 |
69 | raw_metadata = extract_metadata(doctree)
70 |
71 | analyzed_metadata = process_metadata(raw_metadata.copy())
72 | assert set(raw_metadata.keys()) == set(analyzed_metadata.keys())
73 |
74 | created = analyzed_metadata["created"]
75 |
76 | assert isinstance(created, datetime.date)
77 | assert created.year == 2017
78 | assert created.month == 7
79 | assert created.day == 30
80 | assert created.strftime("%I") == "02"
81 | assert created.strftime("%p") == "PM"
82 | assert created.minute == 30
83 |
--------------------------------------------------------------------------------
/django_docutils/engines.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import io
4 |
5 | from django.conf import settings
6 | from django.template.backends.base import BaseEngine
7 | from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy
8 | from django.template.engine import Engine
9 | from docutils import core
10 |
11 | from .directives import register_pygments_directive
12 |
13 | try:
14 | from django.template.base import TemplateDoesNotExist
15 | except ImportError: # >= 1.9
16 | from django.template.exceptions import TemplateDoesNotExist
17 |
18 |
19 | class Docutils(BaseEngine):
20 | app_dirname = "templates"
21 |
22 | def __init__(self, params):
23 | params = params.copy()
24 | self.options = params.pop("OPTIONS").copy()
25 | self.options.setdefault("debug", settings.DEBUG)
26 | self.options.setdefault("file_charset", settings.FILE_CHARSET)
27 | super(Docutils, self).__init__(params)
28 | self.engine = Engine(self.dirs, self.app_dirs, **self.options)
29 |
30 | def from_string(self, template_code):
31 | return DocutilsTemplate(template_code, self.options)
32 |
33 | def get_template(self, template_name):
34 | for template_file in self.iter_template_filenames(template_name):
35 | try:
36 | with io.open(template_file, encoding=settings.FILE_CHARSET) as fp:
37 | template_code = fp.read()
38 | except IOError:
39 | continue
40 |
41 | return DocutilsTemplate(template_code, self.options)
42 | else:
43 | raise TemplateDoesNotExist(template_name)
44 |
45 |
46 | class DocutilsTemplate(object):
47 | def __init__(self, source, options):
48 | self.source = source
49 | self.options = options
50 |
51 | def render(self, context=None, request=None):
52 | context = self.options
53 | if request is not None:
54 | context["request"] = request
55 | context["csrf_input"] = csrf_input_lazy(request)
56 | context["csrf_token"] = csrf_token_lazy(request)
57 | context = {"source": self.source, "writer_name": "html"}
58 |
59 | return core.publish_parts(**context)["html_body"]
60 |
61 |
62 | register_pygments_directive()
63 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import py
2 | import pytest
3 |
4 | from django.apps import apps
5 |
6 | from django_docutils.lib.fixtures.tests.conftest import create_bare_app
7 | from django_docutils.lib.fixtures.utils import (
8 | find_app_configs_with_fixtures,
9 | find_rst_files,
10 | find_rst_files_in_app,
11 | get_model_from_post_app,
12 | )
13 |
14 |
15 | @pytest.fixture(scope="function")
16 | def bare_app_config_with_empty_fixture_dir(tmpdir_factory, request, settings):
17 | tmpdir = tmpdir_factory.mktemp("bare_project")
18 |
19 | app_config = create_bare_app(tmpdir, request, settings, "app_with_empty_fixtures")
20 | sample_app_dir = py.path.local(app_config.path)
21 | sample_app_dir.ensure("fixtures", dir=True)
22 | yield app_config
23 |
24 |
25 | def test_find_rst_files(tmpdir):
26 | tmpdir.join("hi.rst").write("")
27 | tmpdir.join("not_at_rst_file.html").write("")
28 | tmpdir.mkdir("wat")
29 | tmpdir.join("wat/moo.rst").write("")
30 | tmpdir.join("wat/another.rst").write("")
31 |
32 | assert ["./hi.rst"] == find_rst_files(str(tmpdir))
33 |
34 |
35 | def test_find_app_configs_with_fixtures(sample_app_config):
36 | assert sample_app_config in find_app_configs_with_fixtures(has_rst_files=False)
37 |
38 |
39 | def test_find_app_configs_with_fixtures_with_rst_files(bare_app_config):
40 | assert bare_app_config not in find_app_configs_with_fixtures()
41 | assert bare_app_config not in find_app_configs_with_fixtures(has_rst_files=False)
42 | assert bare_app_config not in find_app_configs_with_fixtures(has_rst_files=True)
43 |
44 |
45 | def test_find_app_configs_with_fixtures_with_empty_fixtures_dir(
46 | bare_app_config_with_empty_fixture_dir,
47 | ):
48 | assert bare_app_config_with_empty_fixture_dir in find_app_configs_with_fixtures(
49 | has_rst_files=False
50 | )
51 | assert bare_app_config_with_empty_fixture_dir not in find_app_configs_with_fixtures(
52 | has_rst_files=True
53 | )
54 |
55 |
56 | def test_find_rst_files_in_app(sample_app_config):
57 | assert "./hi.rst" in find_rst_files_in_app(sample_app_config)
58 | assert "2017/06/04/moo.rst" not in find_rst_files_in_app(sample_app_config)
59 |
60 |
61 | def test_get_model_from_post_app():
62 | app = apps.get_app_config("test_app")
63 | assert get_model_from_post_app(app) == app.get_model("RSTPost")
64 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/amazon.py:
--------------------------------------------------------------------------------
1 | from urllib.error import HTTPError
2 |
3 | from bitly_api import bitly_api
4 | from django.conf import settings
5 |
6 | from django_docutils.references.models import get_reference_model
7 |
8 | from .common import generic_remote_url_role
9 |
10 | Reference = get_reference_model()
11 |
12 |
13 | def get_client():
14 | from amazon.api import AmazonAPI
15 |
16 | return AmazonAPI(
17 | settings.AMAZON_PRODUCT_API_ACCESS_KEY,
18 | settings.AMAZON_PRODUCT_API_SECRET_KEY,
19 | settings.AMAZON_ASSOCIATES_TRACKING_ID,
20 | )
21 |
22 |
23 | def amazon_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
24 | """Role for linking to amazon product.
25 |
26 | First, try to resolve via memoization (redis kv store).
27 | Second, try to resolve via database back-end.
28 | Finally, try to resolve via remote API call, which saves to
29 | the database of references.
30 |
31 | In the future, create a wrapper / function / decorator to automate this
32 | for remote API roles.
33 |
34 | :amzn:`B01MG342KU` ->
35 | link: amazn shortline
36 | text: The Tao of tmux: and Terminal Tricks
37 |
38 | :amzn:`my book ` ->
39 | link: amzn shortlink
40 | text: my book
41 | """
42 | amzn = get_client()
43 |
44 | def url_handler(target):
45 | try:
46 | r = Reference.objects.get(project="amazon", target=target)
47 | except Reference.DoesNotExist:
48 | query = amzn.lookup(ItemId=target)
49 | url = query.offer_url
50 |
51 | access_token = settings.BITLY_ACCESS_TOKEN
52 | bitly = bitly_api.Connection(access_token=access_token)
53 | url = bitly.shorten(url)["url"]
54 |
55 | r = Reference(
56 | project="amazon",
57 | type=name,
58 | target=target,
59 | display_name=query.title,
60 | uri=url,
61 | )
62 | r.save()
63 | return r.display_name, r.uri
64 |
65 | try:
66 | return generic_remote_url_role(name, text, url_handler)
67 | except HTTPError:
68 | msg = inliner.reporter.error(
69 | 'Error connecting to amazon API for "%s"' % text, line=lineno
70 | )
71 | prb = inliner.problematic(rawtext, rawtext, msg)
72 | return [prb], [msg]
73 |
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## django-docutils 0.6.0 (2022-03-27)
4 |
5 | ### Compatibility
6 |
7 | - Drop python 3.5 to 3.7 (minimum version python 3.8)
8 | - Minimum django version 3.0
9 |
10 | ### Development
11 |
12 | - CI:
13 | - Fix CI variables
14 | - Rename Publish Docs -> docs
15 | - Fix poetry installation and caching
16 | - Update development packages (black, isort)
17 | - Add .tool-versions, .python-version
18 | - Run code through black w/o `--skip-string-normalization`
19 |
20 | ### Documentation
21 |
22 | - Move theme to furo
23 | - Move to markdown
24 |
25 | ## django-docutils 0.5.1 (2020-08-09)
26 |
27 | - [#270](https://github.com/tony/django-docutils/pull/270) Fix packaging / optional dependencies
28 | - Remove twine extra dependency
29 | - Remove setup.py (in favor of poetry, which we used to build and publish as of django-docutils
30 | 0.5.0)
31 |
32 | ## django-docutils 0.5.0 (2020-08-08)
33 |
34 | - Use our new bitly api fork at /
35 |
36 | - pipenv -> poetry
37 | - readthedocs -> self-hosted docs
38 | - travis -> github actions
39 | - Add black + isort and format code with it
40 | - Remove vulture
41 | - Remove python 2.x support, python 3.3 and 3.4 (reached end of life)
42 | - Update to new django versions (2.2 and 2.3)
43 | - Remove unsupported django versions (1.8, 1.9, 1.10, 2.0, 2.1)
44 | - Cleanup CI, add caching
45 | - Add `InjectFontAwesome` transformer to inject icon `` tags for font awesome based on regex
46 | patterns
47 | - Additional support for detecting font-awesome patterns and injecting the icon in other
48 | transformers (e.g. `XRefTransform`)
49 |
50 | ## django-docutils 0.4.0 (2017-02-21)
51 |
52 | - Django template tag
53 | - Some README documentation
54 |
55 | ## django-docutils 0.3.4 (2017-02-12)
56 |
57 | - Add requirements/test.txt to manifest
58 |
59 | ## django-docutils 0.3.3 (2017-02-12)
60 |
61 | - Add requirements/base.txt to manifest
62 |
63 | ## django-docutils 0.3.2 (2017-02-12)
64 |
65 | - Another tweak to get pypi readme up
66 |
67 | ## django-docutils 0.3.1 (2017-02-12)
68 |
69 | - Some changes to attempt to fix pypi README
70 |
71 | ## django-docutils 0.3.0 (2017-02-12)
72 |
73 | - Package updates and fixes
74 |
75 | ## django-docutils 0.2.0 (2017-01-01)
76 |
77 | - Support for Django 1.10.0
78 |
79 | ## django-docutils 0.1.0 (2015-06-20)
80 |
81 | - First release on PyPI.
82 |
83 |
86 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/file.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from docutils import nodes, utils
4 |
5 |
6 | def file_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
7 | """Role for files.
8 |
9 | :file:`./path/to/moo` ->
10 | text: ./path/to/moo (italicized + file icon)
11 |
12 | :file:`./path/to/moo/` ->
13 | text: ./path/to/moo/ (italicized + directory icon)
14 |
15 | """
16 | name = name.lower()
17 | title = utils.unescape(text)
18 |
19 | # 'file' would collide with bulma, so we use 'filepath'
20 | # https://github.com/jgthms/bulma/blob/c2fae71/sass/elements/form.sass#L218
21 | # https://github.com/jgthms/bulma/issues/1442
22 | classes = []
23 |
24 | # add .fa class since this isn't a link
25 | classes.append("far")
26 |
27 | if title.endswith("/"):
28 | classes.append("fa-folder")
29 | else:
30 | classes.append("fa-file-alt")
31 | extension = os.path.splitext(title)[1]
32 | if extension:
33 | classes.append(extension.lstrip("."))
34 |
35 | sn = nodes.emphasis(title, title)
36 |
37 | # insert inside the
38 | sn.insert(0, nodes.inline("", "", classes=classes))
39 | return [sn], []
40 |
41 |
42 | # TODO: Let font-awesome classes be configured via settings
43 | def manifest_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
44 | """Role for manifests (package.json, file outputs)
45 |
46 | :manifest:`package.json` ->
47 | text: package.json (italicized + file icon)
48 |
49 | """
50 | name = name.lower()
51 | title = utils.unescape(text)
52 |
53 | classes = ["manifest"]
54 |
55 | # add .fa class since this isn't a link
56 | classes.append("fa-file-alt far")
57 |
58 | sn = nodes.emphasis(title, title)
59 |
60 | # insert inside the
61 | sn.insert(0, nodes.inline("", "", classes=classes))
62 | return [sn], []
63 |
64 |
65 | def exe_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
66 | """Role for executables.
67 |
68 | :exe:`./path/to/webpack` ->
69 | text: ./path/to/webpack (italicized + file icon)
70 |
71 | """
72 | name = name.lower()
73 | title = utils.unescape(text)
74 |
75 | classes = ["exe", "fa"]
76 |
77 | sn = nodes.emphasis(title, title)
78 |
79 | # insert inside the
80 | sn.insert(0, nodes.inline("", "", classes=classes))
81 | return [sn], []
82 |
--------------------------------------------------------------------------------
/django_docutils/lib/metadata/extract.py:
--------------------------------------------------------------------------------
1 | from django.template.defaultfilters import truncatewords
2 | from django.utils.html import strip_tags
3 | from docutils import nodes
4 |
5 |
6 | def extract_title(document):
7 | """Return the title of the document.
8 |
9 | :param document:
10 | :type document: :class:`docutils.nodes.document`
11 | """
12 | for node in document.traverse(nodes.PreBibliographic):
13 | if isinstance(node, nodes.title):
14 | return node.astext()
15 |
16 |
17 | def extract_metadata(document):
18 | """Return the dict containing document metadata.
19 |
20 | :param document:
21 | :type document: :class:`docutils.nodes.document`
22 | :returns: docinfo data from document
23 | :rtype: dict
24 |
25 | From: https://github.com/adieu/mezzanine-cli @ mezzanine_cli/parser.py
26 | License: BSD (https://github.com/adieu/mezzanine-cli/blob/master/setup.py)
27 | """
28 | output = {}
29 | for docinfo in document.traverse(nodes.docinfo):
30 | for element in docinfo.children:
31 | if element.tagname == "field": # custom fields (e.g. summary)
32 | name_elem, body_elem = element.children
33 | name = name_elem.astext()
34 | value = body_elem.astext()
35 | else: # standard fields (e.g. address)
36 | name = element.tagname
37 | value = element.astext()
38 | name = name.lower()
39 |
40 | output[name] = value
41 | return output
42 |
43 |
44 | def extract_subtitle(document):
45 | """Return the subtitle of the document."""
46 | for node in document.traverse(nodes.PreBibliographic):
47 | if isinstance(node, nodes.subtitle):
48 | return node.astext()
49 |
50 |
51 | def extract_abstract(doctree, length=100):
52 | """Pull first n words from a docutils document.
53 |
54 | We use this to create snippets for Twitter Cards, FB, etc.
55 |
56 | :param doctree: docutils document to extract from
57 | :type doctree: :class:`docutils.nodes.document`
58 | :param length: word count to cut content off at
59 | :type length: int
60 | :rtype: string
61 | :returns: truncated content, html tags removed
62 |
63 | """
64 | paragraph_nodes = doctree.traverse(nodes.paragraph)
65 | text = ""
66 | for node in paragraph_nodes:
67 | text += node.astext()
68 | if len(text.split(" ")) > 100:
69 | break
70 | return truncatewords(strip_tags(text), 100)
71 |
--------------------------------------------------------------------------------
/django_docutils/favicon/scrape.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import logging
3 | import sys
4 | from urllib.parse import urljoin
5 |
6 | import requests
7 | from lxml import html
8 | from six.moves.urllib.parse import urlparse
9 |
10 | from django_docutils.exc import BasedException
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | def _request_favicon(url):
16 | """Tries to download favicon from URL and checks if it's valid."""
17 | r = requests.get(url)
18 | r.raise_for_status()
19 | if "image" not in r.headers["Content-Type"]:
20 | raise BasedException("Not an image")
21 | return r.content
22 |
23 |
24 | def get_favicon(url):
25 | try:
26 | r = requests.get(url)
27 | r.raise_for_status()
28 | # update url if redirected
29 | if r.url != url:
30 | url = r.url
31 | doc = html.fromstring(r.content)
32 | except requests.exceptions.ConnectionError as e:
33 | raise BasedException(f"The website {url} isn't connecting:", e)
34 |
35 | paths = ['//link[@rel="shortcut icon"]/@href', '//link[@rel="icon"]/@href']
36 | for path in paths:
37 | # Method 1: to find favicon via "shortcut icon"
38 | favicons = doc.xpath(path)
39 |
40 | if len(favicons): # Is pattern found?
41 | try:
42 | favicon_url = favicons[0]
43 | favicon_url = urljoin(url, favicon_url)
44 | return _request_favicon(favicon_url)
45 | except Exception as e:
46 | logger.debug(
47 | "Could not retrieve {favicon_url}: \n{e}".format(
48 | favicon_url=favicon_url, e=e
49 | )
50 | )
51 |
52 | # Method 2: site root/favicon.ico
53 | try:
54 | parsed = urlparse(url)
55 | parsed = parsed._replace(path="/favicon.ico")
56 | favicon_url = parsed.geturl()
57 | return _request_favicon(favicon_url)
58 | except Exception as e:
59 | logger.debug(
60 | "Could not retrieve {favicon_url}.\n{e}".format(
61 | favicon_url=favicon_url, e=e
62 | )
63 | )
64 |
65 | raise BasedException(
66 | """
67 | Could not retrieve favicon for {url}. Both strategies failed
68 | """.format(
69 | url=url
70 | )
71 | )
72 |
73 |
74 | if __name__ == "__main__":
75 | favicon = get_favicon(sys.argv[1])
76 | file_ = open("/Users/me/favicon.ico", "wb")
77 | file_.write(favicon)
78 | file_.close()
79 |
--------------------------------------------------------------------------------
/django_docutils/lib/templatetags/rst.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.base import kwarg_re
3 | from django.template.defaulttags import Node
4 | from django.template.exceptions import TemplateSyntaxError
5 |
6 | from ..publisher import publish_html_from_source
7 |
8 | register = template.Library()
9 |
10 |
11 | class ReStructuredTextNode(Node):
12 |
13 | """Implement the actions of the rst tag."""
14 |
15 | def __init__(self, content, args, kwargs, asvar):
16 | self.content = content
17 | self.args = args
18 | self.kwargs = kwargs
19 | self.asvar = asvar
20 |
21 | def render(self, context):
22 | args = [arg.resolve(context) for arg in self.args]
23 | kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
24 |
25 | content = self.content.resolve(context)
26 |
27 | return publish_html_from_source(content, *args, **kwargs)
28 |
29 |
30 | @register.tag
31 | def restructuredtext(parser, token):
32 | """Parse raw reStructuredText into HTML. Supports keyword arguments!
33 |
34 | Usage::
35 |
36 | {% restructuredtext content %}
37 |
38 | {% restructuredtext content inject_ads=False %}
39 |
40 | {% restructuredtext content toc_only=True %}
41 |
42 | {% restructuredtext content show_title=False %}
43 |
44 | {% restructuredtext content inject_ads=True ad_keywords=ad_keywords %}
45 |
46 | Why does toc_only=true needed (why do you need to call twice just to get
47 | a ToC)? Because of how docutils parses.
48 |
49 | Passing content/params right into publish_html_from_source.
50 | """
51 | bits = token.split_contents()
52 | if len(bits) < 2:
53 | raise TemplateSyntaxError(
54 | "'%s' takes at least one argument, a URL pattern name." % bits[0]
55 | )
56 | content = parser.compile_filter(bits[1])
57 | args = []
58 | kwargs = {}
59 | asvar = None
60 | bits = bits[2:]
61 | if len(bits) >= 2 and bits[-2] == "as":
62 | asvar = bits[-1]
63 | bits = bits[:-2]
64 |
65 | if len(bits):
66 | for bit in bits:
67 | match = kwarg_re.match(bit)
68 | if not match:
69 | raise TemplateSyntaxError("Malformed arguments to url tag")
70 | name, value = match.groups()
71 | if name:
72 | kwargs[name] = parser.compile_filter(value)
73 | else:
74 | args.append(parser.compile_filter(value))
75 | return ReStructuredTextNode(content, args, kwargs, asvar)
76 |
--------------------------------------------------------------------------------
/docs/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {# Import the theme's layout. #}
2 | {% extends "!layout.html" %}
3 |
4 | {# Include our new CSS file into existing ones. #}
5 | {% set css_files = css_files + ['_static/django-docutils.css']%}
6 |
7 |
8 | {%- block extrahead %}
9 | {{ super() }}
10 | {%- if theme_show_meta_manifest_tag == true %}
11 |
12 | {% endif -%}
13 | {%- if theme_show_meta_og_tags == true %}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {% endif -%}
30 | {%- if theme_show_meta_app_icon_tags == true %}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endif -%}
51 | {% endblock %}
52 |
--------------------------------------------------------------------------------
/django_docutils/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import django
4 | from django.core.exceptions import ImproperlyConfigured
5 | from django.template.loader import select_template
6 | from django.template.response import TemplateResponse
7 | from django.views.generic.base import TemplateView
8 |
9 |
10 | class DocutilsResponse(TemplateResponse):
11 |
12 | template_name = "base.html"
13 |
14 | def __init__(
15 | self,
16 | request,
17 | template,
18 | rst,
19 | context=None,
20 | content_type=None,
21 | status=None,
22 | charset=None,
23 | using=None,
24 | ):
25 | self.rst_name = rst
26 | super(DocutilsResponse, self).__init__(
27 | request, template, context, content_type, status, charset, using
28 | )
29 |
30 | @property
31 | def rendered_content(self):
32 | """Return the freshly rendered content for the template and context
33 | described by the TemplateResponse.
34 |
35 | This *does not* set the final content of the response. To set the
36 | response content, you must either call render(), or set the
37 | content explicitly using the value of this property.
38 | """
39 |
40 | if django.VERSION < (1, 10):
41 | context = self._resolve_context(self.context_data)
42 | else:
43 | context = self.resolve_context(self.context_data)
44 |
45 | # we should be able to use the engine to .Render this
46 | from django.utils.safestring import mark_safe
47 |
48 | context["content"] = mark_safe(
49 | select_template(self.rst_name, using="docutils").render()
50 | )
51 |
52 | if django.VERSION < (1, 10):
53 | template = self._resolve_template(self.template_name)
54 | else:
55 | template = self.resolve_template(self.template_name)
56 | content = template.render(context, self._request)
57 | return content
58 |
59 |
60 | class DocutilsView(TemplateView):
61 | response_class = DocutilsResponse
62 | rst_name = None
63 |
64 | def render_to_response(self, context, **response_kwargs):
65 | """Override to pay in rst content."""
66 | return self.response_class(
67 | request=self.request,
68 | template=self.get_template_names(),
69 | rst=self.get_rst_names(),
70 | context=context,
71 | using=self.template_engine,
72 | **response_kwargs
73 | )
74 |
75 | def get_rst_names(self):
76 | """
77 | Follows after get_template_names, but for scanning for rst content.
78 | """
79 | if self.rst_name is None:
80 | raise ImproperlyConfigured(
81 | "DocutilsView requires either a definition of "
82 | "'rst_name' or an implementation of 'get_rst_names()'"
83 | )
84 | else:
85 | return [self.rst_name]
86 |
--------------------------------------------------------------------------------
/django_docutils/favicon/rst/transforms/favicon.py:
--------------------------------------------------------------------------------
1 | import tldextract
2 | from django.db.models import Q
3 | from docutils import nodes
4 | from docutils.transforms import Transform
5 |
6 | from django_docutils.favicon.models import get_favicon_model
7 |
8 | from ..nodes import icon
9 |
10 | Favicon = get_favicon_model()
11 |
12 |
13 | def resolve_favicon(url):
14 | """Given a URL to a website, see if a Favicon exists in db.
15 |
16 | URL will be resolved to a fqdn for a key lookup.
17 |
18 | :param url: URL to any page on a website
19 | :type url: str
20 | :returns: Full Storage based favicon url path, or None
21 | :rtype: str|None
22 | """
23 | # e.g. forums.bbc.co.uk
24 | fqdn = tldextract.extract(url).fqdn
25 |
26 | try:
27 | return Favicon.objects.get(domain=fqdn).favicon.url
28 | except (ValueError, Favicon.DoesNotExist):
29 | return None
30 |
31 |
32 | class FaviconTransform(Transform):
33 | #: run after based.app.references.rst.transforms.xref
34 | default_priority = 20
35 |
36 | def apply(self):
37 | q = Q()
38 |
39 | # first run, iterate through references, extract FQDN's, add to query
40 | for node in self.document.traverse(plain_references):
41 | q.add(Q(domain__exact=tldextract.extract(node["refuri"]).fqdn), Q.OR)
42 |
43 | # pull all fqdn's with a favicon
44 | favicons = Favicon.objects.filter(q)
45 |
46 | for node in self.document.traverse(plain_references):
47 | fqdn = tldextract.extract(node["refuri"]).fqdn
48 | try:
49 | favicon_url = next( # Find favicon matching fqdn
50 | (f.favicon.url for f in favicons if f.domain == fqdn), None
51 | )
52 | except ValueError: # no favicon exists for fqdn
53 | favicon_url = None
54 |
55 | if favicon_url:
56 | nodecopy = node.deepcopy()
57 | ico = icon(
58 | "",
59 | "",
60 | style=f"background-image: url({favicon_url})",
61 | classes=["ico"],
62 | )
63 | nodecopy.insert(0, ico)
64 | node.replace_self(nodecopy)
65 |
66 |
67 | def plain_references(node):
68 | """Docutils traversal: Only return references with URI's, skip xref's
69 |
70 | If a nodes.reference already has classes, it's an icon class from xref,
71 | so skip that.
72 |
73 | If a nodes.reference has no 'refuri', it's junk, skip.
74 |
75 | Docutils node.traverse condition callback
76 |
77 | :returns: True if it's a URL we want to lookup favicons for
78 | :rtype: bool
79 | """
80 | if isinstance(node, nodes.reference):
81 | # skip nodes already with xref icon classes or no refuri
82 | no_classes = "classes" not in node or not node["classes"]
83 | has_refuri = "refuri" in node
84 | if no_classes and has_refuri and node["refuri"].startswith("http"):
85 | return True
86 | return False
87 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: [ '3.x' ]
11 | django-version: [ '3.0', '3.1', '3.2', '4.0' ]
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v3
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 |
19 | - name: Get full Python version
20 | id: full-python-version
21 | shell: bash
22 | run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")
23 |
24 | - name: Install poetry
25 | run: |
26 | curl -O -sSL https://install.python-poetry.org/install-poetry.py
27 | python install-poetry.py -y --version 1.1.12
28 | echo "PATH=${HOME}/.poetry/bin:${PATH}" >> $GITHUB_ENV
29 | rm install-poetry.py
30 |
31 | - name: Add ~/.local/bin to PATH
32 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH
33 |
34 | - name: Get poetry cache paths from config
35 | run: |
36 | echo "poetry_virtualenvs_path=$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV
37 | echo "poetry_virtualenvs_path=$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV
38 |
39 | - name: Configure poetry
40 | shell: bash
41 | run: poetry config virtualenvs.in-project true
42 |
43 | - name: Set up cache
44 | uses: actions/cache@v3
45 | id: cache
46 | with:
47 | path: |
48 | .venv
49 | ${{ env.poetry_cache_dir }}
50 | ${{ env.poetry_virtualenvs_path }}
51 | key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
52 |
53 | - name: Ensure cache is healthy
54 | if: steps.cache.outputs.cache-hit == 'true'
55 | shell: bash
56 | run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv
57 |
58 | - name: Install dependencies
59 | run: |
60 | poetry install -E "docs test coverage lint format favicon"
61 | poetry run pip install DJANGO~=${{ matrix.django-version }}
62 |
63 | - name: Lint with flake8
64 | run: poetry run flake8
65 |
66 | - name: Test with pytest
67 | run: poetry run py.test --cov=./ --cov-report=xml
68 |
69 | - uses: codecov/codecov-action@v2
70 | with:
71 | token: ${{ secrets.CODECOV_TOKEN }}
72 |
73 | - name: Build package
74 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
75 | run: poetry build
76 |
77 | - name: Publish package
78 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
79 | uses: pypa/gh-action-pypi-publish@release/v1
80 | with:
81 | user: __token__
82 | password: ${{ secrets.PYPI_API_TOKEN }}
83 | skip_existing: true
84 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/post_page.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.core.exceptions import ObjectDoesNotExist
3 | from django.db import models
4 | from django.urls import reverse
5 | from django.urls.exceptions import NoReverseMatch
6 | from django.utils.functional import cached_property
7 | from django.utils.translation import gettext_lazy as _
8 |
9 | from django_docutils.lib.publisher import publish_doctree
10 |
11 |
12 | def get_postpage_models():
13 | """Return high-level PageBase models. Highest level PostPage."""
14 | models = []
15 | for model in apps.get_models():
16 | if issubclass(model, RSTPostPageBase) and not model.__subclasses__():
17 | models.append(model)
18 | return models
19 |
20 |
21 | class RSTPostPageBase(models.Model):
22 |
23 | """Where the content of a post resides. Posts can have multiple pages."""
24 |
25 | body = models.TextField()
26 | subtitle = models.CharField(_("sub title"), max_length=255, null=True, blank=True)
27 | page_number = models.PositiveSmallIntegerField(null=True)
28 |
29 | class Meta:
30 | abstract = True
31 |
32 | def __str__(self):
33 | if self.subtitle:
34 | return "{title}: {subtitle}".format(
35 | title=self.post.title, subtitle=self.subtitle
36 | )
37 | return self.post.title
38 |
39 | @cached_property
40 | def title(self):
41 | return self.post.title
42 |
43 | @cached_property
44 | def previous_page(self):
45 | try:
46 | return self.post.pages.get(page_number=self.page_number - 1)
47 | except ObjectDoesNotExist:
48 | return None
49 |
50 | @cached_property
51 | def next_page(self):
52 | try:
53 | return self.post.pages.get(page_number=self.page_number + 1)
54 | except ObjectDoesNotExist:
55 | return None
56 |
57 | def get_absolute_url(self):
58 | try:
59 | return reverse(
60 | f"{self.post_url_key}:detail-view",
61 | kwargs={
62 | "slug_id": self.post.slug_id,
63 | "slug_title": self.post.slug_title,
64 | "page": self.page_number,
65 | },
66 | )
67 | except NoReverseMatch:
68 | return reverse(
69 | f"{self.post_url_key}:detail-view",
70 | kwargs={"slug_title": self.post.slug_title, "page": self.page_number},
71 | )
72 |
73 | @cached_property
74 | def post_url_key(self):
75 | return self.post.__class__.__name__.lower() + "s"
76 |
77 | @cached_property
78 | def document(self):
79 | """Return page content in a docutils' document
80 |
81 | :rtype: :class:`docutils.nodes.document`
82 | """
83 | return publish_doctree(self.body)
84 |
85 | def get_subclass(self):
86 | return self.post._meta.concrete_model.objects.get_subclass(pk=self.pk)
87 |
88 | @classmethod
89 | def check(cls, **kwargs):
90 | from .checks import _check_postpage_post_back_relation
91 |
92 | errors = super().check(**kwargs)
93 | errors.extend(_check_postpage_post_back_relation(cls))
94 | return errors
95 |
--------------------------------------------------------------------------------
/django_docutils/lib/transforms/ads.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from django.conf import settings
4 | from docutils import nodes
5 | from docutils.transforms import Transform
6 |
7 | from django_docutils.lib.utils import append_html_to_node, find_root_sections
8 |
9 | """
10 | Ideas:
11 |
12 | Create a generic Ad class that can counter the number of times it's
13 | posted, and render itself. It can be used to wrap Google and Amazon ads.
14 |
15 | If content is < 1000 TOTAL across all sections, show NO ads whatsoever
16 |
17 | """
18 |
19 |
20 | class InjectAds(Transform):
21 |
22 | """Add AdSense Javascript after the second nodes.section in Writer.
23 |
24 | Finding the node:
25 |
26 | 1. First preference, use the second node (usually it picks this)
27 | 2. But if the node's text ends with a :, continue to traverse nodes
28 | """
29 |
30 | default_priority = 100
31 |
32 | #: list of keywords for the ad system, default 'linux'
33 | ad_keywords = ["linux"]
34 |
35 | #: minimum amount of chars in a section to show and add
36 | #: https://support.google.com/adsense/answer/1346295?hl=en#Ad_limit_per_page # NOQA
37 | ad_section_length_min = 1000
38 |
39 | #: minimum content on page to show an ad
40 | #: showing ads where there isn't enough content sucks
41 | ad_page_length_min = ad_section_length_min
42 |
43 | #: max ads per page
44 | ads_max = 3
45 |
46 | @classmethod
47 | def keywords(cls, ad_keywords):
48 | if ad_keywords:
49 | cls.ad_keywords = ad_keywords
50 | return cls
51 |
52 | def apply(self):
53 | section_nodes = list(find_root_sections(self.document))
54 |
55 | ads_posted = 0
56 |
57 | page_length = 0
58 |
59 | for node in section_nodes:
60 | # Don't show Google ads on sections that are too small
61 | section_length = 0
62 | for p in node.traverse(nodes.paragraph):
63 | section_length += len(p.astext())
64 |
65 | # add this section size to the current page length
66 | page_length += section_length
67 |
68 | if section_length < self.ad_section_length_min:
69 | continue
70 |
71 | ad_code = random.choice(
72 | [
73 | settings.BASED_ADS["GOOGLE_AD_CODE"],
74 | settings.BASED_ADS["AMAZON_AD_STRIP"],
75 | ]
76 | )
77 | # ad_code = settings.BASED_ADS['AMZN_AD_CODE'].format(
78 | # keyword=self.ad_keywords[0]
79 | # )
80 |
81 | # append node to end of section
82 | append_html_to_node(node, ad_code)
83 | ads_posted += 1
84 |
85 | if ads_posted >= self.ads_max:
86 | break
87 |
88 | # if no ads posted, inject in last section
89 | if not ads_posted and page_length > self.ad_page_length_min:
90 | try:
91 | last_section = section_nodes[-1]
92 | except IndexError: # there's no sections after main title
93 | last_section = self.document
94 |
95 | node = last_section[-1] # end of section
96 |
97 | append_html_to_node(node, settings.BASED_ADS["GOOGLE_AD_CODE"])
98 |
--------------------------------------------------------------------------------
/django_docutils/lib/directives/code.py:
--------------------------------------------------------------------------------
1 | """
2 | The Pygments reStructuredText directive
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
5 | This fragment is a Docutils_ 0.5 directive that renders source code
6 | (to HTML only, currently) via Pygments.
7 |
8 | To use it, adjust the options below and copy the code into a module
9 | that you import on initialization. The code then automatically
10 | registers a ``sourcecode`` directive that you can use instead of
11 | normal code blocks like this::
12 |
13 | .. sourcecode:: python
14 |
15 | My code goes here.
16 |
17 | If you want to have different code styles, e.g. one with line numbers
18 | and one without, add formatters with their names in the VARIANTS dict
19 | below. You can invoke them instead of the DEFAULT one by using a
20 | directive option::
21 |
22 | .. sourcecode:: python
23 | :linenos:
24 |
25 | My code goes here.
26 |
27 | Look at the `directive documentation`_ to get all the gory details.
28 |
29 | .. _Docutils: http://docutils.sf.net/
30 | .. _directive documentation:
31 | http://docutils.sourceforge.net/docs/howto/rst-directives.html
32 |
33 | :copyright: Copyright 2006-2015 by the Pygments team, see AUTHORS.
34 | :license: BSD, see LICENSE for details.
35 | """
36 |
37 | import re
38 |
39 | from docutils import nodes
40 | from docutils.parsers.rst import Directive, directives
41 | from pygments import highlight
42 | from pygments.formatters.html import HtmlFormatter
43 | from pygments.lexers import get_lexer_by_name
44 | from pygments.lexers.shell import BashSessionLexer
45 | from pygments.lexers.special import TextLexer
46 |
47 | #: Monkey patch Bash Session lexer to gobble up initial space after prompt
48 | BashSessionLexer._ps1rgx = re.compile(
49 | r"^((?:(?:\[.*?\])|(?:\(\S+\))?(?:| |sh\S*?|\w+\S+[@:]\S+(?:\s+\S+)"
50 | r"?|\[\S+[@:][^\n]+\].+))\s*[$#%] )(.*\n?)"
51 | )
52 |
53 | # Options
54 | # ~~~~~~~
55 |
56 | #: Set to True if you want inline CSS styles instead of classes
57 | INLINESTYLES = False
58 |
59 | #: The default formatter
60 | DEFAULT = HtmlFormatter(cssclass="highlight code-block", noclasses=INLINESTYLES)
61 |
62 | #: Add name -> formatter pairs for every variant you want to use
63 | VARIANTS = {
64 | # 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
65 | }
66 |
67 |
68 | class CodeBlock(Directive):
69 |
70 | """Source code syntax hightlighting."""
71 |
72 | required_arguments = 1
73 | optional_arguments = 0
74 | final_argument_whitespace = True
75 | option_spec = {key: directives.flag for key in VARIANTS}
76 | has_content = True
77 |
78 | def run(self):
79 | self.assert_has_content()
80 | try:
81 | lexer_name = self.arguments[0]
82 |
83 | lexer = get_lexer_by_name(lexer_name)
84 | except ValueError:
85 | # no lexer found - use the text one instead of an exception
86 | lexer = TextLexer()
87 | # take an arbitrary option if more than one is given
88 | formatter = self.options and VARIANTS[list(self.options)[0]] or DEFAULT
89 | parsed = highlight("\n".join(self.content), lexer, formatter)
90 | return [nodes.raw("", parsed, format="html")]
91 |
--------------------------------------------------------------------------------
/django_docutils/references/rst/transforms/xref.py:
--------------------------------------------------------------------------------
1 | from os import path
2 |
3 | from django.utils.translation import gettext_lazy as _
4 | from docutils import nodes
5 | from docutils.transforms import Transform
6 | from docutils.utils import relative_path
7 |
8 | from django_docutils.lib.transforms.font_awesome import fa_classes_from_url
9 | from django_docutils.references.models import get_reference_model
10 |
11 | from ..nodes import pending_xref
12 |
13 | Reference = get_reference_model()
14 |
15 |
16 | class XRefTransform(Transform):
17 | default_priority = 5
18 |
19 | def apply(self):
20 | references = Reference.objects.all().values()
21 |
22 | for node in self.document.traverse(pending_xref):
23 | contnode = node[0].deepcopy()
24 | domain = "std"
25 | project, target = node["reftarget"].split(":", 1)
26 |
27 | ref = next(
28 | (
29 | r
30 | for r in references
31 | if r["target"] == target and r["project"] == project
32 | ),
33 | None,
34 | )
35 |
36 | if not ref:
37 | ref = next((r for r in references if r["target"] == target), None)
38 |
39 | proj, version, uri, dispname = (
40 | ref["project"],
41 | ref["project_version"],
42 | ref["uri"],
43 | ref["display_name"],
44 | )
45 |
46 | if not dispname:
47 | dispname = "-"
48 | if "://" not in uri and node.get("refdoc"):
49 | # get correct path in case of subdirectories
50 | uri = path.join(relative_path(node["refdoc"], "."), uri)
51 | newnode = nodes.reference(
52 | "",
53 | "",
54 | internal=False,
55 | refuri=uri,
56 | reftitle=_("(in %s v%s)") % (proj, version),
57 | )
58 |
59 | if node.get("refexplicit"):
60 | # use whatever title was given
61 | newnode.append(contnode)
62 | elif dispname == "-" or (domain == "std" and node["reftype"] == "keyword"):
63 | # use whatever title was given, but strip prefix
64 | title = contnode.astext()
65 | if project and title.startswith(project + ":"):
66 | newnode.append(
67 | contnode.__class__(
68 | title[len(project) + 1 :], title[len(project) + 1 :]
69 | )
70 | )
71 | else:
72 | newnode.append(contnode)
73 | else:
74 | # else use the given display name (used for :ref:)
75 | newnode.append(contnode.__class__(dispname, dispname))
76 |
77 | fa_classes = fa_classes_from_url(url=uri)
78 | if fa_classes != "":
79 | fa_tag = f''
80 | newnode.insert(0, nodes.raw("", fa_tag, format="html"))
81 |
82 | node.replace_self(newnode)
83 |
84 | def visit_pending_xref(self, node):
85 | pass
86 |
87 | def depart_pending_xref(self, node):
88 | pass
89 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/post.py:
--------------------------------------------------------------------------------
1 | import dirtyfields
2 | from django.apps import apps as django_apps
3 | from django.conf import settings
4 | from django.contrib.contenttypes.models import ContentType
5 | from django.core.exceptions import ImproperlyConfigured
6 | from django.db import models
7 | from django.utils.functional import cached_property
8 | from django.utils.translation import gettext_lazy as _
9 | from django_extensions.db.fields import (
10 | AutoSlugField,
11 | CreationDateTimeField,
12 | ModificationDateTimeField,
13 | )
14 | from django_slugify_processor.text import slugify
15 | from randomslugfield import RandomSlugField
16 |
17 |
18 | def get_post_model():
19 | try:
20 | return django_apps.get_model(settings.BASED_POST_MODEL, require_ready=False)
21 | except ValueError:
22 | raise ImproperlyConfigured(
23 | "BASED_POST_MODEL must be of the form 'app_label.model_name'"
24 | )
25 | except LookupError:
26 | raise ImproperlyConfigured(
27 | "BASED_POST_MODEL refers to model '%s' that has not been installed"
28 | % settings.BASED_POST_MODEL
29 | )
30 |
31 |
32 | def get_post_models():
33 | """Return high-level PageBase models. Skips subclasses of PostBase."""
34 | models = []
35 | for model in django_apps.get_models():
36 | if issubclass(model, RSTPostBase) and model.__subclasses__():
37 | models.append(model)
38 | return models
39 |
40 |
41 | def get_anonymous_user_instance(UserModel=None):
42 | if UserModel is None:
43 | from django.contrib.auth import get_user_model
44 |
45 | UserModel = get_user_model()
46 | user, _ = UserModel.objects.get_or_create(
47 | username=settings.ANONYMOUS_USER_NAME, email="noone@localhost"
48 | )
49 | return user
50 |
51 |
52 | class RSTPostBase(dirtyfields.DirtyFieldsMixin, models.Model):
53 | title = models.CharField(_("title"), max_length=255)
54 |
55 | slug_title = AutoSlugField(
56 | _("slug"), populate_from="title", slugify_function=slugify
57 | )
58 |
59 | slug_id = RandomSlugField(length=8, unique=True, editable=False)
60 | author_name = models.CharField(_("Author name"), max_length=255)
61 | is_draft = models.BooleanField(default=False, editable=False, db_index=True)
62 | created = CreationDateTimeField(_("created"))
63 | modified = ModificationDateTimeField(_("modified"))
64 |
65 | class Meta:
66 | ordering = ["-created"]
67 | abstract = True
68 |
69 | def save(self, **kwargs):
70 | self.update_modified = kwargs.pop(
71 | "update_modified", getattr(self, "update_modified", True)
72 | )
73 | super().save(**kwargs)
74 |
75 | @cached_property
76 | def content_type(self):
77 | return ContentType.objects.get_for_model(self)
78 |
79 | @cached_property
80 | def subtitle(self):
81 | return self.root_page.subtitle
82 |
83 | def __str__(self):
84 | title = self.title
85 | if self.subtitle:
86 | title += ": " + self.subtitle
87 | return title
88 |
89 | @classmethod
90 | def check(cls, **kwargs):
91 | from .checks import _check_root_page
92 |
93 | errors = super().check(**kwargs)
94 | errors.extend(_check_root_page(cls))
95 | return errors
96 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "django-docutils"
3 | version = "0.6.0"
4 | description = "Documentation Utilities (Docutils, reStructuredText) for django.)"
5 |
6 | license = "MIT"
7 |
8 | authors = [
9 | "Tony Narlock ",
10 | ]
11 |
12 | readme = "README.md"
13 | packages = [
14 | { include = "django_docutils" },
15 | ]
16 | classifiers = [
17 | 'Development Status :: 2 - Pre-Alpha',
18 | 'Framework :: Django',
19 | 'Framework :: Django :: 3.1',
20 | 'Framework :: Django :: 3.2',
21 | 'Framework :: Django :: 4.0',
22 | 'Intended Audience :: Developers',
23 | 'License :: OSI Approved :: MIT License',
24 | 'Natural Language :: English',
25 | 'Programming Language :: Python :: 3',
26 | 'Programming Language :: Python :: 3.8',
27 | 'Programming Language :: Python :: 3.9',
28 | 'Programming Language :: Python :: 3.10',
29 | ]
30 | keywords = ["django", "docutils", "reStructuredText", "rst", "reST"]
31 |
32 | homepage = "https://django-docutils.git-pull.com"
33 |
34 | [tool.poetry.urls]
35 | "Bug Tracker" = "https://github.com/tony/django-docutils/issues"
36 | Documentation = "https://django-docutils.git-pull.com"
37 | Repository = "https://github.com/tony/django-docutils"
38 | Changes = "https://github.com/tony/django-docutils/blob/master/CHANGES"
39 | "Q & A" = "https://github.com/tony/django-docutils/discussions"
40 |
41 | [tool.poetry.dependencies]
42 | python = "^3.8"
43 | Django = ">=3.2"
44 | docutils = "*"
45 | tldextract = { version = "*", optional = true }
46 | tqdm = { version = "*", optional = true }
47 | pygments = "<3"
48 | django-extensions = "*"
49 | django-randomslugfield = "*"
50 | django-slugify-processor = "*"
51 | django-dirtyfields = ">1.3.0"
52 | lxml = "*"
53 | bitly-api-py3 = "*"
54 |
55 | [tool.poetry.dev-dependencies]
56 | ### Docs ###
57 | sphinx = "*"
58 | furo = "*"
59 | sphinx-autobuild = "*"
60 | sphinx-autodoc-typehints = "*"
61 | sphinx-click = "*"
62 | sphinx-issues = "*"
63 | sphinx-inline-tabs = "*"
64 | sphinxext-opengraph = "*"
65 | sphinx-copybutton = "*"
66 | sphinxext-rediraffe = "*"
67 | myst_parser = "*"
68 |
69 | ### Testing ###
70 | pytest = "*"
71 | pytest-rerunfailures = "*"
72 | pytest-mock = "*"
73 | pytest-watcher = "^0.2.3"
74 | factory-boy = "*"
75 | pytest-factoryboy = "*"
76 | pytest-django = "*"
77 | dj-inmemorystorage = "*"
78 | responses = "*"
79 |
80 | ### Coverage ###
81 | codecov = "*"
82 | coverage = "*"
83 | pytest-cov = "*"
84 |
85 | ### Format ###
86 | black = "*"
87 | isort = "*"
88 |
89 | ### Lint ###
90 | flake8 = "*"
91 | mypy = "*"
92 |
93 | [tool.poetry.extras]
94 | favicon = ["tldextract", "tqdm"]
95 | intersphinx = ["tqdm"]
96 |
97 | # Development stuff
98 | docs = [
99 | "sphinx",
100 | "sphinx-issues",
101 | "sphinx-click",
102 | "sphinx-autodoc-typehints",
103 | "sphinx-autobuild",
104 | "sphinxext-rediraffe",
105 | "sphinx-copybutton",
106 | "sphinxext-opengraph",
107 | "sphinx-inline-tabs",
108 | "myst_parser",
109 | "furo",
110 | ]
111 | test = [
112 | "pytest",
113 | "pytest-rerunfailures",
114 | "pytest-mock",
115 | "pytest-watcher",
116 | "factory-boy",
117 | "pytest-factoryboy",
118 | "pytest-django",
119 | "dj-inmemorystorage",
120 | "responses",
121 | ]
122 | coverage = ["codecov", "coverage", "pytest-cov"]
123 | format = ["black", "isort"]
124 | lint = ["flake8", "mypy"]
125 |
--------------------------------------------------------------------------------
/django_docutils/lib/views.py:
--------------------------------------------------------------------------------
1 | from django.utils.functional import cached_property
2 | from django.views.generic.base import ContextMixin, TemplateView
3 |
4 | from django_docutils.lib.publisher import (
5 | publish_doctree,
6 | publish_html_from_doctree,
7 | publish_toc_from_doctree,
8 | )
9 |
10 | from .text import smart_title
11 |
12 |
13 | class TitleMixin(ContextMixin):
14 | title = None
15 | subtitle = None
16 |
17 | def get_context_data(self, **kwargs):
18 | context = super().get_context_data(**kwargs)
19 | if self.title:
20 | context["title"] = smart_title(self.title)
21 | if self.subtitle:
22 | context["subtitle"] = smart_title(self.subtitle)
23 | return context
24 |
25 |
26 | class TemplateTitleView(TemplateView, TitleMixin):
27 | title = None
28 | subtitle = None
29 |
30 | def get_context_data(self, **kwargs):
31 | context = super().get_context_data(**kwargs)
32 | return context
33 |
34 |
35 | class RSTMixin:
36 | @cached_property
37 | def raw_content(self):
38 | raise NotImplementedError
39 |
40 | @cached_property
41 | def doctree(self):
42 | return publish_doctree(self.raw_content)
43 |
44 | @cached_property
45 | def sidebar(self, **kwargs):
46 | return publish_toc_from_doctree(self.doctree)
47 |
48 | @cached_property
49 | def content(self):
50 | return publish_html_from_doctree(
51 | self.doctree, **getattr(self, "rst_settings", {})
52 | )
53 |
54 | def get_base_template(self):
55 | """TODO: move this out of RSTMixin, it is AMP related, not RST"""
56 | if self.request.GET.get("is_amp", False):
57 | return "based/base-amp.html"
58 | else:
59 | return "base.html"
60 |
61 |
62 | class RSTRawView(TemplateTitleView):
63 |
64 | """Send pure reStructuredText to template.
65 |
66 | Requires template tags to process it.
67 |
68 | .. code-block:: html
69 |
70 | {% block content %}
71 |
72 | {% restructuredtext content show_title=False inject_ads=False %}
73 |
74 | {% endblock content %}
75 |
76 | {% block sidebar %}
77 | {% restructuredtext content toc_only=True %}
78 | {% endblock sidebar %}
79 |
80 | """
81 |
82 | template_name = "rst/raw.html"
83 | file_path = None
84 | title = None
85 | rst_settings = {"inject_ads": True}
86 |
87 | def get_context_data(self, **kwargs):
88 | context = super().get_context_data(**kwargs)
89 | context["content"] = open(self.file_path, "r").read()
90 | context["inject_ads"] = self.rst_settings["inject_ads"]
91 | return context
92 |
93 |
94 | class RSTView(RSTRawView, RSTMixin):
95 | template_name = "rst/base.html"
96 | file_path = None
97 | title = None
98 | rst_settings = {"inject_ads": True}
99 |
100 | @cached_property
101 | def raw_content(self):
102 | return open(self.file_path, "r").read()
103 |
104 | def get_context_data(self, **kwargs):
105 | context = super().get_context_data(**kwargs)
106 | context["content"] = self.content
107 | context["sidebar"] = self.sidebar
108 |
109 | return context
110 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | This format is different:
3 |
4 | 1. It resides in its own directory
5 | 2. Includes a JSON file
6 | 3. Does not store configuration in the RST file itself
7 |
8 | This is intended to ensure these projects can be open source repositories
9 | with collaborators on GitHub.
10 |
11 | Here is what a layout of "dir"-type RST fixtures look like:
12 |
13 | fixtures/
14 | - myproject/
15 | - manifest.json: post content configuration information (e.g. title)
16 | - README.rst: reStructuredText content
17 | """
18 | import os
19 |
20 | from django_docutils.exc import BasedException
21 | from django_docutils.lib.fixtures.utils import find_rst_files
22 |
23 |
24 | def find_series_files(config, path):
25 | """Find series files from config + path.
26 |
27 | Configs don't hold the path of the files right now, so we pass it in.
28 |
29 | :param config: config.json in dict format
30 | :type config: :class:`python:dict`
31 | :param path: path to project (directory)
32 | :type path: string
33 | :returns: *ordered* list of files, relative to path
34 | :rtype: list
35 | """
36 |
37 | files_in_path = find_rst_files(path, absolute=True)
38 | files_in_series = [os.path.join(path, f) for f in config["series"]]
39 |
40 | # assert series in config matches files present in directory, if not
41 | # throw exception
42 | has_files = set(files_in_path) == set(files_in_series)
43 |
44 | if not has_files:
45 | raise BasedException(
46 | "Files in {} ({}) do not match the ones listed in"
47 | 'the "series" metadata ({}).'.format(
48 | path, ", ".join(files_in_path), ", ".join(files_in_series)
49 | )
50 | )
51 |
52 | return files_in_series
53 |
54 |
55 | def is_dir_project(path):
56 | """Return True if directory is a directory-based project.
57 |
58 | (As opposed to a single file.)
59 |
60 | :param path: Directory of project
61 | :type path: string
62 | :returns; If directory has the proper files to be a "dir"-type project
63 | :rtype: boolean
64 | """
65 | required_files = [
66 | os.path.join(path, "README.rst"),
67 | os.path.join(path, "manifest.json"),
68 | ]
69 |
70 | return all(os.path.exists(f) for f in required_files)
71 |
72 |
73 | def find_rst_dirs_in_app(app_config):
74 | """Return reStructuredText fixtures from fixtures dir for app.
75 |
76 | :param app_config: Configuration for django app
77 | :type app_config: :class:`django.apps.AppConfig`
78 | :returns: list of files relative to app's fixture dir path
79 | """
80 | fixtures_dir = os.path.join(app_config.path, "fixtures")
81 | return find_rst_dir_projects(fixtures_dir)
82 |
83 |
84 | def find_rst_dir_projects(path):
85 | """Find and return projects in a directory with:
86 |
87 | - manifest.json
88 | - README.rst
89 |
90 | :param path: Path to search for projects
91 | :type path: string
92 | :returns: List of directories containing fixtures projects in dir format
93 | :rtype: list
94 | """
95 | paths = []
96 | for _root, dirname, filenames in os.walk(path):
97 | for dir_ in dirname:
98 | if is_dir_project(os.path.join(_root, dir_)):
99 | paths.append(os.path.join(_root, dir_))
100 | return paths
101 |
--------------------------------------------------------------------------------
/django_docutils/directives.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from docutils import nodes
5 | from docutils.parsers.rst import Directive, directives
6 | from pygments import highlight
7 | from pygments.formatters import HtmlFormatter
8 | from pygments.lexers import TextLexer, get_lexer_by_name
9 |
10 | # -*- coding: utf-8 -*-
11 | """
12 | The Pygments reStructuredText directive
13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 |
15 | This fragment is a Docutils_ 0.5 directive that renders source code
16 | (to HTML only, currently) via Pygments.
17 |
18 | To use it, adjust the options below and copy the code into a module
19 | that you import on initialization. The code then automatically
20 | registers a ``sourcecode`` directive that you can use instead of
21 | normal code blocks like this::
22 |
23 | .. sourcecode:: python
24 |
25 | My code goes here.
26 |
27 | If you want to have different code styles, e.g. one with line numbers
28 | and one without, add formatters with their names in the VARIANTS dict
29 | below. You can invoke them instead of the DEFAULT one by using a
30 | directive option::
31 |
32 | .. sourcecode:: python
33 | :linenos:
34 |
35 | My code goes here.
36 |
37 | Look at the `directive documentation`_ to get all the gory details.
38 |
39 | .. _Docutils: http://docutils.sf.net/
40 | .. _directive documentation:
41 | http://docutils.sourceforge.net/docs/howto/rst-directives.html
42 |
43 | :copyright: Copyright 2006-2015 by the Pygments team, see AUTHORS.
44 | :license: BSD, see LICENSE for details.
45 | """
46 |
47 | # Options
48 | # ~~~~~~~
49 |
50 | # Set to True if you want inline CSS styles instead of classes
51 | INLINESTYLES = False
52 |
53 |
54 | #: The default formatter
55 | DEFAULT = HtmlFormatter(noclasses=INLINESTYLES)
56 |
57 | #: Add name -> formatter pairs for every variant you want to use
58 | VARIANTS = {
59 | # 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
60 | }
61 |
62 |
63 | class CodeBlock(Directive):
64 | """Source code syntax hightlighting."""
65 |
66 | required_arguments = 1
67 | optional_arguments = 0
68 | final_argument_whitespace = True
69 | option_spec = dict([(key, directives.flag) for key in VARIANTS])
70 | has_content = True
71 |
72 | def run(self):
73 | self.assert_has_content()
74 | try:
75 | lexer = get_lexer_by_name(self.arguments[0])
76 | except ValueError:
77 | # no lexer found - use the text one instead of an exception
78 | lexer = TextLexer()
79 | # take an arbitrary option if more than one is given
80 | formatter = self.options and VARIANTS[list(self.options)[0]] or DEFAULT
81 | parsed = highlight("\n".join(self.content), lexer, formatter)
82 | return [nodes.raw("", parsed, format="html")]
83 |
84 |
85 | def register_pygments_directive(directive="code-block"):
86 | """Register pygments directive.
87 |
88 | Parameters
89 | ----------
90 | directive : str
91 | directive name to register pygments to.
92 |
93 | Examples
94 | --------
95 | If you wish to use (override) code-block (default), that means::
96 |
97 | .. code-block::
98 |
99 | // will be highlighted by pygments
100 | """
101 | directives.register_directive(directive, CodeBlock)
102 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/utils.py:
--------------------------------------------------------------------------------
1 | import fnmatch
2 | import os
3 |
4 | from django.apps import apps
5 |
6 |
7 | def get_model_from_post_app(app_config):
8 | """Return post model for app.
9 |
10 | :param app_config: Configuration for django app
11 | :type app_config: :class:`django.apps.AppConfig`
12 | :returns: Post sub-model for app
13 | :rtype: Model
14 | """
15 | from django_docutils.rst_post.models import RSTPostBase
16 |
17 | for model in app_config.get_models():
18 | if issubclass(model, RSTPostBase):
19 | return model
20 |
21 |
22 | def find_rst_files(path, absolute=False, recursive=False):
23 | """Find .rst files in directory.
24 |
25 | This is reused in dir-style projects.
26 |
27 | :param path: path to project (directory)
28 | :type path: string
29 | :param absolute: return absolute paths
30 | :type absolute: bool
31 | :param recursive: recursively check for rst files
32 | :type recursive: bool
33 | :returns: return list of .rst files from path
34 | :rtype: list
35 | """
36 | files = []
37 | for _root, dirname, filenames in os.walk(path):
38 | for filename in fnmatch.filter(filenames, "*.rst"):
39 | p = os.path.relpath(_root, path)
40 | if absolute:
41 | files.append(os.path.normpath(os.path.join(path, p, filename)))
42 | else:
43 | files.append(os.path.join(p, filename))
44 | if not recursive:
45 | break
46 | return files
47 |
48 |
49 | def find_app_configs_with_fixtures(has_rst_files=True):
50 | """Return a list of apps with fixtures dir.
51 |
52 | In Django 1.11 fixture_dirs grabs a directory of all fixtures, in our
53 | situation, we want to grab the correct model.
54 |
55 | :param has_rst_files: Only return apps with .rst files in fixtures
56 | :type has_rst_files: bool
57 | :returns: list of app configs with fixtures directory
58 | :rtype: list of :django:class`django.apps.AppConfig`
59 | """
60 | app_configs = []
61 | for app_config in apps.get_app_configs():
62 | app_dir = os.path.join(app_config.path, "fixtures")
63 | if os.path.isdir(app_dir):
64 | if has_rst_files:
65 | if len(find_rst_files(app_dir, recursive=True)) < 1:
66 | continue
67 | app_configs.append(app_config)
68 |
69 | return app_configs
70 |
71 |
72 | def find_rst_files_in_app(app_config):
73 | """Return fixtures reStructuredText files from fixtures dir for app.
74 |
75 | :param app_config: Configuration for django app
76 | :type app_config: :class:`django.apps.AppConfig`
77 | :returns: list of files relative to app's fixtures dir path
78 | """
79 | fixtures_dir = os.path.join(app_config.path, "fixtures")
80 | return find_rst_files(fixtures_dir)
81 |
82 |
83 | def split_page_data(post_data):
84 | """Pluck the page data from the post data and return both.
85 |
86 | publish_post is pure and doesn't know what a post/page is.
87 |
88 | Posts contain pages. devel.tech's architecture needs to split page data
89 | from the post. The page will store the "body" content and also
90 | a subtitle.
91 | """
92 | page_data = {}
93 | for field in ["body", "subtitle", "draft"]:
94 | try:
95 | page_data[field] = post_data.pop(field)
96 | except KeyError: # handle corner case, no subtitle/body
97 | pass
98 | return post_data, page_data
99 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/utils.py:
--------------------------------------------------------------------------------
1 | from django.core import checks
2 | from django.core.exceptions import FieldDoesNotExist
3 | from django.db import models
4 | from django.db.models.fields.related import resolve_relation
5 |
6 | from .post import RSTPostBase
7 | from .post_page import RSTPostPageBase
8 |
9 |
10 | def _check_root_page(cls):
11 | """System check for root_page field on PostBase models."""
12 | try:
13 | root_page = cls._meta.get_field("root_page")
14 |
15 | # must be correct field type
16 | if root_page.__class__ != models.ForeignKey:
17 | return [
18 | checks.Error(
19 | "Wrong field type for root_page field.",
20 | hint="Use a models.ForeignKey field.",
21 | obj=cls,
22 | id="rst_post.E002",
23 | )
24 | ]
25 | else: # check for the correct relation inside root_page
26 | # incase of model class import strings, e.g. 'MyPostPage'
27 | # instead of MyPostPage
28 | related_model = resolve_relation(cls, root_page.related_model)
29 | if not issubclass(related_model, RSTPostPageBase):
30 | return [
31 | checks.Error(
32 | "Wrong related model for root_page relationship.",
33 | hint="Use a model subclassing RSTPostPageBase",
34 | obj=cls,
35 | id="rst_post.E003",
36 | )
37 | ]
38 | except FieldDoesNotExist: # no root_page field
39 | return [
40 | checks.Error(
41 | "Missing root_page field.",
42 | hint="Add a root_page ForeignKey a subclass of RSTPostPageBase",
43 | obj=cls,
44 | id="rst_post.E001",
45 | )
46 | ]
47 | return []
48 |
49 |
50 | def _check_postpage_post_back_relation(cls):
51 | """System check for post field on PostPageBase models."""
52 | try:
53 | page_field = cls._meta.get_field("post")
54 |
55 | # must be correct field type
56 | if page_field.__class__ != models.ForeignKey:
57 | return [
58 | checks.Error(
59 | "Wrong field type for post field.",
60 | hint="Use a models.ForeignKey field.",
61 | obj=cls,
62 | id="rst_post.E005",
63 | )
64 | ]
65 | else: # check for the correct relation inside page_field
66 | # incase of model class import strings, e.g. 'MyPostPage'
67 | # instead of MyPostPage
68 | related_model = resolve_relation(cls, page_field.related_model)
69 | if not issubclass(related_model, RSTPostBase):
70 | return [
71 | checks.Error(
72 | "Wrong related model for post relationship.",
73 | hint="Use a model subclassing RSTPostBase",
74 | obj=cls,
75 | id="rst_post.E006",
76 | )
77 | ]
78 | except FieldDoesNotExist: # no page_field field
79 | return [
80 | checks.Error(
81 | "Missing post field.",
82 | hint="Add a post ForeignKey that subclasses PostBase",
83 | obj=cls,
84 | id="rst_post.E004",
85 | )
86 | ]
87 | return []
88 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/checks.py:
--------------------------------------------------------------------------------
1 | from django.core import checks
2 | from django.core.exceptions import FieldDoesNotExist
3 | from django.db import models
4 | from django.db.models.fields.related import resolve_relation
5 |
6 | from .post import RSTPostBase
7 | from .post_page import RSTPostPageBase
8 |
9 |
10 | def _check_root_page(cls):
11 | """System check for root_page field on PostBase models."""
12 | try:
13 | root_page = cls._meta.get_field("root_page")
14 |
15 | # must be correct field type
16 | if root_page.__class__ != models.ForeignKey:
17 | return [
18 | checks.Error(
19 | "Wrong field type for root_page field.",
20 | hint="Use a models.ForeignKey field.",
21 | obj=cls,
22 | id="rst_post.E002",
23 | )
24 | ]
25 | else: # check for the correct relation inside root_page
26 | # incase of model class import strings, e.g. 'MyPostPage'
27 | # instead of MyPostPage
28 | related_model = resolve_relation(cls, root_page.related_model)
29 | if not issubclass(related_model, RSTPostPageBase):
30 | return [
31 | checks.Error(
32 | "Wrong related model for root_page relationship.",
33 | hint="Use a model subclassing RSTPostPageBase",
34 | obj=cls,
35 | id="rst_post.E003",
36 | )
37 | ]
38 | except FieldDoesNotExist: # no root_page field
39 | return [
40 | checks.Error(
41 | "Missing root_page field.",
42 | hint="Add a root_page ForeignKey a subclass of RSTPostPageBase",
43 | obj=cls,
44 | id="rst_post.E001",
45 | )
46 | ]
47 | return []
48 |
49 |
50 | def _check_postpage_post_back_relation(cls):
51 | """System check for post field on PostPageBase models."""
52 | try:
53 | page_field = cls._meta.get_field("post")
54 |
55 | # must be correct field type
56 | if page_field.__class__ != models.ForeignKey:
57 | return [
58 | checks.Error(
59 | "Wrong field type for post field.",
60 | hint="Use a models.ForeignKey field.",
61 | obj=cls,
62 | id="rst_post.E005",
63 | )
64 | ]
65 | else: # check for the correct relation inside page_field
66 | # incase of model class import strings, e.g. 'MyPostPage'
67 | # instead of MyPostPage
68 | related_model = resolve_relation(cls, page_field.related_model)
69 | if not issubclass(related_model, RSTPostBase):
70 | return [
71 | checks.Error(
72 | "Wrong related model for post relationship.",
73 | hint="Use a model subclassing RSTPostBase",
74 | obj=cls,
75 | id="rst_post.E006",
76 | )
77 | ]
78 | except FieldDoesNotExist: # no page_field field
79 | return [
80 | checks.Error(
81 | "Missing post field.",
82 | hint="Add a post ForeignKey that subclasses RSTPostBase",
83 | obj=cls,
84 | id="rst_post.E004",
85 | )
86 | ]
87 | return []
88 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Contributing
3 | ============
4 |
5 | Contributions are welcome, and they are greatly appreciated! Every
6 | little bit helps, and credit will always be given.
7 |
8 | You can contribute in many ways:
9 |
10 | Types of Contributions
11 | ----------------------
12 |
13 | Report Bugs
14 | ~~~~~~~~~~~
15 |
16 | Report bugs at https://github.com/tony/django-docutils/issues.
17 |
18 | If you are reporting a bug, please include:
19 |
20 | * Your operating system name and version.
21 | * Any details about your local setup that might be helpful in troubleshooting.
22 | * Detailed steps to reproduce the bug.
23 |
24 | Fix Bugs
25 | ~~~~~~~~
26 |
27 | Look through the GitHub issues for bugs. Anything tagged with "bug"
28 | is open to whoever wants to implement it.
29 |
30 | Implement Features
31 | ~~~~~~~~~~~~~~~~~~
32 |
33 | Look through the GitHub issues for features. Anything tagged with "feature"
34 | is open to whoever wants to implement it.
35 |
36 | Write Documentation
37 | ~~~~~~~~~~~~~~~~~~~
38 |
39 | django-docutils could always use more documentation, whether as part of the
40 | official django-docutils docs, in docstrings, or even on the web in blog posts,
41 | articles, and such.
42 |
43 | Submit Feedback
44 | ~~~~~~~~~~~~~~~
45 |
46 | The best way to send feedback is to file an issue at https://github.com/tony/django-docutils/issues.
47 |
48 | If you are proposing a feature:
49 |
50 | * Explain in detail how it would work.
51 | * Keep the scope as narrow as possible, to make it easier to implement.
52 | * Remember that this is a volunteer-driven project, and that contributions
53 | are welcome :)
54 |
55 | Get Started!
56 | ------------
57 |
58 | Ready to contribute? Here's how to set up `django-docutils` for local development.
59 |
60 | 1. Fork the `django-docutils` repo on GitHub.
61 | 2. Clone your fork locally::
62 |
63 | $ git clone git@github.com:your_name_here/django-docutils.git
64 |
65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
66 |
67 | $ mkvirtualenv django-docutils
68 | $ cd django-docutils/
69 | $ python setup.py develop
70 |
71 | 4. Create a branch for local development::
72 |
73 | $ git checkout -b name-of-your-bugfix-or-feature
74 |
75 | Now you can make your changes locally.
76 |
77 | 5. When you're done making changes, check that your changes pass flake8 and the
78 | tests, including testing other Python versions with tox::
79 |
80 | $ flake8 django_docutils tests
81 | $ python setup.py test
82 | $ tox
83 |
84 | To get flake8 and tox, just pip install them into your virtualenv.
85 |
86 | 6. Commit your changes and push your branch to GitHub::
87 |
88 | $ git add .
89 | $ git commit -m "Your detailed description of your changes."
90 | $ git push origin name-of-your-bugfix-or-feature
91 |
92 | 7. Submit a pull request through the GitHub website.
93 |
94 | Pull Request Guidelines
95 | -----------------------
96 |
97 | Before you submit a pull request, check that it meets these guidelines:
98 |
99 | 1. The pull request should include tests.
100 | 2. If the pull request adds functionality, the docs should be updated. Put
101 | your new functionality into a function with a docstring, and add the
102 | feature to the list in README.rst.
103 | 3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check
104 | the GitHub actions and make sure that the tests pass for all supported Python
105 | versions.
106 |
107 | Tips
108 | ----
109 |
110 | To run a subset of tests::
111 |
112 | $ python -m unittest tests.test_django_docutils
113 |
--------------------------------------------------------------------------------
/django_docutils/rst_post/models/tests/test_models.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django.core import checks
4 | from django.db import models
5 |
6 | from django_docutils.favicon.tests.test_app.models import RSTPost, RSTPostSubclass
7 | from django_docutils.rst_post.models import (
8 | RSTPostBase,
9 | RSTPostPageBase,
10 | get_post_models,
11 | )
12 |
13 |
14 | def test_get_post_models():
15 | models = get_post_models()
16 | assert RSTPost in models
17 | assert RSTPostSubclass not in models
18 |
19 |
20 | def test_no_root_page_field():
21 | class NoRootPage(RSTPostBase):
22 | class Meta:
23 | app_label = "test"
24 |
25 | assert NoRootPage.check() == [
26 | checks.Error(
27 | "Missing root_page field.",
28 | hint="Add a root_page ForeignKey a subclass of RSTPostPageBase",
29 | obj=NoRootPage,
30 | id="rst_post.E001",
31 | )
32 | ]
33 |
34 |
35 | def test_root_page_wrong_field_type():
36 | class WrongRootPageFieldType(RSTPostBase):
37 | root_page = models.CharField(max_length=255)
38 |
39 | class Meta:
40 | app_label = "test"
41 |
42 | assert WrongRootPageFieldType.check() == [
43 | checks.Error(
44 | "Wrong field type for root_page field.",
45 | hint="Use a models.ForeignKey field.",
46 | obj=WrongRootPageFieldType,
47 | id="rst_post.E002",
48 | )
49 | ]
50 |
51 |
52 | @pytest.mark.django_db
53 | def test_root_page_wrong_relation(EmptyModel):
54 | class WrongRootPageRelation(RSTPostBase):
55 | root_page = models.ForeignKey(EmptyModel, on_delete=models.CASCADE)
56 |
57 | class Meta:
58 | app_label = "test"
59 |
60 | assert WrongRootPageRelation.check() == [
61 | checks.Error(
62 | "Wrong related model for root_page relationship.",
63 | hint="Use a model subclassing RSTPostPageBase",
64 | obj=WrongRootPageRelation,
65 | id="rst_post.E003",
66 | )
67 | ]
68 |
69 |
70 | @pytest.mark.django_db
71 | def test_postpage_back_relation_no_field(transactional_db):
72 | class NoPageRelation(RSTPostPageBase):
73 | class Meta:
74 | app_label = "test"
75 |
76 | assert NoPageRelation.check() == [
77 | checks.Error(
78 | "Missing post field.",
79 | hint="Add a post ForeignKey that subclasses RSTPostBase",
80 | obj=NoPageRelation,
81 | id="rst_post.E004",
82 | )
83 | ]
84 |
85 |
86 | @pytest.mark.django_db
87 | def test_postpage_back_relation_field_type():
88 | class WrongPageFieldType(RSTPostPageBase):
89 | post = models.CharField(max_length=255)
90 |
91 | class Meta:
92 | app_label = "test"
93 |
94 | assert WrongPageFieldType.check() == [
95 | checks.Error(
96 | "Wrong field type for post field.",
97 | hint="Use a models.ForeignKey field.",
98 | obj=WrongPageFieldType,
99 | id="rst_post.E005",
100 | )
101 | ]
102 |
103 |
104 | @pytest.mark.django_db
105 | def test_postpage_back_relation_relation_type(EmptyModel):
106 | class WrongPostBackRelation(RSTPostPageBase):
107 | post = models.ForeignKey(EmptyModel, on_delete=models.CASCADE)
108 |
109 | class Meta:
110 | app_label = "test"
111 |
112 | assert WrongPostBackRelation.check() == [
113 | checks.Error(
114 | "Wrong related model for post relationship.",
115 | hint="Use a model subclassing RSTPostBase",
116 | obj=WrongPostBackRelation,
117 | id="rst_post.E006",
118 | )
119 | ]
120 |
--------------------------------------------------------------------------------
/django_docutils/lib/utils.py:
--------------------------------------------------------------------------------
1 | """Docutils util functions and regexes.
2 |
3 | Some stuff is ported from sphinx:
4 |
5 | - explicit_title_re, ws_re, set_role_source_info, split_explicit_title
6 | """
7 | import re
8 |
9 | from docutils import nodes
10 |
11 | if False:
12 | from typing import Any, Tuple, Type, unicode # NOQA
13 |
14 | from docutils import nodes # NOQA
15 | from sphinx import Pattern
16 |
17 | # \x00 means the "<" was backslash-escaped (from sphinx)
18 | explicit_title_re = re.compile(r"^(.+?)\s*(?$", re.DOTALL)
19 |
20 | ws_re = re.compile(r"\s+") # type: Pattern
21 |
22 |
23 | def split_explicit_title(text):
24 | # type: (unicode) -> Tuple[bool, unicode, unicode]
25 | """Split role content into title and target, if given (from sphinx)."""
26 | match = explicit_title_re.match(text) # type: ignore
27 | if match:
28 | return True, match.group(1), match.group(2)
29 | return False, text, text
30 |
31 |
32 | def chop_after_docinfo(source):
33 | """Return the source of a document after DocInfo metadata.
34 |
35 | :param source: Source of RST document
36 | :type source: string
37 | :returns: All source content after docinfo
38 | :rtype: string
39 | """
40 | # find the last docinfo element
41 | index = re.findall(r":[\w_]+: [\w \-_\,]+\n", source)[-1]
42 |
43 | # find the character position of last docinfo element + len of it
44 | rest = source[source.rindex(index) + len(index) :]
45 | return rest.strip()
46 |
47 |
48 | def chop_after_title(source):
49 | """Return the source of a document after DocInfo metadata.
50 |
51 | :param source: Source of RST document
52 | :type source: string
53 | :returns: All source content after docinfo
54 | :rtype: string
55 | """
56 | # find the last docinfo element
57 | index = re.findall(r"[=-]{3,}\n.*\n[-=]{3,}", source, re.MULTILINE)[-1]
58 |
59 | # find the character position of last docinfo element + len of it
60 | rest = source[source.rindex(index) + len(index) :]
61 | return rest.strip()
62 |
63 |
64 | def chop_after_heading_smartly(source):
65 | """Return the content after subtitle, or, if exists, docinfo.
66 |
67 | This is a universal chop that can be used whether a document has docinfo,
68 | a title, subtitle, or not. Traditionally, directory-style RST fixtures keep
69 | metadata inside a JSON file instead of docinfo, so
70 | :func:`chop_after_docinfo` wouldn't work.
71 |
72 | :param source: Source of RST document
73 | :type source: string
74 | :returns: All source content after docinfo or title
75 | :rtype: string
76 | """
77 | try:
78 | return chop_after_docinfo(source)
79 | except IndexError:
80 | return chop_after_title(source)
81 |
82 |
83 | def find_root_sections(document):
84 | """Yield top level section nodes
85 |
86 | :param document: docutils document
87 | :type document: :class:`docutils.nodes.document`
88 | :yields: upper level titles of document
89 | :rtype: :class:`docutils.nodes.Node`
90 | """
91 | for node in document:
92 | if isinstance(node, nodes.section): # traverse root-level sections
93 | yield node
94 |
95 |
96 | def append_html_to_node(node, ad_code):
97 | """Inject HTML in this node
98 |
99 | :param node: node of the section to find last paragraph of
100 | :type node: :class:`docutils.nodes.node`
101 | :param ad_code: html to inject inside ad
102 | :type ad_code: string
103 | """
104 | html = ''
105 | html += ad_code
106 | html += "
"
107 |
108 | html_node = nodes.raw("", html, format="html")
109 |
110 | node.append(html_node)
111 | node.replace_self(node)
112 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/test_scrape.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import responses
4 |
5 | from django_docutils.exc import BasedException
6 | from django_docutils.favicon.scrape import _request_favicon, get_favicon
7 |
8 |
9 | @responses.activate
10 | def test__request_favicon_rejects_wrong_type():
11 | url = "https://aklsdfjaweof.com"
12 |
13 | # page response
14 | responses.add(responses.GET, url, body="blah", status=200, content_type="text/html")
15 |
16 | with pytest.raises(BasedException, match=r"Not an image"):
17 | _request_favicon(url)
18 |
19 |
20 | @responses.activate
21 | def test_get_favicon_url_connection():
22 | url = "https://aklsdfjaweof.com"
23 |
24 | with pytest.raises(BasedException, match=r"The website .* isn\'t connecting."):
25 | get_favicon(url)
26 |
27 |
28 | @responses.activate
29 | def test_get_favicon_url_catches_shortcut_icon():
30 | url = "https://aklsdfjaweof.com"
31 | favicon_url = f"{url}/images/favicon.ico"
32 | favicon_content = b"lol"
33 |
34 | responses.add(
35 | responses.GET,
36 | url,
37 | body=''.format(
38 | favicon_url=favicon_url
39 | ),
40 | status=200,
41 | content_type="text/html",
42 | )
43 |
44 | responses.add(
45 | responses.GET,
46 | favicon_url,
47 | body=favicon_content,
48 | status=200,
49 | content_type="image/ico",
50 | )
51 |
52 | assert get_favicon(url) == favicon_content
53 |
54 |
55 | @responses.activate
56 | def test_get_favicon_url_falls_back_to_root_favicon_error_retrieve():
57 | """In this case, pattern exists, but favicon URL in pattern bad.
58 |
59 | The get_faviconr should then attempt to connect to site_root/favicon.ico"""
60 | url = "https://aklsdfjaweof.com"
61 | favicon_url = f"{url}/images/favicon.ico"
62 | root_favicon_url = f"{url}/favicon.ico"
63 | favicon_content = b"lol"
64 |
65 | responses.add(
66 | responses.GET,
67 | url,
68 | body=''.format(
69 | favicon_url=favicon_url
70 | ),
71 | status=200,
72 | content_type="text/html",
73 | )
74 |
75 | responses.add(
76 | responses.GET,
77 | root_favicon_url,
78 | body=favicon_content,
79 | status=200,
80 | content_type="image/ico",
81 | )
82 |
83 | assert get_favicon(url) == favicon_content
84 |
85 |
86 | @responses.activate
87 | def test_get_favicon_url_falls_back_to_root_favicon_no_pattern():
88 | """In this case, page loads, but 'shortcut icon' pattern not found.
89 |
90 | The get_faviconr should then attempt to connect to site_root/favicon.ico"""
91 | url = "https://aklsdfjaweof.com"
92 | favicon_url = f"{url}/images/favicon.ico"
93 | root_favicon_url = f"{url}/favicon.ico"
94 | favicon_content = b"lol"
95 |
96 | responses.add(
97 | responses.GET,
98 | url,
99 | body=f'',
100 | status=200,
101 | content_type="text/html",
102 | )
103 |
104 | responses.add(
105 | responses.GET,
106 | root_favicon_url,
107 | body=favicon_content,
108 | status=200,
109 | content_type="image/ico",
110 | )
111 |
112 | assert get_favicon(url) == favicon_content
113 |
114 |
115 | @responses.activate
116 | def test_get_favicon_raises_exception_all_strategies_fail():
117 | """Raise BasedException if none of the favicon download methods work."""
118 | url = "https://aklsdfjaweof.com"
119 |
120 | # page response
121 | responses.add(responses.GET, url, body="blah", status=200, content_type="text/html")
122 |
123 | with pytest.raises(BasedException, match=r"Could not retrieve favicon for .*"):
124 | assert get_favicon(url)
125 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | python-version: ["3.10"]
14 | steps:
15 | - uses: actions/checkout@v1
16 |
17 | - name: Filter changed file paths to outputs
18 | uses: dorny/paths-filter@v2.7.0
19 | id: changes
20 | with:
21 | filters: |
22 | root_docs:
23 | - CHANGES
24 | - README.*
25 | docs:
26 | - 'docs/**'
27 | - 'examples/**'
28 | python_files:
29 | - 'django_docutils/**'
30 | - pyproject.toml
31 | - poetry.lock
32 |
33 | - name: Should publish
34 | if: steps.changes.outputs.docs == 'true' || steps.changes.outputs.root_docs == 'true' || steps.changes.outputs.python_files == 'true'
35 | run: echo "PUBLISH=$(echo true)" >> $GITHUB_ENV
36 |
37 | - name: Set up Python ${{ matrix.python-version }}
38 | uses: actions/setup-python@v3
39 | with:
40 | python-version: ${{ matrix.python-version }}
41 |
42 | - name: Get full Python version
43 | id: full-python-version
44 | shell: bash
45 | run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")
46 |
47 | - name: Install poetry
48 | run: |
49 | curl -O -sSL https://install.python-poetry.org/install-poetry.py
50 | python install-poetry.py -y --version 1.1.12
51 | echo "PATH=${HOME}/.poetry/bin:${PATH}" >> $GITHUB_ENV
52 | rm install-poetry.py
53 |
54 | - name: Add ~/.local/bin to PATH
55 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH
56 |
57 | - name: Get poetry cache paths from config
58 | run: |
59 | echo "poetry_virtualenvs_path=$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV
60 | echo "poetry_virtualenvs_path=$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV
61 |
62 | - name: Configure poetry
63 | shell: bash
64 | run: poetry config virtualenvs.in-project true
65 |
66 | - name: Set up cache
67 | uses: actions/cache@v3
68 | id: cache
69 | with:
70 | path: |
71 | .venv
72 | {{ env.poetry_cache_dir }}
73 | {{ env.poetry_virtualenvs_path }}
74 | key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
75 |
76 | - name: Ensure cache is healthy
77 | if: steps.cache.outputs.cache-hit == 'true'
78 | shell: bash
79 | run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv
80 |
81 | - name: Install dependencies [w/ docs]
82 | run: poetry install --extras "docs lint"
83 |
84 | - name: Build documentation
85 | run: |
86 | pushd docs; make SPHINXBUILD='poetry run sphinx-build' html; popd
87 |
88 | - name: Push documentation to S3
89 | uses: jakejarvis/s3-sync-action@v0.5.1
90 | with:
91 | args: --acl public-read --follow-symlinks --delete
92 | env:
93 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
94 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
95 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
96 | AWS_REGION: "us-west-1" # optional: defaults to us-east-1
97 | SOURCE_DIR: "docs/_build/html" # optional: defaults to entire repository
98 |
99 | - name: Purge cache on Cloudflare
100 | uses: jakejarvis/cloudflare-purge-action@v0.3.0
101 | env:
102 | CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
103 | CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }}
104 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import sys
3 |
4 | import py
5 | import pytest
6 |
7 | import factory
8 | from django.apps import apps
9 | from django.contrib.auth import get_user_model
10 | from pytest_factoryboy import register
11 |
12 | from django_docutils.favicon.tests.conftest import RSTPost, favicon_app # NOQA
13 |
14 |
15 | class UserFactory(factory.django.DjangoModelFactory):
16 | class Meta:
17 | model = get_user_model()
18 |
19 | username = factory.Sequence(lambda n: "user%03d" % n)
20 | password = factory.Sequence(lambda n: "pass%03d" % n)
21 |
22 |
23 | register(UserFactory)
24 |
25 |
26 | def create_bare_app(project_tmpdir, request, settings, app_name):
27 | """Create a blank django project for PyTest that cleans out of scope.
28 |
29 | Intended for use inside of PyTest Fixtures.
30 |
31 | :param project_tmpdir: root of the django project
32 |
33 | This cleans up automatically after the fixture falls out of scope.
34 | :type project_tmpdir: :class:`py._path.local.LocalPath`
35 | :param request: PyTest request object (dependency injected)
36 | :type request: :class:`pytest.fixtures.FixtureRequest`
37 | :param settings: django settings fixture (from pytest-django)
38 |
39 | From pytest-django docs:
40 |
41 | "This fixture will provide a handle on the Django settings module, and
42 | automatically revert any changes made to the settings (modifications,
43 | additions and deletions).
44 | :type settings: :class:`pytest_django.fixtures.settings`
45 | :param app_name: application name
46 | :type app_name: str
47 | :rtype: :class:`django.apps.apps.AppConfig`
48 | :returns: a bare app config with temporary file structure created
49 | """
50 |
51 | bare_app = project_tmpdir.mkdir(app_name)
52 |
53 | bare_app.join("__init__.py").write("")
54 |
55 | if project_tmpdir.strpath not in sys.path:
56 | sys.path.append(project_tmpdir.strpath)
57 | settings.INSTALLED_APPS = settings.INSTALLED_APPS + (app_name,)
58 |
59 | def resource_a_teardown():
60 | print("\nresources_a_teardown()")
61 | # todo replace 'bare_app' with app_name
62 | settings.INSTALLED_APPS = (
63 | s for s in settings.INSTALLED_APPS if s != "bare_app"
64 | )
65 |
66 | assert app_name not in settings.INSTALLED_APPS
67 | shutil.rmtree(str(bare_app))
68 |
69 | request.addfinalizer(resource_a_teardown)
70 |
71 | return apps.get_app_config(app_name)
72 |
73 |
74 | @pytest.fixture(scope="function")
75 | def bare_app_config(tmpdir_factory, request, settings):
76 | """Return a Django AppConfig for a blank project.
77 |
78 | It will automatically remove from INSTALLED_APPS and clean created
79 | files on teardown. See :func:`create_bare_app`.
80 |
81 | :rtype: :class:`django.apps.apps.AppConfig`
82 | :returns: a bare app config with temporary file structure created
83 | """
84 |
85 | tmpdir = tmpdir_factory.mktemp("bare_project")
86 |
87 | return create_bare_app(tmpdir, request, settings, "bare_app")
88 |
89 |
90 | @pytest.fixture(scope="function")
91 | def sample_app_config(tmpdir_factory, request, settings):
92 | tmpdir = tmpdir_factory.mktemp("sample_project")
93 |
94 | sample_app = create_bare_app(tmpdir, request, settings, "sample_app")
95 |
96 | sample_app_dir = py.path.local(sample_app.path)
97 |
98 | # give it a fixtures dir
99 | fixtures_dir = sample_app_dir.ensure("fixtures", dir=True)
100 |
101 | fixtures_dir.join("hi.rst").write(
102 | """
103 | ===
104 | moo
105 | ===
106 |
107 | :Author: anonymous
108 | :Slug_Id: tEst
109 |
110 | foo
111 | """.strip()
112 | )
113 | fixtures_dir.ensure("2017", dir=True)
114 | fixtures_dir.join("2017").ensure("06", dir=True)
115 | fixtures_dir.join("2017").join("06").ensure("04", dir=True)
116 | fixtures_dir.join("2017").join("06").join("04").join("moo.rst").write("h")
117 |
118 | return sample_app
119 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/directory/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import py
2 | import pytest
3 |
4 | from django_docutils.lib.fixtures.tests.conftest import create_bare_app
5 |
6 |
7 | @pytest.fixture()
8 | def sample_dir_app_config(tmpdir_factory, request, settings):
9 | """Return a Django AppConfig for a project with a directory-style
10 | fixture inside of it.
11 |
12 | It will automatically remove from INSTALLED_APPS and clean created
13 | files on teardown. See :func:`create_bare_app`.
14 |
15 | :rtype: :class:`django.apps.apps.AppConfig`
16 | :returns: a bare app config with temporary file structure created
17 | """
18 | conf = """{
19 | "Programming_languages": ["python"],
20 | "Topics": ["Web Frameworks"],
21 | "Slug_Id": "Te5t17fr",
22 | "Created": "2017-09-12",
23 | "Author": "tony"
24 | }
25 | """.strip()
26 |
27 | content = """=============================================
28 | Learn JavaScript for free: The best resources
29 | =============================================
30 |
31 | first section
32 | -------------
33 |
34 | some content
35 | """.strip()
36 |
37 | tmpdir = tmpdir_factory.mktemp("sample_dir_project")
38 |
39 | sample_app = create_bare_app(
40 | project_tmpdir=tmpdir,
41 | request=request,
42 | settings=settings,
43 | app_name="sample_dir_app",
44 | )
45 |
46 | sample_app_dir = py.path.local(sample_app.path)
47 |
48 | sample_app_dir.join("__init__.py").write("")
49 |
50 | # give it a fixtures dir
51 | fixtures_dir = sample_app_dir.ensure("fixtures", dir=True)
52 | fixtures_dir.join("hi.rst").write("")
53 | fixtures_dir.ensure("sample_project", dir=True)
54 | fixtures_dir.join("sample_project").join("manifest.json").write(conf)
55 | fixtures_dir.join("sample_project").join("README.rst").write(content)
56 |
57 | return sample_app
58 |
59 |
60 | @pytest.fixture()
61 | def sample_dir_series_app_config(tmpdir_factory, request, settings):
62 | """Returns a "multi-file" directory-style fixture.
63 |
64 | :rtype: :class:`django.apps.apps.AppConfig`
65 | :returns: a bare app config with temporary file structure created
66 | """
67 |
68 | conf = """{
69 | "Programming_languages": ["python"],
70 | "Topics": ["Web Frameworks"],
71 | "Slug_Id": "Te3y28vm",
72 | "Created": "2017-10-20",
73 | "Author": "tony",
74 | "Series": [
75 | "README.rst",
76 | "page2.rst",
77 | "page3.rst"
78 | ]
79 | }
80 | """.strip()
81 |
82 | content = """=============
83 | A test series
84 | =============
85 | hi there
86 | --------
87 |
88 | first section
89 | -------------
90 |
91 | some content
92 | """.strip()
93 |
94 | content2 = """=============
95 | A test series
96 | =============
97 | page 2
98 | ------
99 |
100 | first section of page 2
101 | -----------------------
102 |
103 | some content for page 2
104 | """.strip()
105 |
106 | content3 = """=============
107 | A test series
108 | =============
109 | page 3
110 | ------
111 |
112 | first section of page 3
113 | -----------------------
114 |
115 | some content for page 3
116 | """.strip()
117 |
118 | BASE_FOLDER = "sample_dir_series_project1"
119 |
120 | tmpdir = tmpdir_factory.mktemp(BASE_FOLDER)
121 |
122 | sample_app = create_bare_app(tmpdir, request, settings, "sample_dir_series_app")
123 |
124 | sample_app_dir = py.path.local(sample_app.path)
125 |
126 | sample_app_dir.join("__init__.py").write("")
127 |
128 | # give it a fixtures dir
129 | fixtures_dir = sample_app_dir.ensure("fixtures", dir=True)
130 |
131 | # the app's fixtures/ dir
132 | fixtures_dir.join("hi.rst").write("")
133 | fixtures_dir.mkdir(BASE_FOLDER)
134 |
135 | # The RST project inside the app's fixtures dir
136 | project_dir = fixtures_dir.join(BASE_FOLDER)
137 | project_dir.join("manifest.json").write(conf)
138 | project_dir.join("README.rst").write(content)
139 | project_dir.join("page2.rst").write(content2)
140 | project_dir.join("page3.rst").write(content3)
141 |
142 | return sample_app
143 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/common.py:
--------------------------------------------------------------------------------
1 | from docutils import nodes, utils
2 |
3 | from ..utils import split_explicit_title
4 |
5 |
6 | def generic_url_role(name, text, url_handler_fn, innernodeclass=nodes.Text):
7 | """This cleans up a lot of code we had to repeat over and over.
8 |
9 | This generic role also handles explicit titles (:role:`yata yata `)
10 |
11 | This breaks convention a feels a bit jenky at first. It uses a callback
12 | because figuring out the url is the only magic that happens, but its
13 | sandwiched in the middle.
14 |
15 | :param name: name of the role, e.g. 'github'
16 | :type name: string
17 | :param text: text inside of the role, e.g:
18 | - 'airline-mode/airline-mode'
19 | - 'this repo '
20 | :type text: string
21 | :param url_handler_fn: a function that accepts the target param, example:
22 | :returntype url_handler_fn: string
23 | :returns: tuple ([node], [])
24 | :returntype: tuple
25 |
26 | Simple example, let's create a role::
27 |
28 | .. code-block:: python
29 |
30 | def github_role(
31 | name, rawtext, text, lineno, inliner, options={}, content=[]
32 | ):
33 | def url_handler(target):
34 | return 'https://github.com/{}'.format(target)
35 |
36 | return generic_url_role(name, text, url_handler)
37 |
38 | roles.register_local_role('gh', github_role)
39 | """
40 | name = name.lower()
41 | has_explicit_title, title, target = split_explicit_title(text)
42 | title = utils.unescape(title)
43 | target = utils.unescape(target)
44 |
45 | if not has_explicit_title:
46 | title = utils.unescape(title)
47 | else:
48 | if "**" == title[:2] and "**" == title[-2:]:
49 | innernodeclass = nodes.strong
50 | title = title.strip("**")
51 | elif "*" == title[0] and "*" == title[-1]:
52 | innernodeclass = nodes.emphasis
53 | title = title.strip("*")
54 |
55 | url = url_handler_fn(target)
56 |
57 | sn = innernodeclass(title, title)
58 | rn = nodes.reference("", "", internal=True, refuri=url, classes=[name])
59 | rn += sn
60 | return [rn], []
61 |
62 |
63 | def generic_remote_url_role(name, text, url_handler_fn, innernodeclass=nodes.Text):
64 | """This is a generic_url_role that can return a url AND a title remotely
65 |
66 | The url_handler_fn returns a title and a url
67 |
68 | In cases like Amazon API, database lookups, and other stuff, information
69 | may be looked up by key, and we may get a fresh title to fill in if nothing
70 | else explicit is mentioned.
71 |
72 | :param name: name of the role, e.g. 'github'
73 | :type name: string
74 | :param text: text inside of the role, e.g:
75 | - 'airline-mode/airline-mode'
76 | - 'this repo '
77 | :type text: string
78 | :param url_handler_fn: a function that accepts the target param, example:
79 | :returntype url_handler_fn: (string, string)
80 | :returns: tuple ([node], [])
81 | :returntype: tuple
82 |
83 | Simple example, let's create a role::
84 |
85 | .. code-block:: python
86 |
87 | def amzn_role(
88 | name, rawtext, text, lineno, inliner, options={}, content=[]
89 | ):
90 | def url_handler(target):
91 | query = amzn.lookup(ItemId=target)
92 | return query.title, query.offer_url
93 |
94 | return generic_remote_url_role(name, text, url_handler)
95 |
96 | roles.register_local_role('amzn', amzn_role)
97 | """
98 | name = name.lower()
99 | has_explicit_title, title, target = split_explicit_title(text)
100 | title = utils.unescape(title)
101 | target = utils.unescape(target)
102 |
103 | remote_title, url = url_handler_fn(target)
104 | if not has_explicit_title:
105 | title = utils.unescape(remote_title)
106 |
107 | sn = innernodeclass(title, title)
108 | rn = nodes.reference("", "", internal=True, refuri=url, classes=[name])
109 | rn += sn
110 | return [rn], []
111 |
--------------------------------------------------------------------------------
/django_docutils/references/models.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps as django_apps
2 | from django.conf import settings
3 | from django.core.exceptions import ImproperlyConfigured
4 | from django.db import models
5 | from django.utils.translation import gettext_lazy as _
6 |
7 |
8 | def get_reference_model():
9 | try:
10 | return django_apps.get_model(
11 | settings.BASED_REFERENCE_MODEL, require_ready=False
12 | )
13 | except ValueError:
14 | raise ImproperlyConfigured(
15 | "BASED_REFERENCE_MODEL must be of the form 'app_label.model_name'"
16 | )
17 | except LookupError:
18 | raise ImproperlyConfigured(
19 | "BASED_REFERENCE_MODEL refers to model '%s' that has not been installed"
20 | % settings.BASED_REFERENCE_MODEL
21 | )
22 |
23 |
24 | class ReferenceBase(models.Model):
25 |
26 | r"""Reference for targets, including the intersphinx network.
27 |
28 | :py:mod:`py3k:module2`
29 | | | |
30 | | | ---- target
31 | | -- type |
32 | |-- domain py3k:module2
33 | | |
34 | setname --| |-- settarget
35 |
36 | An intersphinx collection can link to a "set", or a remote manifest of
37 | many references, such as in Python, Sphinx, or SQLAlchemy's documentation.
38 |
39 | While vanilla docutils only has targets for its local documents, sphinx
40 | creates a manifest of targets project-wide. It collects these in an
41 | "inventory", which we namespace through : in the "target", e.g.
42 | "py3k:collections", where py3k is the namespace INTERSPHINX_MAPPING pointed
43 | to python 3's documentation on the internet. e.g.:
44 |
45 | intersphinx_mapping = {
46 | 'https://docs.python.org/': inv_file,
47 | 'py3k': ('https://docs.python.org/py3k/', inv_file),
48 | }
49 |
50 | See how there's also an item with setname (docs.python.org, the first
51 | entry). Sphinx allows this, so items which don't show a setname for the
52 | target (e.g. "collections", without the "py3k:" namespace) will refer to
53 | targets in https://docs.python.org instead of https://docs.python.org/py3k/
54 |
55 | Note: intersphinx represents the nameless inventory to be the "main
56 | inventory". And the others as "name inventories".
57 |
58 | Also of note is the domain and type, e.g. (:py:mod:). By default, sphinx
59 | uses the python domain, so you could actually do :mod: without the :py:.
60 | e.g. \:mod\:`collections` instead of \:py\:mod\:`collections`.
61 |
62 | However, it's still important to store and collect the domain and type,
63 | since we'll want to reference multiple programming languages. In addition,
64 | despite the practice of intersphinx_mapping to have a default set with no
65 | set name, and a default fallback domain, it may be more correct in the long
66 | term to enforce explicit domains and sets.
67 | """
68 |
69 | domain = models.CharField(
70 | _('Sphinx domain of the reference. e.g. "py" or "std"'), max_length=255
71 | )
72 | type = models.CharField(
73 | _('Type of object being linked to, e.g. "mod", "func", "option"'),
74 | max_length=255,
75 | )
76 | project = models.CharField(
77 | _("Project, e.g. py3k, python, sqlalchemy"), max_length=255
78 | )
79 | project_version = models.CharField(
80 | _("Version of project documentation"), max_length=255
81 | )
82 | target = models.CharField(_("Docutils/Sphinx target"), max_length=255)
83 | uri = models.URLField(_("Link to reference"), max_length=255)
84 | display_name = models.CharField(
85 | _("Optional name for the reference item"), max_length=255, null=True
86 | )
87 |
88 | @property
89 | def full_target(self):
90 | return f"{self.project}:{self.target}"
91 |
92 | @property
93 | def full_reference(self):
94 | if self.domain:
95 | return ":{}:{}:`{}:{}`".format(
96 | self.domain, self.type, self.project, self.full_target
97 | )
98 | else:
99 | return f":{self.type}:`{self.target}`"
100 |
101 | class Meta:
102 | unique_together = ("project", "target", "type")
103 | abstract = True
104 |
105 | def __str__(self):
106 | return self.full_reference
107 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # flake8: noqa E501
3 | import os
4 | import sys
5 | from pathlib import Path
6 |
7 | # Get the project root dir, which is the parent dir of this
8 | cwd = Path.cwd()
9 | project_root = cwd.parent
10 |
11 | sys.path.insert(0, str(project_root))
12 | sys.path.insert(0, str(cwd / "_ext"))
13 |
14 |
15 | # package data
16 | about = {}
17 | with open("../django_docutils/__about__.py") as fp:
18 | exec(fp.read(), about)
19 |
20 | extensions = [
21 | "sphinx.ext.autodoc",
22 | "sphinx.ext.intersphinx",
23 | "sphinx.ext.napoleon",
24 | "sphinx_autodoc_typehints",
25 | "sphinx_issues",
26 | "sphinx_click.ext", # sphinx-click
27 | "sphinx_inline_tabs",
28 | "sphinx_copybutton",
29 | "sphinxext.opengraph",
30 | "sphinxext.rediraffe",
31 | "myst_parser",
32 | ]
33 | myst_enable_extensions = ["colon_fence", "substitution", "replacements"]
34 |
35 | issues_github_path = about["__github__"].replace("https://github.com/", "")
36 |
37 | templates_path = ["_templates"]
38 |
39 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"}
40 |
41 | master_doc = "index"
42 |
43 | project = about["__title__"]
44 | copyright = about["__copyright__"]
45 |
46 | version = "%s" % (".".join(about["__version__"].split("."))[:2])
47 | release = "%s" % (about["__version__"])
48 |
49 | exclude_patterns = ["_build"]
50 |
51 | pygments_style = "monokai"
52 | pygments_dark_style = "monokai"
53 |
54 | html_favicon = "_static/favicon.ico"
55 | html_static_path = ["_static"]
56 | html_css_files = ["css/custom.css"]
57 | html_extra_path = ["manifest.json"]
58 | html_theme = "furo"
59 | html_theme_options = {
60 | "light_logo": "img/icons/logo.svg",
61 | "dark_logo": "img/icons/logo.svg",
62 | "footer_icons": [
63 | {
64 | "name": "GitHub",
65 | "url": about["__github__"],
66 | "html": """
67 |
70 | """,
71 | "class": "",
72 | },
73 | ],
74 | }
75 |
76 | html_sidebars = {
77 | "**": [
78 | "sidebar/scroll-start.html",
79 | "sidebar/brand.html",
80 | "sidebar/search.html",
81 | "sidebar/navigation.html",
82 | "sidebar/projects.html",
83 | "sidebar/scroll-end.html",
84 | ]
85 | }
86 |
87 | # sphinxext.opengraph
88 | ogp_site_url = about["__docs__"]
89 | ogp_image = "_static/img/icons/icon-192x192.png"
90 | ogp_desscription_length = about["__description__"]
91 | ogp_site_name = about["__title__"]
92 |
93 | # sphinx-copybutton
94 | copybutton_prompt_text = (
95 | r">>> |\.\.\. |> |\$ |\# | In \[\d*\]: | {2,5}\.\.\.: | {5,8}: "
96 | )
97 | copybutton_prompt_is_regexp = True
98 | copybutton_remove_prompts = True
99 |
100 | # sphinxext-rediraffe
101 | rediraffe_redirects = "redirects.txt"
102 | rediraffe_branch = "master~1"
103 |
104 |
105 | htmlhelp_basename = "%sdoc" % about["__title__"]
106 |
107 | latex_documents = [
108 | (
109 | "index",
110 | "{0}.tex".format(about["__package_name__"]),
111 | "{0} Documentation".format(about["__title__"]),
112 | about["__author__"],
113 | "manual",
114 | ),
115 | ]
116 |
117 | man_pages = [
118 | (
119 | "index",
120 | about["__package_name__"],
121 | "{0} Documentation".format(about["__title__"]),
122 | about["__author__"],
123 | 1,
124 | ),
125 | ]
126 |
127 | texinfo_documents = [
128 | (
129 | "index",
130 | "{0}".format(about["__package_name__"]),
131 | "{0} Documentation".format(about["__title__"]),
132 | about["__author__"],
133 | about["__package_name__"],
134 | about["__description__"],
135 | "Miscellaneous",
136 | ),
137 | ]
138 |
139 | intersphinx_mapping = {
140 | "python": ("http://docs.python.org/", None),
141 | }
142 |
--------------------------------------------------------------------------------
/django_docutils/favicon/prefetch.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import tldextract
4 | from django.core.files.uploadedfile import SimpleUploadedFile
5 | from tqdm import tqdm, trange
6 |
7 | from django_docutils.favicon.models import get_favicon_model
8 | from django_docutils.favicon.rst.transforms.favicon import plain_references
9 | from django_docutils.favicon.scrape import get_favicon
10 |
11 | logger = logging.getLogger(__name__)
12 | Favicon = get_favicon_model()
13 |
14 |
15 | def yield_references(document, url_pattern=None):
16 | """Yield site pages in a docutils document format
17 |
18 | :param document:
19 | :type document: :class:`docutils.nodes.document`
20 | :rtype: string
21 | :yields: Document of pages in side
22 | """
23 | nodes = document.traverse(plain_references)
24 | for node in nodes:
25 | if url_pattern: # if --pattern entered
26 | if url_pattern not in node["refuri"]:
27 | continue
28 |
29 | yield node["refuri"]
30 |
31 |
32 | def prefetch_favicons(url_pattern=None, PostPage=None):
33 | # PostPage=get_postpage_models()[0]
34 | urls = []
35 | t = trange(PostPage.objects.count())
36 |
37 | # iterate through all page documents on the sites
38 | for page_document in yield_page_doctrees(PostPage):
39 | # iterate through all references inside the document
40 | urls.extend(yield_references(page_document, url_pattern))
41 | t.update(1)
42 |
43 | t = tqdm(urls)
44 | for url in t:
45 | prefetch_favicon(url, progress=t)
46 |
47 |
48 | def is_favicon_stored(fqdn):
49 | """Assure the favicon *and its file* exist in storage
50 |
51 | This is split up for testing critical logic. There is a race condition
52 | where the object exists in the database, but the file in not in storage.
53 |
54 | prefetch_favicon must be resilient enough to download the image if its
55 | not existing in storage.
56 | """
57 | # don't redown if fqdn favicon already exists
58 | try:
59 | favicon = Favicon.objects.get(domain=fqdn)
60 |
61 | # check for the file itself
62 | try:
63 | if favicon.favicon.file:
64 | logger.debug(
65 | "{} for {} already exists, skipping".format(
66 | favicon.favicon.file, fqdn
67 | )
68 | )
69 | return True # Favicon for fqdn + file already exists
70 | except FileNotFoundError:
71 | pass # that's fine, we'll download it again!
72 | except Favicon.DoesNotExist:
73 | pass # that's fine! we'll add it
74 |
75 | return False
76 |
77 |
78 | def prefetch_favicon(url, progress=None):
79 | """Download a store a favicon for a site, if it exists.
80 |
81 | Intended for use when running in situ transforms on :class:`
82 | docutils.nodes.reference` traversals of URL's. This will look up the
83 | fqdn (fully qualified domain name) for a URL, check against the Favicon
84 | models for any results, if they don't exist, it'll try to scrape the
85 | favicon from the fqdn, store it, and store the path in a Favicon entry for
86 | that fqdn.
87 |
88 | :param url: URL to any page on a website
89 | :type url: str
90 | :rtype: (model, created date)|None
91 | """
92 | fqdn = tldextract.extract(url).fqdn
93 |
94 | # optional tqdm progress bar pass-in
95 | if progress:
96 | progress.set_description(f"Downloading favicon {fqdn}")
97 |
98 | if is_favicon_stored(fqdn): # don't redownload
99 | return
100 |
101 | try:
102 | favicon_content = get_favicon(url)
103 | except Exception as e:
104 | logger.debug(f"Error occurred fetch icon for {fqdn}: {e}")
105 | return
106 |
107 | return Favicon.objects.update_or_create(
108 | domain=fqdn,
109 | defaults={
110 | "domain": fqdn,
111 | "favicon": SimpleUploadedFile(
112 | name=f"{fqdn}.ico",
113 | content=favicon_content,
114 | content_type="image/ico",
115 | ),
116 | },
117 | )
118 |
119 |
120 | def yield_page_doctrees(PostPage):
121 | """Yield site pages in a docutils document format
122 |
123 | :param PostPage: Any model implementing PostPageBase
124 | :type: :class:`django_docutils.rst_post.models.RSTPostPageBase`
125 | :yields: Document of pages in side
126 | :rtype: :class:`docutils.nodes.document`
127 | """
128 | for page in PostPage.objects.all():
129 | yield page.document
130 |
--------------------------------------------------------------------------------
/django_docutils/lib/fixtures/tests/test_load.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import pytest
4 |
5 | import pytz
6 | from django.conf import settings
7 |
8 | from django_docutils.lib.fixtures.load import load_app_rst_fixtures, load_post_data
9 | from django_docutils.lib.fixtures.publisher import publish_post
10 |
11 |
12 | @pytest.mark.django_db(transaction=True)
13 | def test_load_post_explicitness_persists(bare_app_config, RSTPost):
14 | sample_page_body = """
15 | ==============
16 | Document title
17 | ==============
18 | -----------------
19 | Document subtitle
20 | -----------------
21 |
22 | :Slug_Id: tEst
23 | :Author: anonymous
24 |
25 | Content
26 | -------
27 |
28 | hi
29 | """.strip()
30 |
31 | post_data = publish_post(sample_page_body)
32 |
33 | # assert default behavior
34 | post = load_post_data(RSTPost, post_data)
35 |
36 | assert post.title == "Document title"
37 | assert post.pages.first().subtitle == "Document subtitle"
38 | assert post.slug_id == "tEst"
39 |
40 |
41 | @pytest.mark.django_db(transaction=True)
42 | def test_correct_date_on_initial_and_reimport(RSTPost):
43 | # check tests
44 | sample_page_body = """
45 | ==============
46 | Document title
47 | ==============
48 | -----------------
49 | Document subtitle
50 | -----------------
51 |
52 | :Slug_Id: tEst2
53 | :Author: anonymous
54 | :created: 2017-01-25
55 |
56 | Content
57 | -------
58 |
59 | hi
60 | """.strip()
61 |
62 | post_data = publish_post(sample_page_body)
63 |
64 | assert post_data["created"].year == 2017
65 | assert post_data["created"].month == 1
66 | assert post_data["created"].day == 25
67 |
68 | # assert default behavior
69 | post = load_post_data(RSTPost, post_data)
70 |
71 | assert post.created.year == 2017
72 | assert post.created.month == 1
73 | assert post.created.day == 25
74 |
75 | # simulate re-import
76 | post = load_post_data(RSTPost, post_data)
77 |
78 | assert post.created.year == 2017
79 | assert post.created.month == 1
80 | assert post.created.day == 25
81 |
82 | now = datetime.datetime.now(pytz.timezone("UTC"))
83 |
84 | assert post.modified.year == now.year
85 | assert post.modified.month == now.month
86 | assert post.modified.day == now.day
87 |
88 |
89 | @pytest.mark.django_db(transaction=True)
90 | def test_correct_slug_title_on_initial_and_reimport(RSTPost):
91 | slug_title = "my_brief_url"
92 | sample_page_body = """
93 | ==============
94 | Document title
95 | ==============
96 | -----------------
97 | Document subtitle
98 | -----------------
99 |
100 | :Slug_Id: tEst2
101 | :Slug_title: {slug_title}
102 | :Author: anonymous
103 | :created: 2017-01-25
104 |
105 | Content
106 | -------
107 |
108 | hi
109 | """.format(
110 | slug_title=slug_title
111 | ).strip()
112 |
113 | post_data = publish_post(sample_page_body)
114 |
115 | assert post_data["slug_title"] == slug_title
116 |
117 | # assert default behavior
118 | post = load_post_data(RSTPost, post_data)
119 |
120 | assert post.slug_title == slug_title
121 |
122 | # simulate re-import
123 | post = load_post_data(RSTPost, post_data)
124 |
125 | assert post.slug_title == slug_title
126 |
127 |
128 | @pytest.mark.django_db(transaction=True)
129 | def test_slug_title_updates_on_reimport(RSTPost):
130 | sample_page_body = """
131 | ==============
132 | Document title
133 | ==============
134 | -----------------
135 | Document subtitle
136 | -----------------
137 |
138 | :Slug_Id: tEst2
139 | :Author: anonymous
140 | :created: 2017-01-25
141 |
142 | Content
143 | -------
144 |
145 | hi
146 | """.strip()
147 | from django_slugify_processor.text import slugify
148 |
149 | post_data = publish_post(sample_page_body)
150 |
151 | # assert default behavior
152 | post = load_post_data(RSTPost, post_data)
153 | assert post.slug_title == slugify(post_data["title"])
154 |
155 | # simulate re-import with updated title
156 | new_title = "a new title yay"
157 | new_slug_title = slugify(new_title)
158 | post_data["title"] = new_title
159 | post = load_post_data(RSTPost, post_data)
160 |
161 | assert post.slug_title == new_slug_title
162 |
163 |
164 | @pytest.mark.django_db(transaction=True)
165 | def test_load_app_rst_fixtures_minimal(sample_app_config, RSTPost):
166 | load_app_rst_fixtures(sample_app_config, model=RSTPost)
167 |
168 |
169 | @pytest.mark.django_db(transaction=True)
170 | def test_load_post_data_minimal(RSTPost):
171 | pages = [{"body": "moo", "subtitle": "my subtitle"}]
172 |
173 | post = load_post_data(
174 | RSTPost,
175 | {"author": settings.ANONYMOUS_USER_NAME, "slug_id": "aFeuM8e", "pages": pages},
176 | )
177 |
178 | assert isinstance(post, RSTPost)
179 |
--------------------------------------------------------------------------------
/django_docutils/lib/transforms/code.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from docutils import nodes
4 | from docutils.transforms import Transform
5 | from pygments import highlight
6 | from pygments.formatters.html import HtmlFormatter
7 | from pygments.token import Token
8 |
9 |
10 | class InlineHtmlFormatter(HtmlFormatter):
11 | def format_unencoded(self, tokensource, outfile):
12 | """
13 |
14 | First problem (filter trailing newline):
15 |
16 | There is an issue where the final token generated returns a
17 | (Token.Other, '\n') which results in a blank space
18 |
19 |
20 | This would otherwise be unnoticeable if it was a code block,
21 | but since we're inline, it looks strange. Let's filter out
22 | any trailing newlines from token source, then fallback to
23 | the normal process by passing it back into parent class method.
24 |
25 | Now, AFTER this method, you're not out of the woods yet:
26 | _format_lines will still add a \n (which renders as a space again
27 | in the browser). For that, pass lineseparator='' into the
28 | InlineHtmlFormatter class to supress that.
29 |
30 | """
31 |
32 | def filter_trailing_newline(source):
33 | tokens = list(source)
34 |
35 | # filter out the trailing newline token
36 | if tokens[-1] == (Token.Text, "\n"):
37 | del tokens[-1]
38 |
39 | return ((t, v) for t, v in tokens)
40 |
41 | source = filter_trailing_newline(tokensource)
42 |
43 | return super().format_unencoded(source, outfile)
44 |
45 | def _wrap_div(self, inner):
46 | style = []
47 | if (
48 | self.noclasses
49 | and not self.nobackground
50 | and self.style.background_color is not None
51 | ):
52 | style.append(f"background: {self.style.background_color}")
53 | if self.cssstyles:
54 | style.append(self.cssstyles)
55 | style = "; ".join(style)
56 |
57 | yield 0, (
58 | ""
62 | )
63 | yield from inner
64 | yield 0, "\n"
65 |
66 | def _wrap_pre(self, inner):
67 | yield from inner
68 |
69 |
70 | formatter = InlineHtmlFormatter(
71 | cssclass="highlight docutils literal inline-code",
72 | noclasses=False,
73 | lineseparator="", # removes \n from end of inline snippet
74 | )
75 |
76 |
77 | class CodeTransform(Transform):
78 |
79 | """Run over unparsed literals and try to guess language + highlight."""
80 |
81 | default_priority = 120
82 |
83 | def apply(self):
84 | paragraph_nodes = self.document.traverse(nodes.literal)
85 |
86 | for node in paragraph_nodes:
87 | text = node.astext()
88 |
89 | newnode = None
90 | newtext = None
91 | newlexer = None
92 |
93 | if text.startswith("$ "):
94 | from django_docutils.lib.directives.code import BashSessionLexer
95 |
96 | newlexer = BashSessionLexer()
97 | elif text.startswith("{%") or text.startswith("{{"):
98 | from pygments.lexers.templates import DjangoLexer
99 |
100 | newlexer = DjangoLexer()
101 | elif re.match(r"^:\w+:", text): # match :rolename: beginning
102 | from pygments.lexers.markup import RstLexer
103 |
104 | newlexer = RstLexer()
105 | else:
106 | from pygments.lexers import guess_lexer
107 | from pygments.lexers.mime import MIMELexer
108 | from pygments.lexers.special import TextLexer
109 |
110 | guess = guess_lexer(text)
111 | if not any(guess.__class__ != lex for lex in [MIMELexer, TextLexer]):
112 | newlexer = guess
113 |
114 | if newlexer:
115 | """Inline code can't have newlines, but we still get them:
116 |
117 | Take this reStructuredText code:
118 |
119 | .. code-block:: reStructuredText
120 |
121 | You can set options with ``$ tmux set-option`` and ``$ tmux
122 | set-window-option``.
123 |
124 | Docutils detects the separation between "tmux" and
125 | "set-window-option" in ``$ tmux set-window-options``, now as a
126 | space, but a *new line*.
127 |
128 | Let's replace the newline escape (``\n``) with a space.
129 | """
130 | text = text.strip() # trim any whitespace around text
131 | text = text.replace("\n", " ") # switch out newlines w/ space
132 |
133 | newtext = highlight(text, newlexer, formatter)
134 |
135 | if newtext:
136 | newnode = nodes.raw("", newtext, format="html")
137 |
138 | if newnode and node.parent:
139 | node.replace_self(newnode)
140 |
--------------------------------------------------------------------------------
/django_docutils/favicon/tests/test_prefetch.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import responses
4 | from django.core.files.uploadedfile import SimpleUploadedFile
5 | from docutils.nodes import document
6 | from inmemorystorage.storage import InMemoryFile
7 |
8 | from django_docutils.favicon.prefetch import (
9 | is_favicon_stored,
10 | prefetch_favicon,
11 | yield_page_doctrees,
12 | yield_references,
13 | )
14 | from django_docutils.lib.publisher import publish_doctree
15 |
16 | TEST_RST_DOCUMENT = """
17 | Developing
18 | ==========
19 |
20 | - Make a hobby website in django or flask.
21 |
22 | Services like `Heroku`_ are free to try, and simple to deploy Django
23 | websites to.
24 |
25 | - For free editors, check out good old `vim`_, `Visual Studio Code`_,
26 | `Atom`_, or `PyCharm`_
27 |
28 | .. _Visual Studio Code: https://code.visualstudio.com/
29 | .. _Atom: https://atom.io/
30 | .. _vim: http://vim.org
31 | .. _PyCharm: https://www.jetbrains.com/pycharm/
32 | """
33 |
34 |
35 | def test_yield_references():
36 | document = publish_doctree(TEST_RST_DOCUMENT)
37 | assert set(yield_references(document)) == {
38 | "https://code.visualstudio.com/",
39 | "https://atom.io/",
40 | "http://vim.org",
41 | "https://www.jetbrains.com/pycharm/",
42 | }
43 |
44 |
45 | def test_yield_references_patterns():
46 | document = publish_doctree(TEST_RST_DOCUMENT)
47 | assert set(yield_references(document, url_pattern="atom")) == {"https://atom.io/"}
48 |
49 |
50 | @pytest.mark.django_db(transaction=True)
51 | def test_yield_page_doctrees(RSTPostPage):
52 | RSTPostPage.objects.create(subtitle="lol", body=TEST_RST_DOCUMENT)
53 | assert RSTPostPage.objects.filter(subtitle="lol").count()
54 |
55 | page_doctrees = list(yield_page_doctrees(RSTPostPage))
56 | assert len(page_doctrees)
57 |
58 | for page in page_doctrees:
59 | assert isinstance(page, document)
60 |
61 |
62 | @pytest.mark.django_db(transaction=True)
63 | @responses.activate
64 | def test_prefetch_favicon_working():
65 | url = "http://vim.org"
66 | favicon_url = f"{url}/images/favicon.ico"
67 | favicon_content = b"lol"
68 |
69 | responses.add(
70 | responses.GET,
71 | url,
72 | body=''.format(
73 | favicon_url=favicon_url
74 | ),
75 | status=200,
76 | content_type="text/html",
77 | )
78 |
79 | responses.add(
80 | responses.GET,
81 | favicon_url,
82 | body=favicon_content,
83 | status=200,
84 | content_type="image/ico",
85 | )
86 |
87 | favicon, created = prefetch_favicon(url)
88 |
89 | assert favicon.favicon.read() == favicon_content
90 |
91 |
92 | @pytest.mark.django_db(transaction=True)
93 | @responses.activate
94 | def test_prefetch_favicon_file_missing(monkeypatch):
95 | # case where the favicon is in ORM, but file not in storage
96 | url = "http://vim.org"
97 | favicon_url = f"{url}/images/favicon.ico"
98 | favicon_content = b"lol"
99 |
100 | responses.add(
101 | responses.GET,
102 | url,
103 | body=''.format(
104 | favicon_url=favicon_url
105 | ),
106 | status=200,
107 | content_type="text/html",
108 | )
109 |
110 | responses.add(
111 | responses.GET,
112 | favicon_url,
113 | body=favicon_content,
114 | status=200,
115 | content_type="image/ico",
116 | )
117 |
118 | def mock_file():
119 | raise FileNotFoundError
120 |
121 | favicon, created = prefetch_favicon(url)
122 |
123 | import django_docutils.favicon.prefetch
124 |
125 | assert not prefetch_favicon(url)
126 | monkeypatch.setattr(
127 | django_docutils.favicon.prefetch, "is_favicon_stored", lambda fqdn: False
128 | )
129 |
130 | favicon, created = prefetch_favicon(url)
131 | assert not created
132 |
133 |
134 | @pytest.mark.django_db(transaction=True)
135 | @responses.activate
136 | def test_is_favicon_stored_file_missing(monkeypatch, Favicon):
137 | # case where the favicon is in ORM, but file not in storage
138 | url = "http://vim.org"
139 | fqdn = "vim.org"
140 |
141 | def mock_open(path, mode="r"):
142 | raise FileNotFoundError
143 |
144 | favicon = Favicon.objects.create(
145 | domain=fqdn,
146 | favicon=SimpleUploadedFile(
147 | name=f"{fqdn}.ico",
148 | content=b"lol",
149 | content_type="image/ico",
150 | ),
151 | )
152 |
153 | assert not prefetch_favicon(url), "File should not redownload"
154 |
155 | monkeypatch.setattr(InMemoryFile, "open", mock_open)
156 | with pytest.raises(FileNotFoundError): # Assure monkeypatch
157 | favicon.favicon.file
158 |
159 | assert not is_favicon_stored(
160 | favicon.domain
161 | ), "favicon missing from storage should return False"
162 |
163 |
164 | @pytest.mark.django_db(transaction=True)
165 | @responses.activate
166 | def test_is_favicon_stored_favicon_not_in_db(monkeypatch):
167 | assert not is_favicon_stored(
168 | "nonexistant_fqdn.com"
169 | ), "favicon missing from database should return False"
170 |
--------------------------------------------------------------------------------
/django_docutils/lib/roles/__init__.py:
--------------------------------------------------------------------------------
1 | import inspect
2 |
3 | from django.utils.module_loading import import_string
4 | from docutils.parsers.rst import roles
5 |
6 | from django_docutils.references.rst.roles import XRefRole
7 |
8 | from ..settings import BASED_LIB_RST
9 |
10 |
11 | def register_based_roles():
12 | """Register all roles, exists to avoid race conditions / pulling in deps.
13 |
14 | This makes based a lot leaner by making roles explicit and "opt-in".
15 |
16 | Why? Not all django projects want need intersphinx cross-referencing
17 | or amazon links (which requires bitly and an amazon product api package).
18 | Let's use a TEMPLATES-style django config::
19 |
20 | BASED_LIB_RST = {
21 | 'roles': { #: directive-name: Directive class (import string)
22 | 'local': { #: roles.register_local_role
23 | # below: same as
24 | # roles.register_local_role('gh', github_role)
25 | 'gh': 'django_docutils.lib.roles.github.git_role',
26 | 'pypi': 'django_docutils.lib.roles.pypi.pypi_role',
27 | },
28 | 'canonical': { #: roles.register_canonical_role
29 | # below: same as:
30 | # roles.register_canonical_role('class', PyXRefRole())
31 | 'class': 'django_docutils.lib.roles.xref.PyXRefRole',
32 |
33 | # below: same as
34 | # roles.register_canonical_role(
35 | # 'ref',
36 | # XRefRole(
37 | # lowercase=True, innernodeclass=nodes.inline,
38 | # warn_dangling=True
39 | # )
40 | # )
41 | # See nodes.inline will be resolved
42 | 'ref': (
43 | 'django_docutils.lib.roles.xref.XRefRole',
44 | {
45 | 'lowercase': True,
46 | 'innernodeclass': 'docutils.nodes.inline',
47 | 'warn_dangling': True
48 | }
49 | ),
50 | 'meth': (
51 | 'django_docutils.lib.roles.xref.PyXRefRole',
52 | {
53 | 'fix_parens': True
54 | }
55 | ),
56 | }
57 | }
58 | }
59 | """
60 | if not BASED_LIB_RST:
61 | return
62 |
63 | if "roles" not in BASED_LIB_RST:
64 | return
65 |
66 | based_roles = BASED_LIB_RST["roles"]
67 |
68 | local_roles = based_roles.get("local", None)
69 |
70 | if local_roles:
71 | register_role_mapping(local_roles)
72 |
73 |
74 | def register_role_mapping(role_mapping):
75 | """Register a dict mapping of roles
76 |
77 | An item consists of a role name, import string to a callable, and an
78 | optional mapping of keyword args for special roles that are classes
79 | that can accept arguments.
80 |
81 | The term inside 'cb' is short for callback/callable. Since the string can
82 | be any callable object: a function or class.
83 |
84 | :param role_mapping:
85 | :type role_mapping: dict
86 | :rtype: void
87 | :returns: Nothing
88 | """
89 |
90 | for role_name, role_cb_str in role_mapping.items():
91 | role_cb_kwargs = {}
92 |
93 | if isinstance(role_cb_str, tuple):
94 | # (
95 | # 'django_docutils.lib.roles.xref.PyXRefRole', # role_cb_str
96 | # { # role_cb_kwargs
97 | # 'fix_parens': True
98 | # }
99 | # ),
100 |
101 | # pop off dict of kwargs
102 | role_cb_kwargs = role_cb_str[1]
103 |
104 | # move class string item to a pure string
105 | role_cb_str = role_cb_str[0]
106 |
107 | # One more check, we may have an innernodeclass that needs
108 | # to be resolved, e.g.
109 | # (
110 | # 'django_docutils.lib.roles.xref.XRefRole',
111 | # {
112 | # 'lowercase': True,
113 | # 'innernodeclass': 'docutils.nodes.inline',
114 | # 'warn_dangling': True
115 | # }
116 | # ),
117 | if "innernodeclass" in role_cb_kwargs and isinstance(
118 | role_cb_kwargs["innernodeclass"], str
119 | ):
120 | role_cb_kwargs["innernodeclass"] = import_string(
121 | role_cb_kwargs["innernodeclass"]
122 | )
123 |
124 | # Docutils roles accept a function or callable class as a callback
125 | role_ = import_string(role_cb_str)
126 |
127 | # Stuff like cross-reference roles, which are derived from sphinx work
128 | # differently. Unlike normal function roles, these roles are classes
129 | # passed in instantiated.
130 | #
131 | # If they include kwargs, they are entered as a tuple with a second
132 | # element that's a dict of the kwargs passed into the role.
133 | if inspect.isclass(role_) and issubclass(role_, XRefRole):
134 | if role_cb_kwargs:
135 | roles.register_local_role(role_name, role_(**role_cb_kwargs))
136 | else:
137 | roles.register_local_role(role_name, role_())
138 | else:
139 | roles.register_local_role(role_name, role_)
140 |
--------------------------------------------------------------------------------