flask-admin-autolinks 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ poetry.lock
107
+ uv.lock
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ # End of https://www.toptal.com/developers/gitignore/api/python
@@ -0,0 +1,56 @@
1
+ # .pre-commit-config.yaml
2
+ repos:
3
+ - repo: https://github.com/pre-commit/pre-commit-hooks
4
+ rev: v6.0.0
5
+ hooks:
6
+ - id: trailing-whitespace
7
+ - id: end-of-file-fixer
8
+ - id: check-added-large-files
9
+ args: [--maxkb=1000]
10
+ - id: check-merge-conflict
11
+ - id: check-toml
12
+ - id: check-yaml
13
+ args: [--unsafe] # needed if you have custom yaml tags anywhere
14
+ - id: debug-statements
15
+ - id: mixed-line-ending
16
+ args: [--fix=lf]
17
+ - id: check-case-conflict
18
+ - id: check-json
19
+ - id: check-executables-have-shebangs
20
+ - id: detect-private-key # catches accidentally committed keys — cheap insurance
21
+ - id: mixed-line-ending
22
+
23
+
24
+ - repo: https://github.com/asottile/pyupgrade
25
+ rev: v3.22.0
26
+ hooks:
27
+ - id: pyupgrade
28
+ args: [--py314-plus] # match your actual target version
29
+
30
+ - repo: https://github.com/pycqa/isort
31
+ rev: 10.0.0b1
32
+ hooks:
33
+ - id: isort
34
+
35
+ - repo: https://github.com/psf/black
36
+ rev: 26.10.1
37
+ hooks:
38
+ - id: black
39
+
40
+ - repo: https://github.com/PyCQA/flake8
41
+ rev: 7.4.1
42
+ hooks:
43
+ - id: flake8
44
+ additional_dependencies: ["flake8-pyproject"]
45
+
46
+ - repo: https://github.com/PyCQA/bandit
47
+ rev: 1.9.4
48
+ hooks:
49
+ - id: bandit
50
+ args: [-c, pyproject.toml]
51
+ additional_dependencies: ["bandit[toml]"]
52
+
53
+ - repo: https://github.com/rtts/djhtml
54
+ rev: 3.0.11
55
+ hooks:
56
+ - id: djhtml # formats Jinja2/Django templates — you're wrestling Jinja2 in fonotarot, this cleans that up automatically
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mario Hernandez
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.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.5
2
+ Name: flask-admin-autolinks
3
+ Version: 0.1.0
4
+ Summary: Implements autolinks for related models in ModelView from Flask-Admin
5
+ Author-email: Mario Hernandez <mariofix@proton.me>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: flask-admin>=2.2.1
9
+ Description-Content-Type: text/markdown
10
+
11
+ # flask-admin-autolinks
12
+
13
+ Automatic hyperlinks for relationship columns in Flask-Admin's SQLAlchemy list views.
14
+
15
+ By default, a relationship column renders as plain text (`str()` of the related object). With this mixin, each related object becomes a link to its own edit (or details) page in the admin.
16
+
17
+ ## Features
18
+
19
+ - Opt-in per view. No effect unless `column_relationship_links` is set.
20
+ - Resolves the target view from the registered views by model. No endpoint naming convention required.
21
+ - Respects the target view's `can_edit`, `can_view_details` and `is_accessible()`. Falls back to plain text.
22
+ - Eager-loads link targets through the view's `_auto_joins`, so the list page does not issue N+1 queries.
23
+ - Explicit `column_formatters` entries always win.
24
+ - Output is escaped.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ uv add flask-admin-autolinks
30
+ # or
31
+ pip install flask-admin-autolinks
32
+ ```
33
+
34
+ Requires Flask-Admin with SQLAlchemy support (`flask-admin[sqlalchemy]`). Supported versions are declared in `pyproject.toml`.
35
+
36
+ ## Usage
37
+
38
+ Place `AutoLinksMixin` before `ModelView` in the bases:
39
+
40
+ ```python
41
+ from flask_admin.contrib.sqla import ModelView
42
+ from flask_admin_autolinks import AutoLinksMixin
43
+
44
+
45
+ class OrderView(AutoLinksMixin, ModelView):
46
+ column_list = ("id", "customer", "items")
47
+ column_relationship_links = True
48
+ ```
49
+
50
+ For project-wide use, define a shared base once:
51
+
52
+ ```python
53
+ class BaseView(AutoLinksMixin, ModelView):
54
+ column_relationship_links = True
55
+ ```
56
+
57
+ Only relationship columns listed in `column_list` are linked.
58
+
59
+ ## Configuration
60
+
61
+ `column_relationship_links` accepts:
62
+
63
+ | Value | Behavior |
64
+ | --- | --- |
65
+ | `False` (default) | Disabled. |
66
+ | `True` | Link every relationship column in `column_list` to the registered view for the related model. |
67
+ | `dict[str, str]` | Same as `True`, with per-column overrides mapping a relationship name to a view endpoint. |
68
+
69
+ ```python
70
+ class OrderView(AutoLinksMixin, ModelView):
71
+ column_list = ("id", "customer", "items")
72
+ column_relationship_links = {"customer": "client"} # "client" is the target view's endpoint
73
+ ```
74
+
75
+ An empty dict is falsy and leaves the feature disabled.
76
+
77
+ ## Behavior
78
+
79
+ For each related object, the link target is chosen in this order:
80
+
81
+ 1. Target view has `can_edit` and is accessible: link to `edit_view`.
82
+ 2. Otherwise, target view has `can_view_details` and is accessible: link to `details_view`.
83
+ 3. Otherwise: escaped plain text.
84
+
85
+ If no view is registered for the related model (or the override endpoint is not found), the cell renders as plain text. The list page is never broken by a missing target.
86
+
87
+ Collections render as comma-separated links. `None` renders as an empty cell.
88
+
89
+ A column that already has an entry in `column_formatters` is left untouched and is not eager-loaded by this package.
90
+
91
+ ## To Do
92
+
93
+ - SQLAlchemy backend only (`flask_admin.contrib.sqla`), add others.
94
+
95
+ ## License
96
+
97
+ See `LICENSE` for MIT License.
98
+
99
+ ## IA
100
+
101
+ Documentation done with Claude.
@@ -0,0 +1,91 @@
1
+ # flask-admin-autolinks
2
+
3
+ Automatic hyperlinks for relationship columns in Flask-Admin's SQLAlchemy list views.
4
+
5
+ By default, a relationship column renders as plain text (`str()` of the related object). With this mixin, each related object becomes a link to its own edit (or details) page in the admin.
6
+
7
+ ## Features
8
+
9
+ - Opt-in per view. No effect unless `column_relationship_links` is set.
10
+ - Resolves the target view from the registered views by model. No endpoint naming convention required.
11
+ - Respects the target view's `can_edit`, `can_view_details` and `is_accessible()`. Falls back to plain text.
12
+ - Eager-loads link targets through the view's `_auto_joins`, so the list page does not issue N+1 queries.
13
+ - Explicit `column_formatters` entries always win.
14
+ - Output is escaped.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ uv add flask-admin-autolinks
20
+ # or
21
+ pip install flask-admin-autolinks
22
+ ```
23
+
24
+ Requires Flask-Admin with SQLAlchemy support (`flask-admin[sqlalchemy]`). Supported versions are declared in `pyproject.toml`.
25
+
26
+ ## Usage
27
+
28
+ Place `AutoLinksMixin` before `ModelView` in the bases:
29
+
30
+ ```python
31
+ from flask_admin.contrib.sqla import ModelView
32
+ from flask_admin_autolinks import AutoLinksMixin
33
+
34
+
35
+ class OrderView(AutoLinksMixin, ModelView):
36
+ column_list = ("id", "customer", "items")
37
+ column_relationship_links = True
38
+ ```
39
+
40
+ For project-wide use, define a shared base once:
41
+
42
+ ```python
43
+ class BaseView(AutoLinksMixin, ModelView):
44
+ column_relationship_links = True
45
+ ```
46
+
47
+ Only relationship columns listed in `column_list` are linked.
48
+
49
+ ## Configuration
50
+
51
+ `column_relationship_links` accepts:
52
+
53
+ | Value | Behavior |
54
+ | --- | --- |
55
+ | `False` (default) | Disabled. |
56
+ | `True` | Link every relationship column in `column_list` to the registered view for the related model. |
57
+ | `dict[str, str]` | Same as `True`, with per-column overrides mapping a relationship name to a view endpoint. |
58
+
59
+ ```python
60
+ class OrderView(AutoLinksMixin, ModelView):
61
+ column_list = ("id", "customer", "items")
62
+ column_relationship_links = {"customer": "client"} # "client" is the target view's endpoint
63
+ ```
64
+
65
+ An empty dict is falsy and leaves the feature disabled.
66
+
67
+ ## Behavior
68
+
69
+ For each related object, the link target is chosen in this order:
70
+
71
+ 1. Target view has `can_edit` and is accessible: link to `edit_view`.
72
+ 2. Otherwise, target view has `can_view_details` and is accessible: link to `details_view`.
73
+ 3. Otherwise: escaped plain text.
74
+
75
+ If no view is registered for the related model (or the override endpoint is not found), the cell renders as plain text. The list page is never broken by a missing target.
76
+
77
+ Collections render as comma-separated links. `None` renders as an empty cell.
78
+
79
+ A column that already has an entry in `column_formatters` is left untouched and is not eager-loaded by this package.
80
+
81
+ ## To Do
82
+
83
+ - SQLAlchemy backend only (`flask_admin.contrib.sqla`), add others.
84
+
85
+ ## License
86
+
87
+ See `LICENSE` for MIT License.
88
+
89
+ ## IA
90
+
91
+ Documentation done with Claude.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "flask-admin-autolinks"
3
+ version = "0.1.0"
4
+ description = "Implements autolinks for related models in ModelView from Flask-Admin"
5
+ readme = "README.md"
6
+ authors = [{ name = "Mario Hernandez", email = "mariofix@proton.me" }]
7
+ requires-python = ">=3.11"
8
+ dependencies = ["flask-admin>=2.2.1"]
9
+
10
+ [build-system]
11
+ requires = ["hatchling"]
12
+ build-backend = "hatchling.build"
13
+
14
+ [tool.hatch.metadata]
15
+ allow-direct-references = true
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["src/flask_admin_autolinks"]
19
+
20
+
21
+ [dependency-groups]
22
+ dev = ["pre-commit>=4.7.0"]
23
+
24
+
25
+ [tool.black]
26
+ line-length = 119
27
+ target-version = ["py313"]
28
+
29
+ [tool.isort]
30
+ profile = "black"
31
+ line_length = 119
32
+ known_first_party = ["src/flask_admin_autolinks"]
33
+
34
+ [tool.flake8]
35
+ max-line-length = 119
36
+ extend-ignore = ["E203", "W503"] # black-compatible
37
+ exclude = [".git", "__pycache__", "build", "dist"]
38
+
39
+ [tool.bandit]
40
+ exclude_dirs = ["tests", "alembic"]
41
+ skips = ["B101"]
@@ -0,0 +1,3 @@
1
+ from .mixin import AutoLinksMixin
2
+
3
+ __all__ = ["AutoLinksMixin"]
@@ -0,0 +1,69 @@
1
+ """Hyperlink relationship columns in Flask-Admin SQLAlchemy list views."""
2
+
3
+ import typing as t
4
+
5
+ from flask import url_for
6
+ from flask_admin.contrib.sqla.tools import is_relationship
7
+ from markupsafe import Markup, escape
8
+
9
+
10
+ class AutoLinksMixin:
11
+ """Put before ModelView in the bases. True = auto-detect; dict = per-column endpoint override."""
12
+
13
+ column_relationship_links: bool | dict[str, str] = False
14
+
15
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
16
+ super().__init__(*args, **kwargs)
17
+ self._autolinks = self._scaffold_autolinks()
18
+ if self._autolinks: # eager-load link targets: no N+1 on the list page
19
+ self._auto_joins = list({*self._auto_joins, *self._autolinks.values()})
20
+
21
+ def _scaffold_autolinks(self) -> dict[str, t.Any]:
22
+ if not self.column_relationship_links:
23
+ return {}
24
+ found = {}
25
+ for name, _label in self._list_columns:
26
+ if name in self.column_formatters: # explicit formatters always win
27
+ continue
28
+ attr = getattr(self.model, name, None)
29
+ if attr is not None and is_relationship(attr):
30
+ found[name] = attr
31
+ return found
32
+
33
+ def _target_view(self, name: str, related_cls: type) -> t.Any:
34
+ links = self.column_relationship_links
35
+ endpoint = links.get(name) if isinstance(links, dict) else None
36
+ for view in self.admin._views:
37
+ if endpoint is not None:
38
+ if view.endpoint == endpoint:
39
+ return view
40
+ elif getattr(view, "model", None) is related_cls:
41
+ return view
42
+ return None
43
+
44
+ @staticmethod
45
+ def _href(view: t.Any, target: t.Any) -> str | None:
46
+ if view is None or not view.is_accessible():
47
+ return None
48
+ pk = view.get_pk_value(target)
49
+ if view.can_edit:
50
+ return url_for(f"{view.endpoint}.edit_view", id=pk)
51
+ if view.can_view_details:
52
+ return url_for(f"{view.endpoint}.details_view", id=pk)
53
+ return None
54
+
55
+ def get_list_value(self, context: t.Any, model: t.Any, name: str) -> t.Any:
56
+ attr = self._autolinks.get(name)
57
+ if attr is None:
58
+ return super().get_list_value(context, model, name)
59
+ value = getattr(model, name)
60
+ if value is None:
61
+ return ""
62
+ prop = attr.property
63
+ view = self._target_view(name, prop.mapper.class_)
64
+ out = []
65
+ for target in value if prop.uselist else [value]:
66
+ label = escape(str(target))
67
+ href = self._href(view, target)
68
+ out.append(Markup('<a href="{}">{}</a>').format(href, label) if href else label)
69
+ return Markup(", ").join(out)