├── .github └── workflows │ ├── autoversion.yaml │ ├── publish.yaml │ └── tests.yaml ├── .gitignore ├── LICENSE ├── README.md ├── docker ├── Dockerfile └── unit_config.json ├── docs └── screenview.png ├── manage.py ├── pyproject.toml ├── requirements └── base.in ├── schema_viewer ├── __about__.py ├── __init__.py ├── apps.py ├── schema.py ├── static │ └── schema-viewer │ │ ├── css │ │ └── main.5636232d.css │ │ └── js │ │ └── main.58eb5a4ef.js ├── templates │ └── schema_viewer │ │ └── index.html ├── tests │ ├── __init__.py │ ├── test_schema.py │ └── test_view.py ├── urls.py └── views.py └── tests ├── __init__.py └── conf ├── __init__.py ├── asgi.py ├── settings ├── __init__.py └── demo.py ├── urls.py └── wsgi.py /.github/workflows/autoversion.yaml: -------------------------------------------------------------------------------- 1 | name: autoversion 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version-segment: 7 | description: Version segment 8 | required: true 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | 14 | jobs: 15 | release: 16 | env: 17 | VERSION_SEGMENT: ${{ github.event.inputs.version-segment }} 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | token: ${{ secrets.GH_TOKEN }} 24 | - name: Set up Python 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: '3.11' 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install hatch 32 | - name: Push new version on github 33 | run: | 34 | CHANGELOG=$(hatch version $VERSION_SEGMENT) 35 | echo "CHANGELOG=$CHANGELOG" >> $GITHUB_ENV 36 | TAG=$(hatch version) 37 | echo "TAG=$TAG" >> $GITHUB_ENV 38 | git config --global user.email "auto@version" 39 | git config --global user.name "autoversion" 40 | git commit -am "Release: $TAG 41 | 42 | $CHANGELOG" 43 | git tag "$TAG" 44 | git push && git push --tags 45 | - name: Publish release on github 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 48 | run: | 49 | gh release create ${{ env.TAG }} --title "${{ env.TAG }}" --notes "${{ env.CHANGELOG }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build-and-publish-on-pypi: 9 | if: github.event_name == 'release' && github.event.action == 'created' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.11' 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install hatch 21 | - name: Build package 22 | run: hatch build 23 | - name: Publish package 24 | run : | 25 | hatch publish -u __token__ -a ${{ secrets.PYPI_API_TOKEN }} 26 | 27 | build-and-publish-on-ghcr: 28 | if: github.event_name == 'release' && github.event.action == 'created' 29 | runs-on: ubuntu-latest 30 | env: 31 | DJANGO_SETTINGS_MODULE: tests.conf.settings.demo 32 | GHCR_IMAGE_NAME: "ghcr.io/pikhovkin/${GITHUB_REPOSITORY:10}" 33 | TAG: ${GITHUB_REF:10} 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Docker login 37 | run: echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 38 | - name: Build the Docker image 39 | run: | 40 | docker build -f docker/Dockerfile -t ${{ env.GHCR_IMAGE_NAME }}-demo:${{ env.TAG }} . 41 | - name: Docker push 42 | run: | 43 | docker push ${{ env.GHCR_IMAGE_NAME }}-demo:${{ env.TAG }} 44 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | if: >- 12 | github.event.head_commit.author.email != 'auto@version' && 13 | github.event.head_commit.committer.email != 'auto@version' 14 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | DJANGO_SECRET_KEY: $RANDOM 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest] #, windows-latest, macos-latest] 22 | python-version: [ '3.10', '3.11', '3.12', '3.13' ] 23 | django: [ '4.0', '4.1', '4.2', '5.0', '5.1' ] 24 | exclude: 25 | - python-version: '3.13' 26 | django: '4.0' 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set up Python ${{ matrix.python-version }}, Django ${{ matrix.django }} 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | allow-prereleases: true 36 | 37 | - name: Install Hatch 38 | run: | 39 | pip install --upgrade pip 40 | pip install hatch 41 | 42 | - name: Run Linter 43 | run: hatch run lint:all 44 | 45 | - name: Run tests 46 | run: hatch run +py=${{ matrix.python-version }} +django=${{ matrix.django }} all:cov 47 | 48 | - name: Combine 49 | run: | 50 | export COVERED_DISPLAY=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") 51 | echo "COVERED_DISPLAY=$COVERED_DISPLAY" >> $GITHUB_ENV 52 | 53 | - name: Create the Badge 54 | uses: schneegans/dynamic-badges-action@v1.7.0 55 | with: 56 | auth: ${{ secrets.COVERAGE_TOKEN }} 57 | gistID: dc6f561d32b4e4e6d6f05bfd59c4ffaf 58 | filename: covbadge.json 59 | label: coverage 60 | message: ${{ env.COVERED_DISPLAY }}% 61 | valColorRange: ${{ env.COVERED_DISPLAY }} 62 | maxColorRange: 90 63 | minColorRange: 50 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | .ruff_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # IDE 108 | .idea/ 109 | 110 | # My local notes 111 | .notes 112 | 113 | /frontend 114 | /data 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Sergei Pikhovkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-schema-viewer 2 | 3 | [![GitHub Actions](https://github.com/pikhovkin/django-schema-viewer/actions/workflows/tests.yaml/badge.svg)](https://github.com/pikhovkin/django-schema-viewer/actions) 4 | ![badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/pikhovkin/dc6f561d32b4e4e6d6f05bfd59c4ffaf/raw/covbadge.json) 5 | [![PyPI - Version](https://img.shields.io/pypi/v/django-schema-viewer.svg)](https://pypi.org/project/django-schema-viewer) 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-schema-viewer.svg)](https://pypi.org/project/django-schema-viewer) 7 | [![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-schema-viewer.svg)](https://pypi.org/project/django-schema-viewer) 8 | [![PyPI - License](https://img.shields.io/pypi/l/django-schema-viewer.svg)](./LICENSE) 9 | 10 | [![framework - Django](https://img.shields.io/badge/framework-Django-0C3C26.svg)](https://www.djangoproject.com/) 11 | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) 12 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 13 | 14 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/pikhovkin) 15 | [![Support me](https://img.shields.io/badge/Support%20me-F16061?logo=ko-fi&logoColor=white&labelColor=F16061)](https://ko-fi.com/pikhovkin) 16 | [![Patreon](https://img.shields.io/badge/Patreon-F96854?logo=patreon)](https://patreon.com/pikhovkin) 17 | [![Liberapay](https://img.shields.io/badge/Liberapay-F6C915?logo=liberapay&logoColor=black)](https://liberapay.com/pikhovkin) 18 | 19 | Visualizes a DB schema based on Django models. 20 | 21 | [![django-schema-viewer demo](docs/screenview.png "Click to see demo")](https://django-schema-viewer.demox.dev) 22 | 23 | ### Installation 24 | 25 | ```console 26 | pip install django-schema-viewer 27 | ``` 28 | 29 | ### Usage 30 | 31 | 1. Install the package 32 | 33 | 2. Add `schema_viewer` to your `INSTALLED_APPS` settings like this: 34 | 35 | ```python 36 | INSTALLED_APPS = [ 37 | ..., 38 | 'schema_viewer', 39 | ..., 40 | ] 41 | ``` 42 | 43 | 3. Add `schema_viewer.urls` to main `urls.py`: 44 | 45 | ```python 46 | from django.urls import path, include 47 | 48 | urlpatterns = [ 49 | ..., 50 | path('schema-viewer/', include('schema_viewer.urls')), 51 | ..., 52 | ] 53 | ``` 54 | 55 | 4. Run the project 56 | 57 | ```console 58 | python manange.py runserver 59 | ``` 60 | 61 | 5. Go to http://127.0.0.1:8000/schema-viewer/ 62 | 63 | ### Optional settings 64 | 65 | ```python 66 | SCHEMA_VIEWER = { 67 | 'apps': [ 68 | 'contenttypes', 69 | 'my_app', 70 | ], 71 | 'exclude': { 72 | 'auth': ['User'], 73 | 'my_app': ['SomeModel'], 74 | }, 75 | } 76 | ``` 77 | 78 | ## License 79 | 80 | MIT 81 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx/unit:1.29.1-python3.11 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | 6 | WORKDIR /app 7 | 8 | COPY requirements ./requirements 9 | 10 | RUN apt update \ 11 | && cp /usr/bin/python3 /usr/bin/python \ 12 | && python -m pip install --upgrade pip \ 13 | && pip3 install -r requirements/base.in \ 14 | && apt autoremove --purge -y \ 15 | && rm -rf \ 16 | /var/lib/apt/lists/* \ 17 | /etc/apt/sources.list.d/*.list \ 18 | /root/.cache/ 19 | 20 | COPY schema_viewer ./schema_viewer 21 | COPY tests ./tests 22 | COPY manage.py ./ 23 | 24 | COPY docker/unit_config.json /docker-entrypoint.d/unit_config.json 25 | -------------------------------------------------------------------------------- /docker/unit_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "listeners": { 3 | "0.0.0.0:80": { 4 | "pass": "routes/app" 5 | } 6 | }, 7 | "routes": { 8 | "app": [ 9 | { 10 | "match": { 11 | "uri": "/static/*" 12 | }, 13 | "action": { 14 | "share": "/app/data$uri" 15 | } 16 | }, 17 | { 18 | "match": { 19 | "uri": "/media/*" 20 | }, 21 | "action": { 22 | "share": "/app/data$uri" 23 | } 24 | }, 25 | { 26 | "action": { 27 | "pass": "applications/app" 28 | } 29 | } 30 | ] 31 | }, 32 | "applications": { 33 | "app": { 34 | "type": "python 3.11", 35 | "path": "/app/", 36 | "module": "tests.conf.wsgi", 37 | "processes": 1, 38 | "threads": 4 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/screenview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikhovkin/django-schema-viewer/7d2a5588c75bf735682ee1af8c21a52516cbe4cf/docs/screenview.png -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.conf.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "django-schema-viewer" 7 | dynamic = ["version"] 8 | description = "Visualizes a DB schema based on Django models" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | license = "MIT" 12 | keywords = [ 13 | 'django', 14 | 'database-gui', 15 | 'django-models', 16 | 'django-schema', 17 | 'django-schema-graph', 18 | 'database-schema', 19 | 'entity-relationship-diagram', 20 | 'er-diagram', 21 | 'erd', 22 | 'json-table-schema', 23 | 'schema', 24 | 'schema-diagram', 25 | 'schema-graph', 26 | 'schema-viewer', 27 | 'viewer', 28 | ] 29 | authors = [ 30 | { name = "Sergei Pikhovkin", email = "s@pikhovkin.ru" }, 31 | ] 32 | classifiers = [ 33 | "Development Status :: 4 - Beta", 34 | 'Environment :: Web Environment', 35 | 'Framework :: Django :: 4.0', 36 | 'Framework :: Django :: 4.1', 37 | 'Framework :: Django :: 4.2', 38 | 'Framework :: Django :: 5.0', 39 | 'Framework :: Django :: 5.1', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | "Programming Language :: Python", 43 | 'Programming Language :: Python :: 3', 44 | 'Programming Language :: Python :: 3 :: Only', 45 | "Programming Language :: Python :: 3.10", 46 | "Programming Language :: Python :: 3.11", 47 | "Programming Language :: Python :: 3.12", 48 | "Programming Language :: Python :: 3.13", 49 | "Topic :: System :: Monitoring", 50 | "Topic :: Utilities", 51 | ] 52 | dependencies = [ 53 | 'Django>=4.0', 54 | ] 55 | 56 | [project.urls] 57 | Documentation = "https://github.com/pikhovkin/django-schema-viewer#readme" 58 | Issues = "https://github.com/pikhovkin/django-schema-viewer/issues" 59 | Source = "https://github.com/pikhovkin/django-schema-viewer" 60 | 61 | [tool.hatch.version] 62 | path = "schema_viewer/__about__.py" 63 | 64 | [tool.hatch.build] 65 | include = [ 66 | "schema_viewer/", 67 | "README.md", 68 | "LICENSE", 69 | "pyproject.toml", 70 | ] 71 | 72 | [tool.hatch.envs.default] 73 | dependencies = [ 74 | "coverage[toml]>=6.5", 75 | "pytest", 76 | "pytest-django", 77 | ] 78 | [tool.hatch.envs.default.scripts] 79 | test = "pytest {args:schema_viewer/tests}" 80 | test-cov = "coverage run -m pytest {args:schema_viewer/tests}" 81 | cov-report = [ 82 | "- coverage combine", 83 | "coverage report", 84 | "coverage json", 85 | ] 86 | cov = [ 87 | "test-cov", 88 | "cov-report", 89 | ] 90 | 91 | [tool.hatch.envs.all.overrides] 92 | matrix.django.dependencies = [ 93 | {value='Django~=4.0.0', if=['4.0']}, 94 | {value='Django~=4.1.0', if=['4.1']}, 95 | {value='Django~=4.2.0', if=['4.2']}, 96 | {value='Django~=5.0.0', if=['5.0']}, 97 | {value='Django~=5.1.0', if=['5.1']}, 98 | ] 99 | 100 | [[tool.hatch.envs.all.matrix]] 101 | python = ["3.10", "3.11", "3.12", "3.13"] 102 | django = ["4.0", "4.1", "4.2", "5.0", "5.1"] 103 | 104 | [tool.hatch.envs.typing] 105 | dependecies = [ 106 | "django-stubs[compatible-mypy]", 107 | ] 108 | 109 | [tool.hatch.envs.lint] 110 | detached = true 111 | dependencies = [ 112 | "black>=23.9.0", 113 | "mypy>=1.13", 114 | "ruff>=0.8.0", 115 | "isort>=5.13.2", 116 | "django-stubs[compatible-mypy]>=5.1.1", 117 | ] 118 | [tool.hatch.envs.lint.scripts] 119 | typing = "mypy --install-types --non-interactive {args:schema_viewer}" 120 | style = [ 121 | "isort {args:schema_viewer}", 122 | "ruff check {args:schema_viewer}", 123 | "black --check --diff {args:schema_viewer}", 124 | ] 125 | fmt = [ 126 | "black {args:schema_viewer}", 127 | "ruff format {args:schema_viewer}", 128 | "style", 129 | ] 130 | all = [ 131 | "style", 132 | "typing", 133 | ] 134 | 135 | [tool.pytest.ini_options] 136 | DJANGO_SETTINGS_MODULE = "tests.conf.settings" 137 | 138 | [tool.mypy] 139 | plugins = ["mypy_django_plugin.main"] 140 | 141 | [tool.django-stubs] 142 | django_settings_module = "tests.conf.settings" 143 | 144 | [tool.black] 145 | target-version = ["py311"] 146 | line-length = 120 147 | skip-string-normalization = true 148 | 149 | [tool.ruff] 150 | src = [ 151 | 'schema_viewer', 152 | ] 153 | target-version = "py311" 154 | line-length = 120 155 | lint.select = [ 156 | "A", 157 | "ARG", 158 | "B", 159 | "C", 160 | "DTZ", 161 | "E", 162 | "EM", 163 | "F", 164 | "FBT", 165 | "I", 166 | "ICN", 167 | "ISC", 168 | "N", 169 | "PLC", 170 | "PLE", 171 | "PLR", 172 | "PLW", 173 | "Q", 174 | "RUF", 175 | "S", 176 | "T", 177 | "TID", 178 | "UP", 179 | "W", 180 | "YTT", 181 | ] 182 | lint.ignore = [ 183 | 'Q000', # Single quotes found but double quotes preferred 184 | # 'C408', 185 | # 'T201', 186 | # Allow non-abstract empty methods in abstract base classes 187 | "B027", 188 | # Allow boolean positional values in function calls, like `dict.get(... True)` 189 | "FBT003", 190 | # Ignore checks for possible passwords 191 | "S105", "S106", "S107", 192 | # Ignore complexity 193 | "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", 194 | ] 195 | lint.unfixable = [ 196 | # Don't touch unused imports 197 | "F401", 198 | ] 199 | 200 | [tool.ruff.lint.isort] 201 | known-first-party = ["schema_viewer"] 202 | 203 | [tool.ruff.lint.flake8-tidy-imports] 204 | ban-relative-imports = "all" 205 | 206 | [tool.ruff.lint.per-file-ignores] 207 | # Tests can use magic values, assertions, and relative imports 208 | "tests/**/*" = ["PLR2004", "S101", "TID252"] 209 | 210 | [tool.coverage.run] 211 | source_pkgs = ["schema_viewer"] 212 | branch = true 213 | parallel = true 214 | omit = [ 215 | "schema_viewer/__about__.py", 216 | ] 217 | 218 | [tool.coverage.report] 219 | exclude_lines = [ 220 | "no cov", 221 | "if __name__ == .__main__.:", 222 | "if TYPE_CHECKING:", 223 | ] 224 | -------------------------------------------------------------------------------- /requirements/base.in: -------------------------------------------------------------------------------- 1 | Django>=4.0, <4.3 2 | -------------------------------------------------------------------------------- /schema_viewer/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.5.3' 2 | -------------------------------------------------------------------------------- /schema_viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikhovkin/django-schema-viewer/7d2a5588c75bf735682ee1af8c21a52516cbe4cf/schema_viewer/__init__.py -------------------------------------------------------------------------------- /schema_viewer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SchemaViewerConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'schema_viewer' 7 | -------------------------------------------------------------------------------- /schema_viewer/schema.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from typing import Any, cast 3 | 4 | from django import apps 5 | from django.conf import settings 6 | from django.contrib.contenttypes.fields import GenericForeignKey 7 | from django.db import connection, models 8 | 9 | 10 | def get_app_models() -> Iterator[tuple[apps.AppConfig, type[models.Model]]]: 11 | for app in apps.apps.get_app_configs(): 12 | for model in app.get_models(): 13 | yield app, model 14 | 15 | 16 | def get_app_name(model: type[models.Model]) -> str: 17 | app_label = model._meta.app_label 18 | try: 19 | return apps.apps.get_app_config(app_label).name 20 | except LookupError: 21 | return model.__module__ 22 | 23 | 24 | def get_model_id(model: type[models.Model]) -> str: 25 | return f"{model.__module__}.{model.__name__}" 26 | 27 | 28 | def is_model_subclass(obj: type[models.Model]) -> bool: 29 | if obj is models.Model: 30 | return False 31 | return issubclass(obj, models.Model) 32 | 33 | 34 | def _make_resource(app: apps.AppConfig, model: type[models.Model], app_names: list, excludes: dict) -> list[dict]: 35 | if app_names and app.name not in app_names: 36 | return [] 37 | elif model._meta.proxy or model._meta.abstract: 38 | return [] 39 | elif app.name in excludes and model.__name__.lower() in excludes[app.name]: 40 | return [] 41 | 42 | resources: list[dict] = [ 43 | { 44 | 'name': model._meta.db_table, 45 | 'title': f'{app.name}.{model.__name__}', 46 | 'description': model._meta.verbose_name, 47 | 'schema': { 48 | 'fields': [], 49 | 'primaryKey': [], 50 | 'foreignKeys': [], 51 | }, 52 | } 53 | ] 54 | schema_fields: list = resources[-1]['schema']['fields'] 55 | schema_primary_key: list = resources[-1]['schema']['primaryKey'] 56 | schema_foreign_keys: list = resources[-1]['schema']['foreignKeys'] 57 | 58 | for field in model._meta.get_fields(): 59 | if isinstance(field, GenericForeignKey): 60 | continue 61 | elif not field.concrete: 62 | continue 63 | elif field.many_to_many: 64 | if not field.auto_created and isinstance(field.remote_field, models.ManyToManyRel): 65 | m2m_model = field.remote_field.through 66 | if m2m_model is not None and m2m_model._meta.auto_created: 67 | resources.extend(_make_resource(m2m_model._meta.app_config, m2m_model, app_names, excludes)) 68 | continue 69 | elif field.model is not model: 70 | continue 71 | 72 | field_name: str = field.attname 73 | db_type: str | None = field.db_type(connection) 74 | if db_type is None: 75 | continue 76 | 77 | schema_fields.append( 78 | { 79 | 'name': field_name, 80 | 'title': getattr(field, 'verbose_name', ''), 81 | 'description': getattr(field, 'verbose_name', ''), 82 | 'type': db_type, 83 | 'constraints': { 84 | 'required': not field.null, 85 | 'unique': field.unique, 86 | }, 87 | } 88 | ) 89 | if field.is_relation: 90 | rel_model = cast(type[models.Model], field.related_model) 91 | if rel_model is None: 92 | continue 93 | rel_model_app_name = get_app_name(rel_model) 94 | if app_names and rel_model_app_name not in app_names: 95 | continue 96 | if rel_model_app_name in excludes and rel_model.__name__.lower() in excludes[rel_model_app_name]: 97 | continue 98 | 99 | rel_field = rel_model._meta.pk 100 | if rel_field is None: 101 | continue 102 | 103 | schema_foreign_keys.append( 104 | { 105 | 'fields': field_name, 106 | 'reference': { 107 | 'resource': rel_model._meta.db_table, 108 | 'fields': [ 109 | rel_field.attname, 110 | ], 111 | }, 112 | } 113 | ) 114 | elif field.primary_key: 115 | schema_primary_key.append(field_name) 116 | 117 | return resources 118 | 119 | 120 | def get_schema(conf: dict | None = None) -> dict: 121 | if not conf: 122 | conf = getattr(settings, 'SCHEMA_VIEWER', {}) or {} 123 | 124 | app_names = conf.get('apps', []) or [] 125 | excludes = conf.get('exclude', {}) or {} 126 | 127 | json_table_schema: dict[str, Any] = { 128 | 'resources': [], 129 | 'name': '', 130 | } 131 | 132 | resources = json_table_schema['resources'] 133 | for app, model in get_app_models(): 134 | resources.extend(_make_resource(app, model, app_names, excludes)) 135 | 136 | return json_table_schema 137 | -------------------------------------------------------------------------------- /schema_viewer/static/schema-viewer/css/main.5636232d.css: -------------------------------------------------------------------------------- 1 | body,html{font-family:Arial,Helvetica Neue,Helvetica,sans-serif;font-size:10px;margin:0;padding:0} -------------------------------------------------------------------------------- /schema_viewer/templates/schema_viewer/index.html: -------------------------------------------------------------------------------- 1 | {% load static %}django-schema-viewer
2 | -------------------------------------------------------------------------------- /schema_viewer/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikhovkin/django-schema-viewer/7d2a5588c75bf735682ee1af8c21a52516cbe4cf/schema_viewer/tests/__init__.py -------------------------------------------------------------------------------- /schema_viewer/tests/test_schema.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from schema_viewer.schema import get_schema 4 | 5 | 6 | class SchemaTest(TestCase): 7 | def test_schema(self): 8 | tables = { 9 | 'auth_permission', 10 | 'django_admin_log', 11 | 'auth_group', 12 | 'auth_group_permissions', 13 | 'auth_user', 14 | 'auth_user_user_permissions', 15 | 'auth_user_groups', 16 | 'django_content_type', 17 | 'django_session', 18 | } 19 | schema = get_schema() 20 | self.assertTrue({t['name'] for t in schema['resources']} == tables) 21 | -------------------------------------------------------------------------------- /schema_viewer/tests/test_view.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import TestCase 4 | 5 | 6 | class ViewTest(TestCase): 7 | def test_view(self): 8 | response = self.client.get('/schema-viewer/') 9 | self.assertContains(response, 'django-schema-viewer') 10 | 11 | def test_schema(self): 12 | tables = { 13 | 'auth_permission', 14 | 'django_admin_log', 15 | 'auth_group', 16 | 'auth_group_permissions', 17 | 'auth_user', 18 | 'auth_user_user_permissions', 19 | 'auth_user_groups', 20 | 'django_content_type', 21 | 'django_session', 22 | } 23 | 24 | response = self.client.get('/schema-viewer/schema/') 25 | schema = json.loads(response.content) 26 | self.assertTrue({t['name'] for t in schema['resources']}, tables) 27 | -------------------------------------------------------------------------------- /schema_viewer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from schema_viewer.views import IndexView, SchemaView 4 | 5 | urlpatterns = [ 6 | path('', IndexView.as_view()), 7 | path('schema/', SchemaView.as_view()), 8 | ] 9 | -------------------------------------------------------------------------------- /schema_viewer/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.views.generic import TemplateView, View 3 | 4 | from schema_viewer.schema import get_schema 5 | 6 | 7 | class IndexView(TemplateView): 8 | template_name = 'schema_viewer/index.html' 9 | 10 | 11 | class SchemaView(View): 12 | def get(self, request): # noqa: ARG002 13 | return JsonResponse(get_schema()) 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikhovkin/django-schema-viewer/7d2a5588c75bf735682ee1af8c21a52516cbe4cf/tests/__init__.py -------------------------------------------------------------------------------- /tests/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikhovkin/django-schema-viewer/7d2a5588c75bf735682ee1af8c21a52516cbe4cf/tests/conf/__init__.py -------------------------------------------------------------------------------- /tests/conf/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.asgi import get_asgi_application 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.conf.settings') 6 | 7 | application = get_asgi_application() 8 | -------------------------------------------------------------------------------- /tests/conf/settings/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | 5 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 6 | BASE_DIR = Path(__file__).resolve().parent.parent 7 | 8 | # Quick-start development settings - unsuitable for production 9 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 10 | 11 | # SECURITY WARNING: keep the secret key used in production secret! 12 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') 13 | 14 | # SECURITY WARNING: don't run with debug turned on in production! 15 | DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True' 16 | 17 | ALLOWED_HOSTS: list = ['*'] 18 | 19 | 20 | # Application definition 21 | 22 | INSTALLED_APPS = [ 23 | 'django.contrib.admin', 24 | 'django.contrib.auth', 25 | 'django.contrib.contenttypes', 26 | 'django.contrib.sessions', 27 | 'django.contrib.messages', 28 | 'django.contrib.staticfiles', 29 | 30 | 'schema_viewer', 31 | ] 32 | 33 | MIDDLEWARE = [ 34 | 'django.middleware.security.SecurityMiddleware', 35 | 'django.contrib.sessions.middleware.SessionMiddleware', 36 | 'django.middleware.common.CommonMiddleware', 37 | 'django.middleware.csrf.CsrfViewMiddleware', 38 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 39 | 'django.contrib.messages.middleware.MessageMiddleware', 40 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 41 | ] 42 | 43 | ROOT_URLCONF = 'tests.conf.urls' 44 | 45 | TEMPLATES = [ 46 | { 47 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 48 | 'DIRS': [], 49 | 'APP_DIRS': True, 50 | 'OPTIONS': { 51 | 'context_processors': [ 52 | 'django.template.context_processors.debug', 53 | 'django.template.context_processors.request', 54 | 'django.contrib.auth.context_processors.auth', 55 | 'django.contrib.messages.context_processors.messages', 56 | ], 57 | }, 58 | }, 59 | ] 60 | 61 | WSGI_APPLICATION = 'tests.conf.wsgi.application' 62 | 63 | 64 | # Database 65 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 66 | 67 | DATABASES = { 68 | 'default': { 69 | 'ENGINE': 'django.db.backends.sqlite3', 70 | 'NAME': ':memory:', 71 | }, 72 | } 73 | 74 | 75 | # Password validation 76 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 77 | 78 | AUTH_PASSWORD_VALIDATORS = [ 79 | { 80 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 81 | }, 82 | { 83 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 84 | }, 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 87 | }, 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 90 | }, 91 | ] 92 | 93 | 94 | # Internationalization 95 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 96 | 97 | LANGUAGE_CODE = 'en-us' 98 | 99 | TIME_ZONE = 'UTC' 100 | 101 | USE_I18N = True 102 | 103 | USE_TZ = True 104 | 105 | 106 | # Static files (CSS, JavaScript, Images) 107 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 108 | 109 | STATIC_ROOT = BASE_DIR / 'static' 110 | STATIC_URL = '/static/' 111 | 112 | # Default primary key field type 113 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 114 | 115 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 116 | -------------------------------------------------------------------------------- /tests/conf/settings/demo.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | 3 | 4 | # Database 5 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 6 | 7 | DB_DIR = BASE_DIR.parent.parent / 'data' / 'db' 8 | DB_DIR.mkdir(parents=True, exist_ok=True) 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.sqlite3', 12 | 'NAME': DB_DIR / 'db.sqlite3', 13 | }, 14 | } 15 | 16 | # Static files (CSS, JavaScript, Images) 17 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 18 | 19 | STATIC_ROOT = BASE_DIR.parent.parent / 'data' / 'static' 20 | STATIC_URL = '/static/' 21 | -------------------------------------------------------------------------------- /tests/conf/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [ 4 | path('schema-viewer/', include('schema_viewer.urls')), 5 | ] 6 | -------------------------------------------------------------------------------- /tests/conf/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.conf.settings') 6 | 7 | application = get_wsgi_application() 8 | --------------------------------------------------------------------------------