Skip to content

Commit d92d84f

Browse files
committed
Improve docs
1 parent 160b6aa commit d92d84f

File tree

6 files changed

+502
-37
lines changed

6 files changed

+502
-37
lines changed

.readthedocs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ version: 2
22

33
# Set the OS, Python version and other tools you might need
44
build:
5-
os: ubuntu-22.04
5+
os: ubuntu-24.04
66
tools:
7-
python: "3.11"
7+
python: "3.12"
88

99
sphinx:
1010
configuration: docs/conf.py

README.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,38 @@
2121

2222
**Empowering Your SaaS Tenants with Custom Options and Sane Defaults**
2323

24-
`django-tenant-options` provides a powerful and flexible way for your SaaS application’s tenants to customize the selectable options in user-facing forms. This package allows you to offer a balance between providing default options—either mandatory or optional—and giving your tenants the freedom to define their own custom choices, all within a structured framework that ensures consistency and ease of use.
25-
2624
## So your SaaS tenants want to provide end users with choices in a form...
2725

2826
*How can you implement this?*
2927

30-
- **CharField with TextChoices or IntegerChoices**: Define a fixed set of options in your model. This approach is inflexible and doesn't allow for any customization by tenants. What one tenant needs may not be what another tenant needs.
31-
- **ManyToManyField with a custom model**: Create a custom model to store options and use a ManyToManyField in your form. But what if one tenant wants to add a new option? Or if you would like to provide some default options? Or if not every tenant needs to show all of your defaults?
32-
- **JSON Fields**: Store custom options as JSON in a single field. This can be difficult to query and manage, and doesn't provide a structured way to define defaults. And it has all of the problems of the ManyToManyField approach.
33-
- **`django-tenant-options`**: A structured and flexible solution that allows tenants to define their own sets of values for form input while still allowing you, the developer, to offer global defaults (both mandatory and optional).
34-
35-
## Why Use django-tenant-options?
36-
37-
In a SaaS environment, one size doesn't fit all. Tenants often have unique needs for the choices they offer in user-facing forms, but building an entirely custom solution for each tenant - or requiring each tenant to define their own options from scratch - can be complex and time-consuming. `django-tenant-options` addresses this challenge by offering:
38-
39-
- **Flexibility**: Tenants can tailor the options available in forms to better meet their specific needs.
40-
- **Control**: Provide mandatory defaults to maintain a consistent experience across all tenants, while still allowing for customization.
41-
- **Scalability**: Easily manage multiple tenants with differing requirements without compromising on performance or maintainability.
42-
- **Simplicity**: Avoid the complexity of dynamic models or JSON fields, offering a more structured and maintainable solution.
28+
- **CharField with TextChoices or IntegerChoices**: Define a fixed set of options in your model.
29+
- ❌ One size fits all
30+
- ❌ No customization for tenants
31+
- ❌ Code changes required for new options
32+
- **ManyToManyField with a custom model**: Create a custom model to store options and use a ManyToManyField in your form.
33+
- ❌ No distinction between tenant options
34+
- ❌ Complex to manage defaults
35+
- ❌ Hard to maintain consistency
36+
- **JSON Fields**: Store custom options as JSON in a single field.
37+
- ❌ No schema validation
38+
- ❌ No referential integrity
39+
- **Custom Tables Per Tenant**
40+
- ❌ Schema complexity
41+
- ❌ Migration nightmares
42+
- ❌ Performance issues
43+
- **django-tenant-options**:
44+
- ✅ Structured and flexible
45+
- ✅ Allows tenants to define their own sets of values for form inputs
46+
- ✅ Allows you, the developer, to offer global defaults (both mandatory and optional)
47+
48+
In a SaaS environment, one size doesn't fit all. Tenants often have unique needs for the choices they offer in user-facing forms, but building an entirely custom solution for each tenant - or requiring each tenant to define their own options from scratch - can be complex and time-consuming.
4349

4450
## Key Features
4551

4652
- **Customizable Options**: Allow tenants to define their own sets of values for form input while still offering global defaults.
4753
- **Mandatory and Optional Defaults**: Define which options are mandatory for all tenants and which can be optionally used by tenants in their forms.
48-
- **Seamless Integration**: Designed to work smoothly with your existing Django models, making it easy to integrate into your project.
49-
- **Tenant-Specific Logic**: Built-in support for tenant-specific logic, ensuring that each tenant’s unique needs are met.
54+
- **Seamless Integration**: Works with your existing Django models, making it easy to integrate into your project.
55+
- **Tenant-Specific Logic**: Built-in support for tenant-specific logic, so each tenant’s unique needs can be met.
5056

5157
## Potential Use-Cases
5258

@@ -100,7 +106,6 @@ We will define a very basic `Tenant` model and a `Task` model to illustrate the
100106
from django.contrib.auth import get_user_model
101107
from django.db import models
102108

103-
104109
User = get_user_model()
105110

106111

@@ -172,7 +177,6 @@ from django.db import models
172177

173178
from example.models import TaskPriorityOption, TaskStatusOption, User
174179

175-
176180
User = get_user_model()
177181

178182

@@ -210,7 +214,9 @@ class Task(models.Model):
210214

211215
### Forms
212216

213-
`django-tenant-options` provides a set of form mixins and fields to manage the options and selections for each tenant. You can use these forms in your views to allow tenants to customize their options.
217+
`django-tenant-options` provides a set of form mixins and fields to manage the options and selections for each tenant.
218+
219+
You can use these forms in your views to allow tenants to customize their options.
214220

215221
- `OptionCreateFormMixin` and `OptionUpdateFormMixin` are provided to create and update Options.
216222
- `SelectionForm` is used to manage the Selections associated with a tenant.
@@ -275,6 +281,6 @@ python manage.py syncoptions
275281

276282
## Conclusion
277283

278-
`django-tenant-options` makes it easy to provide your SaaS application’s tenants with customizable form options, while still maintaining the consistency and control needed to ensure a smooth user experience. Whether you're managing project tasks, HR roles, marketplace filters, or any other customizable value sets, this package offers a robust solution.
284+
`django-tenant-options` makes it easy to provide your SaaS application’s tenants with customizable form options, while maintaining consistency and control.
279285

280286
Explore the [full documentation](https://django-tenant-options.readthedocs.io/en/latest/) for more details and start empowering your tenants today!

docs/conf.py

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,152 @@
1-
"""Sphinx configuration."""
1+
"""Sphinx configuration for django-tenant-options documentation."""
22

3-
import django
4-
from django.conf import settings
3+
import inspect
4+
import os
5+
import sys
6+
from datetime import datetime
57

8+
import django
9+
from django.utils.html import strip_tags
610

7-
settings.configure(DEBUG=True)
811

9-
# Initialize Django
12+
sys.path.insert(0, os.path.abspath(".."))
13+
os.environ["DJANGO_SETTINGS_MODULE"] = "example_project.settings"
1014
django.setup()
1115

16+
17+
# Project information
1218
project = "django-tenant-options"
1319
author = "Jack Linke"
14-
copyright = "2024, Jack Linke"
20+
copyright = f"{datetime.now().year}, {author}"
21+
22+
# General configuration
1523
extensions = [
1624
"sphinx.ext.autodoc",
1725
"sphinx.ext.napoleon",
26+
"sphinx.ext.intersphinx",
27+
"sphinx.ext.viewcode",
1828
"sphinx_click",
1929
"myst_parser",
2030
]
21-
autodoc_typehints = "description"
31+
32+
# Any paths that contain templates here, relative to this directory.
33+
# templates_path = ["_templates"]
34+
35+
# List of patterns, relative to source directory, that match files and
36+
# directories to ignore when looking for source files.
37+
exclude_patterns = ["_build"]
38+
39+
# The name of the Pygments (syntax highlighting) style to use.
40+
pygments_style = "sphinx"
41+
42+
# -- Options for HTML output -------------------------------------------------
43+
44+
# The theme to use for HTML and HTML Help pages.
2245
html_theme = "furo"
46+
47+
# Add any paths that contain custom static files (such as style sheets) here,
48+
# relative to this directory. They are copied after the builtin static files,
49+
# so a file named "default.css" will overwrite the builtin "default.css".
50+
# html_static_path = ["_static"]
51+
52+
# -- Extension configuration -------------------------------------------------
53+
54+
# Napoleon settings
55+
napoleon_google_docstring = True
56+
napoleon_numpy_docstring = False
57+
napoleon_include_init_with_doc = False
58+
napoleon_include_private_with_doc = False
59+
napoleon_include_special_with_doc = True
60+
napoleon_use_admonition_for_examples = False
61+
napoleon_use_admonition_for_notes = False
62+
napoleon_use_admonition_for_references = False
63+
napoleon_use_ivar = False
64+
napoleon_use_param = True
65+
napoleon_use_rtype = True
66+
napoleon_preprocess_types = False
67+
napoleon_type_aliases = None
68+
napoleon_attr_annotations = True
69+
70+
# Autodoc settings
71+
autodoc_typehints = "description"
72+
autodoc_default_options = {
73+
"members": True,
74+
"special-members": "__init__",
75+
"exclude-members": "__weakref__",
76+
}
77+
autodoc_mock_imports = [
78+
"django",
79+
] # Add any modules that might cause import errors during doc building
80+
81+
# Intersphinx settings
82+
intersphinx_mapping = {
83+
"python": ("https://docs.python.org/3", None),
84+
"django": ("https://docs.djangoproject.com/en/stable/", "https://docs.djangoproject.com/en/stable/_objects/"),
85+
}
86+
87+
# MyST Parser settings
88+
myst_enable_extensions = [
89+
"amsmath",
90+
"colon_fence",
91+
"deflist",
92+
"dollarmath",
93+
"html_admonition",
94+
"html_image",
95+
"linkify",
96+
"replacements",
97+
"smartquotes",
98+
"substitution",
99+
"tasklist",
100+
]
101+
102+
103+
def project_django_models(app, what, name, obj, options, lines): # pylint: disable=W0613 disable=R0913
104+
"""Process Django models for autodoc.
105+
106+
From: https://djangosnippets.org/snippets/2533/
107+
"""
108+
from django.db import models # pylint: disable=C0415
109+
110+
# Only look at objects that inherit from Django's base model class
111+
if inspect.isclass(obj) and issubclass(obj, models.Model):
112+
# Grab the field list from the meta class
113+
fields = obj._meta.get_fields() # pylint: disable=W0212
114+
115+
for field in fields:
116+
# If it's a reverse relation, skip it
117+
if isinstance(
118+
field,
119+
(
120+
models.fields.related.ManyToOneRel,
121+
models.fields.related.ManyToManyRel,
122+
models.fields.related.OneToOneRel,
123+
),
124+
):
125+
continue
126+
127+
# Decode and strip any html out of the field's help text
128+
help_text = strip_tags(field.help_text) if hasattr(field, "help_text") else None
129+
130+
# Decode and capitalize the verbose name, for use if there isn't
131+
# any help text
132+
verbose_name = field.verbose_name if hasattr(field, "verbose_name") else ""
133+
134+
if help_text:
135+
# Add the model field to the end of the docstring as a param
136+
# using the help text as the description
137+
lines.append(f":param {field.attname}: {help_text}")
138+
else:
139+
# Add the model field to the end of the docstring as a param
140+
# using the verbose name as the description
141+
lines.append(f":param {field.attname}: {verbose_name}")
142+
143+
# Add the field's type to the docstring
144+
lines.append(f":type {field.attname}: {field.__class__.__name__}")
145+
146+
# Return the extended docstring
147+
return lines
148+
149+
150+
def setup(app):
151+
"""Register the Django model processor with Sphinx."""
152+
app.connect("autodoc-process-docstring", project_django_models)

docs/reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,9 @@
120120
(`int`) Provide detailed output of the migration creation process.
121121
```
122122

123+
124+
### `removetriggers`
125+
126+
```{eval-rst}
127+
.. autoclass:: django_tenant_options.management.commands.removetriggers.Command
128+
```

docs/requirements.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
furo==2024.8.6
2-
sphinx==8.1.3
1+
# Documentation dependencies
2+
Sphinx==8.1.3
3+
sphinx-rtd-theme==2.0.0
34
sphinx-click==6.0.0
4-
myst_parser==2.0.0
5+
myst-parser==4.0
6+
furo==2024.8.6
7+
linkify-it-py==2.0.3
8+
9+
# Django and related packages (for autodoc)
10+
Django==5.0.8

0 commit comments

Comments
 (0)