├── .gitignore ├── .travis.yml ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── modelsdoc ├── __init__.py ├── constants.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── listing_models.py ├── templates │ └── modelsdoc │ │ ├── models.md │ │ ├── models.rst │ │ └── models.yaml ├── templatetags │ ├── __init__.py │ └── modelsdoc_tags.py ├── utils.py └── wrappers.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── models.py ├── sample_models.md ├── sample_models.rst ├── settings.py ├── tests │ ├── __init__.py │ ├── test_commands.py │ ├── test_templatetags.py │ ├── test_utils.py │ └── test_wrappers.py └── urls.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | \#* 4 | .\#* 5 | *.swp 6 | tmp/ 7 | build/ 8 | dist/ 9 | *.pyc 10 | *.egg 11 | *.eggs 12 | *.egg-info 13 | .tox 14 | .coverage 15 | __pycache__ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - python: 2.7 5 | env: TOXENV=py27-django15 6 | - python: 2.7 7 | env: TOXENV=py27-django16 8 | - python: 2.7 9 | env: TOXENV=py27-django17 10 | - python: 2.7 11 | env: TOXENV=py27-django18 12 | - python: 2.7 13 | env: TOXENV=py27-django19 14 | - python: 2.7 15 | env: TOXENV=py27-django110 16 | - python: 2.7 17 | env: TOXENV=py27-django111 18 | - python: pypy 19 | env: TOXENV=pypy-django15 20 | - python: pypy 21 | env: TOXENV=pypy-django16 22 | - python: pypy 23 | env: TOXENV=pypy-django17 24 | - python: pypy 25 | env: TOXENV=pypy-django18 26 | - python: pypy 27 | env: TOXENV=pypy-django19 28 | - python: pypy 29 | env: TOXENV=pypy-django110 30 | - python: pypy 31 | env: TOXENV=pypy-django111 32 | - python: 3.3 33 | env: TOXENV=py33-django17 34 | - python: 3.3 35 | env: TOXENV=py33-django18 36 | # - python: 3.3 37 | # env: TOXENV=py33-django1.9 this version does not support py33. 38 | - python: 3.4 39 | env: TOXENV=py34-django17 40 | - python: 3.4 41 | env: TOXENV=py34-django18 42 | - python: 3.4 43 | env: TOXENV=py34-django19 44 | - python: 3.4 45 | env: TOXENV=py34-django110 46 | - python: 3.4 47 | env: TOXENV=py34-django111 48 | - python: 3.4 49 | env: TOXENV=py34-django20 50 | # - python: 3.5 51 | # env: TOXENV=py35-django1.7 do not use this combination. 52 | # see also http://stackoverflow.com/q/34827566 53 | - python: 3.5 54 | env: TOXENV=py35-django18 55 | - python: 3.5 56 | env: TOXENV=py35-django19 57 | - python: 3.5 58 | env: TOXENV=py35-django110 59 | - python: 3.5 60 | env: TOXENV=py35-django111 61 | - python: 3.5 62 | env: TOXENV=py35-django20 63 | - python: 3.6 64 | env: TOXENV=py36-django18 65 | - python: 3.6 66 | env: TOXENV=py36-django19 67 | - python: 3.6 68 | env: TOXENV=py36-django110 69 | - python: 3.6 70 | env: TOXENV=py36-django111 71 | - python: 3.6 72 | env: TOXENV=py36-django20 73 | - python: 3.6 74 | env: TOXENV=py36-django21 75 | - python: 3.6 76 | env: TOXENV=py36-django22 77 | - python: 3.7 78 | env: TOXENV=py37-django20 79 | - python: 3.7 80 | env: TOXENV=py37-django21 81 | - python: 3.7 82 | env: TOXENV=py37-django22 83 | - python: 3.6 84 | env: TOXENV=flake8 85 | 86 | install: 87 | - pip install tox 88 | - if test "$TOXENV" = py36-django111; then pip install coveralls ; fi 89 | script: tox 90 | after_script: 91 | - if test "$TOXENV" = py36-django111; then coveralls ; fi 92 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 tell-k 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tox.ini 2 | include LICENSE.rst 3 | include README.rst 4 | recursive-include modelsdoc *.py *.rst *.md *.yaml 5 | recursive-include tests *.py *.rst *.md *.yaml 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Create models definitions document from your django project. This project help the documentation related to Django models. 2 | 3 | |travis| |coveralls| |version| |license| |requires| 4 | 5 | Quick start 6 | ============= 7 | 8 | 1. Add "modelsdoc" to your INSTALLED_APPS setting like this 9 | 10 | :: 11 | 12 | INSTALLED_APPS = ( 13 | ... 14 | 'modelsdoc', 15 | ) 16 | 17 | 2. Run **python manage.py listing_models** to listing model definition 18 | 19 | * You can see `the results `_ of executing the command to `tests/models.py `_. 20 | 21 | Option 22 | ======= 23 | 24 | --app(-a) 25 | ----------- 26 | 27 | You can pass specify app name. Listing only the specified app. 28 | 29 | :: 30 | 31 | $ python manage.py listing_models --app polls 32 | 33 | --output(-o) 34 | ------------- 35 | 36 | It writes the results to the specified file. 37 | 38 | :: 39 | 40 | $ python manage.py listing_models --output sample.rst 41 | 42 | --format(-f) 43 | ------------- 44 | 45 | You can choice output format. **rst** (reStructuredText) or **md** (Markdown). Default format is **rst**. 46 | 47 | :: 48 | 49 | $ python manage.py listing_models --format md 50 | 51 | Customize Settings 52 | =================== 53 | 54 | MODELSDOC_APPS 55 | ---------------- 56 | 57 | You can specify the apps and change the order. 58 | 59 | :: 60 | 61 | # output only models of poll 62 | MODELSDOC_APPS = (polls,) 63 | 64 | 65 | MODELSDOC_DISPLAY_FIELDS 66 | ------------------------- 67 | 68 | You can specify the field value and change the order. 69 | 70 | :: 71 | 72 | MODELSDOC_DISPLAY_FIELDS = ( 73 | ('Fullname', 'verbose_name'), 74 | ('Name', 'name'), 75 | ('Type', 'db_type'), 76 | ('PK', 'primary_key'), 77 | ('Unique', 'unique'), 78 | ('Index', 'db_index'), 79 | ('Null/Blank', 'null_blank'), 80 | ('Comment', 'comment'), 81 | ) 82 | 83 | MODELSDOC_MODEL_OPTIONS 84 | ------------------------- 85 | 86 | # TODO more documented 87 | 88 | :: 89 | 90 | MODELSDOC_MODEL_OPTIONS = ( 91 | 'unique_together', 92 | 'index_together', 93 | 'ordering', 94 | 'permissions', 95 | 'get_latest_by', 96 | 'order_with_respect_to', 97 | 'db_tablespace', 98 | 'abstract', 99 | 'swappable', 100 | 'select_on_save', 101 | 'default_permissions', 102 | 'default_related_name' 103 | ) 104 | 105 | Other settings 106 | --------------- 107 | 108 | # TODO more documented 109 | 110 | :: 111 | 112 | MODELSDOC_OUTPUT_TEMPLATE = 'modelsdoc/models' 113 | MODELSDOC_OUTPUT_FORMAT = 'rst' # default format 114 | MODELSDOC_MODEL_WRAPPER = 'modelsdoc.wrappers.ModelWrapper' 115 | MODELSDOC_FIELD_WRAPPER = 'modelsdoc.wrappers.FieldWrapper' 116 | MODELSDOC_INCLUDE_AUTO_CREATED = True 117 | 118 | 119 | Python and Django Support 120 | ========================= 121 | 122 | .. csv-table:: 123 | :widths: 10, 10, 10, 10, 10, 10, 10, 10, 10 124 | 125 | " ", "Django.1.5", "Django1.6", "Django1.7", "Django1.8", "Django1.9", "Django1.10", "Django1.11", "Django2.0" 126 | "Python 2.7","◯","◯","◯","◯","◯","◯","◯","" 127 | "PyPy","◯","◯","◯","◯","◯","◯","◯","" 128 | "Python 3.3","","","◯","◯","","","","" 129 | "Python 3.4","","","◯","◯","◯","◯","◯","◯" 130 | "Python 3.5","","","","◯","◯","◯","◯","◯" 131 | "Python 3.6","","","","◯","◯","◯","◯","◯" 132 | 133 | 134 | License 135 | ======= 136 | 137 | MIT Licence. See the LICENSE file for specific terms. 138 | 139 | 140 | Authors 141 | ========= 142 | 143 | * tell-k 144 | * wanshot 145 | 146 | History 147 | ======= 148 | 149 | 0.1.11(Nov 28, 2019) 150 | --------------------- 151 | * Add models.yaml template file. 152 | 153 | 0.1.10(Nov 28, 2019) 154 | --------------------- 155 | * Add Support YAML format. 156 | 157 | 0.1.9(Feb 8, 2018) 158 | --------------------- 159 | * Fix `Set section length dynamically `_. Thanks to wanshot 160 | 161 | 0.1.8(Dec 3, 2017) 162 | --------------------- 163 | * Add Support Django2.0 164 | 165 | 0.1.7(May 29, 2017) 166 | --------------------- 167 | * Add Support Django1.11 and Python 3.6 168 | 169 | 0.1.6(Nov 4, 2016) 170 | --------------------- 171 | * Add Support Django1.10 172 | 173 | 0.1.5(May 4, 2016) 174 | --------------------- 175 | * Add Support Python3.5 and Django1.9 176 | 177 | 0.1.4(Sep 23, 2015) 178 | --------------------- 179 | * Fixed bug. When print models, linebreak is ignored. 180 | * Add ManyToManyField's info on "listing_models" results. 181 | 182 | 0.1.3(Jul 19, 2015) 183 | --------------------- 184 | * Fixed bug. install test code. 185 | * Add new option "MODELSDOC_INCLUDE_AUTO_CREATED" 186 | 187 | 0.1.2(Jun 21, 2015) 188 | --------------------- 189 | * Bug fixed. Not include output templates. 190 | 191 | 0.1.0(Jun 21, 2015) 192 | --------------------- 193 | * First release 194 | 195 | 196 | .. |travis| image:: https://travis-ci.org/tell-k/django-modelsdoc.svg?branch=master 197 | :target: https://travis-ci.org/tell-k/django-modelsdoc 198 | 199 | .. |coveralls| image:: https://coveralls.io/repos/tell-k/django-modelsdoc/badge.png 200 | :target: https://coveralls.io/r/tell-k/django-modelsdoc 201 | :alt: coveralls.io 202 | 203 | .. |requires| image:: https://requires.io/github/tell-k/django-modelsdoc/requirements.svg?branch=master 204 | :target: https://requires.io/github/tell-k/django-modelsdoc/requirements/?branch=master 205 | :alt: requirements status 206 | 207 | .. |version| image:: https://img.shields.io/pypi/v/django-modelsdoc.svg 208 | :target: http://pypi.python.org/pypi/django-modelsdoc/ 209 | :alt: latest version 210 | 211 | .. |license| image:: https://img.shields.io/pypi/l/django-modelsdoc.svg 212 | :target: http://pypi.python.org/pypi/django-modelsdoc/ 213 | :alt: license 214 | -------------------------------------------------------------------------------- /modelsdoc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | modelsdoc 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | __version__ = '0.1.11' 12 | -------------------------------------------------------------------------------- /modelsdoc/constants.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | modelsdoc.constants 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | from django.conf import settings 11 | 12 | 13 | DEFAULT_DISPLAY_FIELDS = ( 14 | ('Fullname', 'verbose_name'), 15 | ('Name', 'name'), 16 | ('Type', 'db_type'), 17 | ('PK', 'primary_key'), 18 | ('Unique', 'unique'), 19 | ('Index', 'db_index'), 20 | ('Null/Blank', 'null_blank'), 21 | ('Comment', 'comment'), 22 | ) 23 | DEFAULT_OUTPUT_TEMPLATE = 'modelsdoc/models' 24 | DEFAULT_OUTPUT_FORMAT = 'rst' 25 | DEFAULT_MODEL_WRAPPER = 'modelsdoc.wrappers.ModelWrapper' 26 | DEFAULT_FIELD_WRAPPER = 'modelsdoc.wrappers.FieldWrapper' 27 | DEFAULT_INCLUDE_AUTO_CREATED = True 28 | 29 | DEFAULT_MODEL_OPTIONS = ( 30 | 'unique_together', 31 | 'index_together', 32 | 'ordering', 33 | 'permissions', 34 | 'get_latest_by', 35 | 'order_with_respect_to', 36 | 'db_tablespace', 37 | 'abstract', 38 | 'swappable', 39 | 'select_on_save', 40 | 'default_permissions', 41 | 'default_related_name' 42 | 'auto_created', 43 | # 'managed', 44 | # 'proxy', 45 | # 'verbose_name', 46 | # 'verbose_name_plural', 47 | # 'app_label', 48 | # 'db_table', 49 | # 'apps', 50 | ) 51 | 52 | APPS = getattr(settings, 'MODELSDOC_APPS', []) 53 | 54 | INCLUDE_AUTO_CREATED = getattr(settings, 'MODELSDOC_INCLUDE_AUTO_CREATED', 55 | DEFAULT_INCLUDE_AUTO_CREATED) 56 | DISPLAY_FIELDS = getattr(settings, 'MODELSDOC_DISPLAY_FIELDS', 57 | DEFAULT_DISPLAY_FIELDS) 58 | OUTPUT_TEMPLATE = getattr(settings, 'MODELSDOC_OUTPUT_TEMPLATE', 59 | DEFAULT_OUTPUT_TEMPLATE) 60 | OUTPUT_FORMAT = getattr(settings, 'MODELSDOC_OUTPUT_FORMAT', 61 | DEFAULT_OUTPUT_FORMAT) 62 | MODEL_WRAPPER = getattr(settings, 'MODELSDOC_MODEL_WRAPPER', 63 | DEFAULT_MODEL_WRAPPER) 64 | FIELD_WRAPPER = getattr(settings, 'MODELSDOC_FIELD_WRAPPER', 65 | DEFAULT_FIELD_WRAPPER) 66 | MODEL_OPTIONS = getattr(settings, 'MODELSDOC_MODEL_OPTIONS', 67 | DEFAULT_MODEL_OPTIONS) 68 | -------------------------------------------------------------------------------- /modelsdoc/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tell-k/django-modelsdoc/da8ef1c1ba4f20cecac98ca963d55352e6b3bba7/modelsdoc/management/__init__.py -------------------------------------------------------------------------------- /modelsdoc/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tell-k/django-modelsdoc/da8ef1c1ba4f20cecac98ca963d55352e6b3bba7/modelsdoc/management/commands/__init__.py -------------------------------------------------------------------------------- /modelsdoc/management/commands/listing_models.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | modelsdoc.management.commands.listing_models 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | import sys 11 | 12 | import logging 13 | from optparse import make_option 14 | 15 | import django 16 | from django.core.management.base import BaseCommand 17 | from django.core.exceptions import ImproperlyConfigured 18 | from django.db import connection 19 | 20 | from django.template.loader import render_to_string 21 | from django.template import TemplateDoesNotExist 22 | 23 | from modelsdoc import constants 24 | from modelsdoc.utils import import_class, get_model_attr, get_models 25 | 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class Command(BaseCommand): 31 | """ listing_models command """ 32 | 33 | help = 'Listing your model definition. You can pass specify app name.' 34 | 35 | option_args = [ 36 | dict( 37 | args=['-a', '--app'], 38 | kwargs=dict(dest='app', 39 | help='Target only a specific app', default=None) 40 | ), 41 | dict( 42 | args=['-o', '--output'], 43 | kwargs=dict(dest='output_file', 44 | help='Output file', default=None), 45 | ), 46 | dict( 47 | args=['-f', '--format'], 48 | kwargs=dict(dest='output_format', 49 | help='Output format(rst/md)', 50 | default=constants.OUTPUT_FORMAT) 51 | ), 52 | ] 53 | 54 | def __init__(self): 55 | if django.VERSION < (1, 8): # pragma: no cover 56 | options = tuple([make_option(*o['args'], **o['kwargs']) for o in self.option_args]) # NOQA 57 | Command.option_list = BaseCommand.option_list + options 58 | else: 59 | def add_arguments(self, parser): 60 | for o in self.option_args: 61 | parser.add_argument(*o['args'], **o['kwargs']) 62 | Command.add_arguments = add_arguments 63 | 64 | super(Command, self).__init__() 65 | 66 | def handle(self, app, output_file, output_format, *args, **options): 67 | 68 | if app or constants.APPS: 69 | apps = [app] if app else constants.APPS 70 | models = [] 71 | for app in apps: 72 | try: 73 | models += get_models( 74 | constants.INCLUDE_AUTO_CREATED, 75 | django.VERSION, 76 | app 77 | ) 78 | except (ImproperlyConfigured, LookupError): 79 | pass 80 | else: 81 | models = get_models(constants.INCLUDE_AUTO_CREATED, django.VERSION) 82 | models = sorted( 83 | models, 84 | key=lambda m: get_model_attr(m._meta, django.VERSION).__module__ # NOQA 85 | ) 86 | 87 | if not models: 88 | msg = 'Cannot find models. Please add one model at least.' 89 | print(msg, file=sys.stderr) 90 | return 91 | 92 | try: 93 | model_wrapper = import_class(constants.MODEL_WRAPPER) 94 | template = '{}.{}'.format(constants.OUTPUT_TEMPLATE, output_format) 95 | rendered = render_to_string( 96 | template, 97 | { 98 | 'models': [model_wrapper(m, connection) for m in models], 99 | 'display_fields': constants.DISPLAY_FIELDS, 100 | } 101 | ) 102 | except TemplateDoesNotExist: 103 | msg = 'Cannot find the output template file. {}' 104 | print(msg.format(template), file=sys.stderr) 105 | return 106 | 107 | if output_file: 108 | with open(output_file, 'wb') as fp: 109 | fp.write(rendered.encode('utf-8', errors='replace')) 110 | print('Complete! Create the output file. {}'.format(output_file)) 111 | else: 112 | print(rendered) 113 | -------------------------------------------------------------------------------- /modelsdoc/templates/modelsdoc/models.md: -------------------------------------------------------------------------------- 1 | {% load modelsdoc_tags %} 2 | {% autoescape off %} 3 | 4 | {% for model in models %} 5 | ## {{ model.display_name }} 6 | 7 | ``` 8 | {{ model.doc }} 9 | ``` 10 | 11 | {% emptylineless %} 12 | {% for label, attr in display_fields %}|{{ label }}{% endfor %}| 13 | {% for label, attr in display_fields %}|---{% endfor %}| 14 | {% for field in model.fields %} 15 | {% for label, attr in display_fields %}|{{ field|get_attr:attr }} {% endfor %}| 16 | {% endfor %} 17 | {% endemptylineless %} 18 | {% if model.model_options %} 19 | Options 20 | ``` 21 | {% emptylineless %} 22 | {% for name, value in model.model_options.items %} 23 | {{ name }} : {{ value|safe }} 24 | {% endfor %} 25 | {% endemptylineless %} 26 | ``` 27 | {% endif %} 28 | {% endfor %} 29 | {% endautoescape %} 30 | -------------------------------------------------------------------------------- /modelsdoc/templates/modelsdoc/models.rst: -------------------------------------------------------------------------------- 1 | {% load modelsdoc_tags %} 2 | {% autoescape off %} 3 | 4 | .. contents:: 5 | :local: 6 | 7 | {% for model in models %} 8 | {{ model.display_name }} 9 | {{ model.display_name_length|str_repeat:'-' }} 10 | 11 | :: 12 | 13 | {{ model.doc }} 14 | 15 | .. list-table:: 16 | :header-rows: 1 17 | 18 | {% emptylineless %} 19 | {% for label, attr in display_fields %} 20 | {% if forloop.first %}*{% else %} {% endif %} - {{ label }} 21 | {% endfor %} 22 | 23 | {% for field in model.fields %} 24 | {% for label, attr in display_fields %} 25 | {% if forloop.first %}*{% else %} {% endif %} - {{ field|get_attr:attr }} 26 | {% endfor %} 27 | {% endfor %} 28 | {% endemptylineless %} 29 | 30 | {% if model.model_options %} 31 | Options:: 32 | 33 | {% emptylineless %} 34 | {% for name, value in model.model_options.items %} 35 | {{ name }} : {{ value|safe }} 36 | {% endfor %} 37 | {% endemptylineless %} 38 | {% endif %} 39 | {% endfor %} 40 | {% endautoescape %} 41 | -------------------------------------------------------------------------------- /modelsdoc/templates/modelsdoc/models.yaml: -------------------------------------------------------------------------------- 1 | {% load modelsdoc_tags %} 2 | {% autoescape off %} 3 | {{ models|yamldump }} 4 | {% endautoescape %} 5 | -------------------------------------------------------------------------------- /modelsdoc/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tell-k/django-modelsdoc/da8ef1c1ba4f20cecac98ca963d55352e6b3bba7/modelsdoc/templatetags/__init__.py -------------------------------------------------------------------------------- /modelsdoc/templatetags/modelsdoc_tags.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | """ 3 | modelsdoc.templatetags.modelsdoc_tags 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | import django 12 | from django.db.models.fields import NOT_PROVIDED 13 | from django.db import router 14 | from django.template import Library, Node 15 | 16 | import yaml 17 | 18 | from modelsdoc.utils import get_parent_model_attr, get_related_field 19 | 20 | register = Library() 21 | 22 | 23 | class EmptylinelessNode(Node): 24 | 25 | def __init__(self, nodelist): 26 | self.nodelist = nodelist 27 | 28 | def render(self, context): 29 | lines = [] 30 | for line in self.nodelist.render(context).strip().split('\n'): 31 | if not line.strip(): 32 | continue 33 | lines.append(line) 34 | return '\n'.join(lines) 35 | 36 | 37 | @register.tag 38 | def emptylineless(parser, token): 39 | """ 40 | Removes empty line. 41 | 42 | Example usage:: 43 | 44 | {% emptylineless %} 45 | test1 46 | 47 | test2 48 | 49 | test3 50 | {% endemptylineless %} 51 | 52 | This example would return this HTML:: 53 | 54 | test1 55 | test2 56 | test3 57 | 58 | """ 59 | nodelist = parser.parse(('endemptylineless',)) 60 | parser.delete_first_token() 61 | return EmptylinelessNode(nodelist) 62 | 63 | 64 | @register.filter 65 | def get_attr(obj, attr): 66 | return getattr(obj, attr, '') 67 | 68 | 69 | @register.filter 70 | def str_repeat(times, string): 71 | return string * times 72 | 73 | 74 | @register.filter 75 | def yamldump(models): 76 | result = {} 77 | 78 | for model in models: 79 | dbname = router.db_for_read(model) 80 | 81 | table = { 82 | "name": model._model._meta.db_table, 83 | "logical_name": model.display_name, 84 | "comment": model.doc, 85 | "columns": [], 86 | } 87 | for field in model.fields: 88 | 89 | fk_table = None 90 | related = get_related_field(field, django.VERSION) 91 | if related: 92 | # TODO change table to column 93 | fk = get_parent_model_attr(related, django.VERSION) 94 | fk_table = fk._meta.db_table 95 | 96 | default_value = None 97 | if field.default != NOT_PROVIDED: 98 | default_value = field.default 99 | if callable(default_value): 100 | default_value = (default_value.__module__ 101 | + "." + default_value.__name__) 102 | 103 | column = { 104 | "name": field.name, 105 | "verbose_name": str(field.verbose_name), 106 | "data_type": field.db_type, 107 | "is_primary": field.primary_key or False, 108 | "is_unique": field.unique or False, 109 | "is_index": field.db_index or False, 110 | "not_null": not field.null, 111 | "foreignkey": fk_table, 112 | "comment": field.comment, 113 | "default": default_value, 114 | } 115 | table["columns"].append(column) 116 | 117 | if dbname not in result: 118 | result[dbname] = dict( 119 | name=dbname, 120 | logical_name=dbname, 121 | comment="", 122 | tables=[] 123 | ) 124 | 125 | result[dbname]["tables"].append(table) 126 | 127 | return yaml.dump(result, allow_unicode=True, default_flow_style=False) 128 | -------------------------------------------------------------------------------- /modelsdoc/utils.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | modelsdoc.utils 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | import django 12 | 13 | 14 | def get_models(include_auto_created, django_version, appname=None): 15 | if django_version < (1, 7): # pragma: no cover 16 | from django.db.models import get_app, get_models 17 | if appname: 18 | return get_models( 19 | app_mod=get_app(appname), 20 | include_auto_created=include_auto_created 21 | ) 22 | else: 23 | return get_models( 24 | include_auto_created=include_auto_created 25 | ) 26 | else: 27 | from django.apps import apps 28 | if appname: 29 | app = apps.get_app_config(appname) 30 | return app.get_models(include_auto_created=include_auto_created) 31 | else: 32 | return apps.get_models(include_auto_created=include_auto_created) 33 | 34 | 35 | def get_model_attr(option_model, django_version): 36 | if django_version < (1, 6): 37 | return getattr(option_model, 'concrete_model') 38 | else: 39 | return getattr(option_model, 'model') 40 | 41 | 42 | def get_fields_attr(option_model, django_version): 43 | if django_version < (1, 6): 44 | fields = list(getattr(option_model, 'fields')) 45 | else: 46 | fields = list(getattr(option_model, 'concrete_fields')) 47 | for f in getattr(option_model, 'many_to_many', []): 48 | fields.append(f) 49 | return fields 50 | 51 | 52 | def get_parent_model_attr(related_field, django_version): 53 | if django_version < (1, 8): 54 | return getattr(related_field, 'parent_model') 55 | else: 56 | return getattr(related_field, 'model') 57 | 58 | 59 | def class_to_string(model): 60 | return '{}.{}'.format(model.__module__, model.__name__) 61 | 62 | 63 | def get_null_blank(field): 64 | if field.blank and field.null: 65 | return 'Both' 66 | elif field.blank: 67 | return 'Blank' 68 | elif field.null: 69 | return 'Null' 70 | return '' 71 | 72 | 73 | def get_related_field(field, django_version): 74 | if django_version < (1, 9): 75 | return getattr(field, 'related', None) 76 | else: 77 | return getattr(field, 'remote_field', None) 78 | 79 | 80 | def get_through(field, django_version): 81 | if django_version < (1, 9): 82 | return field.rel.through 83 | else: 84 | return field.remote_field.through 85 | 86 | 87 | def get_foreignkey(field): 88 | related_field = get_related_field(field, django.VERSION) 89 | if not related_field: 90 | return '' 91 | 92 | label = 'FK:' 93 | through = '' 94 | if hasattr(field, 'm2m_column_name'): 95 | label = 'M2M:' 96 | through = ' (through: {})'.format( 97 | class_to_string(get_through(field, django.VERSION))) 98 | 99 | return '{}{}{}'.format( 100 | label, 101 | class_to_string(get_parent_model_attr(related_field, django.VERSION)), 102 | through 103 | ) 104 | 105 | 106 | def get_choices(field): 107 | if not getattr(field, 'choices', None): 108 | return '' 109 | return ', '.join(['{}:{}'.format(*c) for c in field.choices]) 110 | 111 | 112 | def import_class(cl): 113 | d = cl.rfind('.') 114 | classname = cl[d + 1:len(cl)] 115 | m = __import__(cl[0:d], globals(), locals(), [classname]) 116 | return getattr(m, classname) 117 | -------------------------------------------------------------------------------- /modelsdoc/wrappers.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | modelsdoc.wrappers 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | from sphinx.pycode import ModuleAnalyzer 12 | import django 13 | from django.utils.encoding import force_bytes 14 | 15 | from modelsdoc.utils import ( 16 | class_to_string, get_foreignkey, 17 | get_choices, get_null_blank, import_class, 18 | get_model_attr, get_fields_attr 19 | ) 20 | from modelsdoc import constants 21 | 22 | 23 | class FieldWrapper(object): 24 | 25 | def __init__(self, field, model, connection, attrdocs): 26 | self._model = model 27 | self._field = field 28 | self._connection = connection 29 | self._attrdocs = attrdocs 30 | 31 | @property 32 | def comment(self): 33 | comment = get_foreignkey(self._field) 34 | comment += get_choices(self._field) 35 | key = (self._model._model.__name__, self._field.name) 36 | comment += ' '.join(self._attrdocs.get(key, [])) 37 | return comment 38 | 39 | @property 40 | def null_blank(self): 41 | return get_null_blank(self._field) 42 | 43 | @property 44 | def db_type(self): 45 | return self._field.db_type(self._connection) or '' 46 | 47 | def __getattr__(self, name): 48 | if hasattr(self._field, name): 49 | return getattr(self._field, name) or '' 50 | return '' 51 | 52 | 53 | class ModelWrapper(object): 54 | 55 | def __init__(self, model, connection): 56 | self._model = model 57 | self._attrdocs = [] 58 | self._model_options = {} 59 | self._connection = connection 60 | self._field_wrapper_cls = import_class(constants.FIELD_WRAPPER) 61 | 62 | @property 63 | def class_fullname(self): 64 | return class_to_string( 65 | get_model_attr(self._model._meta, django.VERSION)) 66 | 67 | @property 68 | def class_name(self): 69 | return self._model._model.__name__ 70 | 71 | @property 72 | def display_name(self): 73 | return '{}({})'.format(self.name, self.class_fullname) 74 | 75 | @property 76 | def display_name_length(self): 77 | """ Return length of byte string. for reST section """ 78 | return len(force_bytes(self.display_name)) 79 | 80 | @property 81 | def doc(self): 82 | return self._model.__doc__ 83 | 84 | @property 85 | def attrdocs(self): 86 | if self._attrdocs: 87 | return self._attrdocs 88 | analyzer = ModuleAnalyzer.for_module(self._model.__module__) 89 | self._attrdocs = analyzer.find_attr_docs() 90 | return self._attrdocs 91 | 92 | @property 93 | def model_options(self): 94 | if self._model_options: 95 | return self._model_options 96 | for option in constants.MODEL_OPTIONS: 97 | if not hasattr(self._model._meta, option) or\ 98 | not getattr(self._model._meta, option): 99 | continue 100 | self._model_options.update( 101 | {option: getattr(self._model._meta, option)}) 102 | return self._model_options 103 | 104 | @property 105 | def name(self): 106 | return self._model._meta.verbose_name 107 | 108 | @property 109 | def fields(self): 110 | return [ 111 | self._field_wrapper_cls(f, self, self._connection, self.attrdocs) 112 | for f in get_fields_attr(self._model._meta, django.VERSION) 113 | ] 114 | 115 | def __getattr__(self, name): 116 | if hasattr(self._model, name): 117 | return getattr(self._model, name) or '' 118 | return '' 119 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [sdist] 2 | formats = gztar 3 | 4 | [upload_docs] 5 | upload-dir = _build/sphinx/html 6 | 7 | [wheel] 8 | universal = 1 9 | 10 | [check] 11 | strict = 1 12 | 13 | [aliases] 14 | ;upload_docs = build_sphinx upload_docs 15 | ;release = upload_docs sdist upload bdist_wheel 16 | release = sdist bdist_wheel 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import os 5 | import sys 6 | from setuptools import setup, find_packages 7 | from setuptools import Command 8 | from setuptools.command.test import test as TestCommand 9 | 10 | 11 | class DjangoTest(TestCommand): 12 | 13 | def finalize_options(self): 14 | TestCommand.finalize_options(self) 15 | self.test_args = [] 16 | self.test_suite = True 17 | 18 | def run_tests(self): 19 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 20 | 21 | test_dir = os.path.dirname(__file__) 22 | sys.path.insert(0, test_dir) 23 | 24 | import django 25 | from django.test.utils import get_runner 26 | from django.conf import settings 27 | 28 | if django.VERSION >= (1, 7): 29 | django.setup() 30 | 31 | runner = get_runner(settings)( 32 | verbosity=1, 33 | interactive=False, failfast=False 34 | ) 35 | errno = runner.run_tests(['tests']) 36 | sys.exit(errno) 37 | 38 | 39 | class GenerateSamples(Command): 40 | 41 | description = 'Generate sample docs into tests directory.' 42 | user_options = [] 43 | 44 | def initialize_options(self): 45 | pass 46 | 47 | def finalize_options(self): 48 | pass 49 | 50 | def run(self): 51 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 52 | sys.path.insert(0, os.path.dirname(__file__)) 53 | 54 | import django 55 | from django.core.management import call_command 56 | 57 | if django.VERSION >= (1, 7): 58 | django.setup() 59 | 60 | call_command('listing_models', format='rst', output='tests/sample_models.rst') 61 | call_command('listing_models', format='md', output='tests/sample_models.md') 62 | 63 | 64 | here = os.path.dirname(__file__) 65 | 66 | with open(os.path.join(here, 'modelsdoc', '__init__.py'), 'r') as f: 67 | version = re.compile( 68 | r".*__version__ = '(.*?)'", re.S).match(f.read()).group(1) 69 | 70 | with open(os.path.join(here, 'README.rst')) as fp: 71 | readme = fp.read() 72 | 73 | # allow setup.py to be run from any path 74 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 75 | 76 | install_requires = [ 77 | 'Django', 78 | 'Sphinx', 79 | 'PyYAML', 80 | ] 81 | 82 | tests_require = [ 83 | 'mock', 84 | 'testfixtures', 85 | 'six', 86 | 'pbr<1.4', 87 | ] 88 | 89 | classifiers = [ 90 | 'Environment :: Web Environment', 91 | 'Framework :: Django', 92 | 'Intended Audience :: Developers', 93 | 'License :: OSI Approved :: MIT License', 94 | 'Operating System :: OS Independent', 95 | 'Framework :: Django', 96 | 'Framework :: Django :: 1.5', 97 | 'Framework :: Django :: 1.6', 98 | 'Framework :: Django :: 1.7', 99 | 'Framework :: Django :: 1.8', 100 | 'Framework :: Django :: 1.9', 101 | 'Framework :: Django :: 1.10', 102 | 'Framework :: Django :: 1.11', 103 | 'Framework :: Django :: 2.0', 104 | 'Programming Language :: Python', 105 | 'Programming Language :: Python :: 2', 106 | 'Programming Language :: Python :: 2.7', 107 | 'Programming Language :: Python :: 3', 108 | 'Programming Language :: Python :: 3.3', 109 | 'Programming Language :: Python :: 3.4', 110 | 'Programming Language :: Python :: 3.5', 111 | 'Programming Language :: Python :: 3.6', 112 | 'Topic :: Documentation', 113 | 'Topic :: Documentation :: Sphinx', 114 | ] 115 | 116 | setup( 117 | name='django-modelsdoc', 118 | version=version, 119 | packages=find_packages(exclude=['tests', 'tests.*']), 120 | include_package_data=True, 121 | license='MIT', 122 | keywords='django models document documentation', 123 | description='Create models definitions document from your Django project.', 124 | long_description=readme, 125 | url='https://github.com/tell-k/django-modelsdoc', 126 | install_requires=install_requires, 127 | tests_require=tests_require, 128 | cmdclass={ 129 | 'test': DjangoTest, 130 | 'generate_samples': GenerateSamples 131 | }, 132 | author='tell-k', 133 | author_email='ffk2005@gmail.com', 134 | classifiers=classifiers, 135 | ) 136 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tell-k/django-modelsdoc/da8ef1c1ba4f20cecac98ca963d55352e6b3bba7/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | from django.contrib.auth.models import User 5 | 6 | 7 | class Poll(models.Model): 8 | """ Poll 9 | 10 | * Poll has question and description fields 11 | """ 12 | 13 | question = models.CharField('Question Name', max_length=255) 14 | description = models.TextField('Description', blank=True) 15 | """ Description field allows Blank """ 16 | 17 | null_field = models.CharField('Null Test', null=True, max_length=255) 18 | blank_field = models.CharField('Blank Test', blank=True, max_length=255) 19 | both_field = models.CharField('Both Test', 20 | null=True, blank=True, max_length=255) 21 | index_field = models.CharField('Index Test', db_index=True, max_length=255) 22 | 23 | class Meta: 24 | verbose_name = 'Poll' 25 | 26 | 27 | class Genre(models.Model): 28 | """ Genre 29 | 30 | * Choice has genre 31 | """ 32 | name = models.CharField('Genre name', max_length=255) 33 | 34 | class Meta: 35 | verbose_name = 'Genre' 36 | 37 | 38 | class Choice(models.Model): 39 | """ Choice 40 | 41 | * Choice has poll reference 42 | * Choice has choices field 43 | """ 44 | 45 | CHOICES = ( 46 | (1, 'test1'), 47 | (2, 'test2'), 48 | (3, 'test3'), 49 | ) 50 | 51 | poll = models.ForeignKey(Poll, verbose_name='Poll', 52 | on_delete=models.CASCADE) 53 | choice = models.SmallIntegerField('Choice', choices=CHOICES) 54 | 55 | genres = models.ManyToManyField(Genre, verbose_name='Genre') 56 | 57 | class Meta: 58 | verbose_name = 'Choice' 59 | 60 | 61 | class Vote(models.Model): 62 | """ Vote 63 | 64 | * Vote has user reference 65 | * Vote has poll reference 66 | * Vote has choice reference 67 | """ 68 | 69 | user = models.ForeignKey(User, verbose_name='Voted User', 70 | on_delete=models.CASCADE) 71 | poll = models.ForeignKey(Poll, verbose_name='Voted Poll', 72 | on_delete=models.CASCADE) 73 | choice = models.ForeignKey(Choice, verbose_name='Voted Choice', 74 | on_delete=models.CASCADE) 75 | 76 | class Meta: 77 | verbose_name = 'Vote' 78 | unique_together = (('user', 'poll')) 79 | -------------------------------------------------------------------------------- /tests/sample_models.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## permission(django.contrib.auth.models.Permission) 6 | 7 | ``` 8 | 9 | The permissions system provides a way to assign permissions to specific 10 | users and groups of users. 11 | 12 | The permission system is used by the Django admin site, but may also be 13 | useful in your own code. The Django admin site uses permissions as follows: 14 | 15 | - The "add" permission limits the user's ability to view the "add" form 16 | and add an object. 17 | - The "change" permission limits a user's ability to view the change 18 | list, view the "change" form and change an object. 19 | - The "delete" permission limits the ability to delete an object. 20 | 21 | Permissions are set globally per type of object, not per specific object 22 | instance. It is possible to say "Mary may change news stories," but it's 23 | not currently possible to say "Mary may change news stories, but only the 24 | ones she created herself" or "Mary may only change news stories that have a 25 | certain status or publication date." 26 | 27 | Three basic permissions -- add, change and delete -- are automatically 28 | created for each Django model. 29 | 30 | ``` 31 | 32 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 33 | |---|---|---|---|---|---|---|---| 34 | |ID |id |integer |True |True | |Blank | | 35 | |name |name |varchar(255) | | | | | | 36 | |content type |content_type |integer | | |True | |FK:django.contrib.contenttypes.models.ContentType | 37 | |codename |codename |varchar(100) | | | | | | 38 | 39 | Options 40 | ``` 41 | unique_together : (('content_type', 'codename'),) 42 | ordering : ('content_type__app_label', 'content_type__model', 'codename') 43 | default_permissions : ('add', 'change', 'delete') 44 | ``` 45 | 46 | 47 | ## group-permission relationship(django.contrib.auth.models.Group_permissions) 48 | 49 | ``` 50 | Group_permissions(id, group, permission) 51 | ``` 52 | 53 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 54 | |---|---|---|---|---|---|---|---| 55 | |ID |id |integer |True |True | |Blank | | 56 | |group |group |integer | | |True | |FK:django.contrib.auth.models.Group | 57 | |permission |permission |integer | | |True | |FK:django.contrib.auth.models.Permission | 58 | 59 | Options 60 | ``` 61 | unique_together : (('group', 'permission'),) 62 | default_permissions : ('add', 'change', 'delete') 63 | ``` 64 | 65 | 66 | ## group(django.contrib.auth.models.Group) 67 | 68 | ``` 69 | 70 | Groups are a generic way of categorizing users to apply permissions, or 71 | some other label, to those users. A user can belong to any number of 72 | groups. 73 | 74 | A user in a group automatically has all the permissions granted to that 75 | group. For example, if the group 'Site editors' has the permission 76 | can_edit_home_page, any user in that group will have that permission. 77 | 78 | Beyond permissions, groups are a convenient way to categorize users to 79 | apply some label, or extended functionality, to them. For example, you 80 | could create a group 'Special users', and you could write code that would 81 | do special things to those users -- such as giving them access to a 82 | members-only portion of your site, or sending them members-only email 83 | messages. 84 | 85 | ``` 86 | 87 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 88 | |---|---|---|---|---|---|---|---| 89 | |ID |id |integer |True |True | |Blank | | 90 | |name |name |varchar(80) | |True | | | | 91 | |permissions |permissions | | | | |Blank |M2M:django.contrib.auth.models.Permission (through: django.contrib.auth.models.Group_permissions) | 92 | 93 | Options 94 | ``` 95 | default_permissions : ('add', 'change', 'delete') 96 | ``` 97 | 98 | 99 | ## user-group relationship(django.contrib.auth.models.User_groups) 100 | 101 | ``` 102 | User_groups(id, user, group) 103 | ``` 104 | 105 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 106 | |---|---|---|---|---|---|---|---| 107 | |ID |id |integer |True |True | |Blank | | 108 | |user |user |integer | | |True | |FK:django.contrib.auth.models.User | 109 | |group |group |integer | | |True | |FK:django.contrib.auth.models.Group | 110 | 111 | Options 112 | ``` 113 | unique_together : (('user', 'group'),) 114 | default_permissions : ('add', 'change', 'delete') 115 | ``` 116 | 117 | 118 | ## user-permission relationship(django.contrib.auth.models.User_user_permissions) 119 | 120 | ``` 121 | User_user_permissions(id, user, permission) 122 | ``` 123 | 124 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 125 | |---|---|---|---|---|---|---|---| 126 | |ID |id |integer |True |True | |Blank | | 127 | |user |user |integer | | |True | |FK:django.contrib.auth.models.User | 128 | |permission |permission |integer | | |True | |FK:django.contrib.auth.models.Permission | 129 | 130 | Options 131 | ``` 132 | unique_together : (('user', 'permission'),) 133 | default_permissions : ('add', 'change', 'delete') 134 | ``` 135 | 136 | 137 | ## user(django.contrib.auth.models.User) 138 | 139 | ``` 140 | 141 | Users within the Django authentication system are represented by this 142 | model. 143 | 144 | Username, password and email are required. Other fields are optional. 145 | 146 | ``` 147 | 148 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 149 | |---|---|---|---|---|---|---|---| 150 | |ID |id |integer |True |True | |Blank | | 151 | |password |password |varchar(128) | | | | | | 152 | |last login |last_login |datetime | | | |Both | | 153 | |superuser status |is_superuser |bool | | | |Blank | | 154 | |username |username |varchar(150) | |True | | | | 155 | |first name |first_name |varchar(30) | | | |Blank | | 156 | |last name |last_name |varchar(150) | | | |Blank | | 157 | |email address |email |varchar(254) | | | |Blank | | 158 | |staff status |is_staff |bool | | | |Blank | | 159 | |active |is_active |bool | | | |Blank | | 160 | |date joined |date_joined |datetime | | | | | | 161 | |groups |groups | | | | |Blank |M2M:django.contrib.auth.models.Group (through: django.contrib.auth.models.User_groups) | 162 | |user permissions |user_permissions | | | | |Blank |M2M:django.contrib.auth.models.Permission (through: django.contrib.auth.models.User_user_permissions) | 163 | 164 | Options 165 | ``` 166 | swappable : AUTH_USER_MODEL 167 | default_permissions : ('add', 'change', 'delete') 168 | ``` 169 | 170 | 171 | ## content type(django.contrib.contenttypes.models.ContentType) 172 | 173 | ``` 174 | ContentType(id, app_label, model) 175 | ``` 176 | 177 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 178 | |---|---|---|---|---|---|---|---| 179 | |ID |id |integer |True |True | |Blank | | 180 | |app label |app_label |varchar(100) | | | | | | 181 | |python model class name |model |varchar(100) | | | | | | 182 | 183 | Options 184 | ``` 185 | unique_together : (('app_label', 'model'),) 186 | default_permissions : ('add', 'change', 'delete') 187 | ``` 188 | 189 | 190 | ## site(django.contrib.sites.models.Site) 191 | 192 | ``` 193 | Site(id, domain, name) 194 | ``` 195 | 196 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 197 | |---|---|---|---|---|---|---|---| 198 | |ID |id |integer |True |True | |Blank | | 199 | |domain name |domain |varchar(100) | |True | | | | 200 | |display name |name |varchar(50) | | | | | | 201 | 202 | Options 203 | ``` 204 | ordering : ('domain',) 205 | default_permissions : ('add', 'change', 'delete') 206 | ``` 207 | 208 | 209 | ## Poll(tests.models.Poll) 210 | 211 | ``` 212 | Poll 213 | 214 | * Poll has question and description fields 215 | 216 | ``` 217 | 218 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 219 | |---|---|---|---|---|---|---|---| 220 | |ID |id |integer |True |True | |Blank | | 221 | |Question Name |question |varchar(255) | | | | | | 222 | |Description |description |text | | | |Blank |Description field allows Blank | 223 | |Null Test |null_field |varchar(255) | | | |Null | | 224 | |Blank Test |blank_field |varchar(255) | | | |Blank | | 225 | |Both Test |both_field |varchar(255) | | | |Both | | 226 | |Index Test |index_field |varchar(255) | | |True | | | 227 | 228 | Options 229 | ``` 230 | default_permissions : ('add', 'change', 'delete') 231 | ``` 232 | 233 | 234 | ## Genre(tests.models.Genre) 235 | 236 | ``` 237 | Genre 238 | 239 | * Choice has genre 240 | 241 | ``` 242 | 243 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 244 | |---|---|---|---|---|---|---|---| 245 | |ID |id |integer |True |True | |Blank | | 246 | |Genre name |name |varchar(255) | | | | | | 247 | 248 | Options 249 | ``` 250 | default_permissions : ('add', 'change', 'delete') 251 | ``` 252 | 253 | 254 | ## choice-genre relationship(tests.models.Choice_genres) 255 | 256 | ``` 257 | Choice_genres(id, choice, genre) 258 | ``` 259 | 260 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 261 | |---|---|---|---|---|---|---|---| 262 | |ID |id |integer |True |True | |Blank | | 263 | |choice |choice |integer | | |True | |FK:tests.models.Choice | 264 | |genre |genre |integer | | |True | |FK:tests.models.Genre | 265 | 266 | Options 267 | ``` 268 | unique_together : (('choice', 'genre'),) 269 | default_permissions : ('add', 'change', 'delete') 270 | ``` 271 | 272 | 273 | ## Choice(tests.models.Choice) 274 | 275 | ``` 276 | Choice 277 | 278 | * Choice has poll reference 279 | * Choice has choices field 280 | 281 | ``` 282 | 283 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 284 | |---|---|---|---|---|---|---|---| 285 | |ID |id |integer |True |True | |Blank | | 286 | |Poll |poll |integer | | |True | |FK:tests.models.Poll | 287 | |Choice |choice |smallint | | | | |1:test1, 2:test2, 3:test3 | 288 | |Genre |genres | | | | | |M2M:tests.models.Genre (through: tests.models.Choice_genres) | 289 | 290 | Options 291 | ``` 292 | default_permissions : ('add', 'change', 'delete') 293 | ``` 294 | 295 | 296 | ## Vote(tests.models.Vote) 297 | 298 | ``` 299 | Vote 300 | 301 | * Vote has user reference 302 | * Vote has poll reference 303 | * Vote has choice reference 304 | 305 | ``` 306 | 307 | |Fullname|Name|Type|PK|Unique|Index|Null/Blank|Comment| 308 | |---|---|---|---|---|---|---|---| 309 | |ID |id |integer |True |True | |Blank | | 310 | |Voted User |user |integer | | |True | |FK:django.contrib.auth.models.User | 311 | |Voted Poll |poll |integer | | |True | |FK:tests.models.Poll | 312 | |Voted Choice |choice |integer | | |True | |FK:tests.models.Choice | 313 | 314 | Options 315 | ``` 316 | unique_together : (('user', 'poll'),) 317 | default_permissions : ('add', 'change', 'delete') 318 | ``` 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /tests/sample_models.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .. contents:: 5 | :local: 6 | 7 | 8 | permission(django.contrib.auth.models.Permission) 9 | ------------------------------------------------- 10 | 11 | :: 12 | 13 | 14 | The permissions system provides a way to assign permissions to specific 15 | users and groups of users. 16 | 17 | The permission system is used by the Django admin site, but may also be 18 | useful in your own code. The Django admin site uses permissions as follows: 19 | 20 | - The "add" permission limits the user's ability to view the "add" form 21 | and add an object. 22 | - The "change" permission limits a user's ability to view the change 23 | list, view the "change" form and change an object. 24 | - The "delete" permission limits the ability to delete an object. 25 | 26 | Permissions are set globally per type of object, not per specific object 27 | instance. It is possible to say "Mary may change news stories," but it's 28 | not currently possible to say "Mary may change news stories, but only the 29 | ones she created herself" or "Mary may only change news stories that have a 30 | certain status or publication date." 31 | 32 | Three basic permissions -- add, change and delete -- are automatically 33 | created for each Django model. 34 | 35 | 36 | .. list-table:: 37 | :header-rows: 1 38 | 39 | * - Fullname 40 | - Name 41 | - Type 42 | - PK 43 | - Unique 44 | - Index 45 | - Null/Blank 46 | - Comment 47 | * - ID 48 | - id 49 | - integer 50 | - True 51 | - True 52 | - 53 | - Blank 54 | - 55 | * - name 56 | - name 57 | - varchar(255) 58 | - 59 | - 60 | - 61 | - 62 | - 63 | * - content type 64 | - content_type 65 | - integer 66 | - 67 | - 68 | - True 69 | - 70 | - FK:django.contrib.contenttypes.models.ContentType 71 | * - codename 72 | - codename 73 | - varchar(100) 74 | - 75 | - 76 | - 77 | - 78 | - 79 | 80 | 81 | Options:: 82 | 83 | unique_together : (('content_type', 'codename'),) 84 | ordering : ('content_type__app_label', 'content_type__model', 'codename') 85 | default_permissions : ('add', 'change', 'delete') 86 | 87 | 88 | group-permission relationship(django.contrib.auth.models.Group_permissions) 89 | --------------------------------------------------------------------------- 90 | 91 | :: 92 | 93 | Group_permissions(id, group, permission) 94 | 95 | .. list-table:: 96 | :header-rows: 1 97 | 98 | * - Fullname 99 | - Name 100 | - Type 101 | - PK 102 | - Unique 103 | - Index 104 | - Null/Blank 105 | - Comment 106 | * - ID 107 | - id 108 | - integer 109 | - True 110 | - True 111 | - 112 | - Blank 113 | - 114 | * - group 115 | - group 116 | - integer 117 | - 118 | - 119 | - True 120 | - 121 | - FK:django.contrib.auth.models.Group 122 | * - permission 123 | - permission 124 | - integer 125 | - 126 | - 127 | - True 128 | - 129 | - FK:django.contrib.auth.models.Permission 130 | 131 | 132 | Options:: 133 | 134 | unique_together : (('group', 'permission'),) 135 | default_permissions : ('add', 'change', 'delete') 136 | 137 | 138 | group(django.contrib.auth.models.Group) 139 | --------------------------------------- 140 | 141 | :: 142 | 143 | 144 | Groups are a generic way of categorizing users to apply permissions, or 145 | some other label, to those users. A user can belong to any number of 146 | groups. 147 | 148 | A user in a group automatically has all the permissions granted to that 149 | group. For example, if the group 'Site editors' has the permission 150 | can_edit_home_page, any user in that group will have that permission. 151 | 152 | Beyond permissions, groups are a convenient way to categorize users to 153 | apply some label, or extended functionality, to them. For example, you 154 | could create a group 'Special users', and you could write code that would 155 | do special things to those users -- such as giving them access to a 156 | members-only portion of your site, or sending them members-only email 157 | messages. 158 | 159 | 160 | .. list-table:: 161 | :header-rows: 1 162 | 163 | * - Fullname 164 | - Name 165 | - Type 166 | - PK 167 | - Unique 168 | - Index 169 | - Null/Blank 170 | - Comment 171 | * - ID 172 | - id 173 | - integer 174 | - True 175 | - True 176 | - 177 | - Blank 178 | - 179 | * - name 180 | - name 181 | - varchar(80) 182 | - 183 | - True 184 | - 185 | - 186 | - 187 | * - permissions 188 | - permissions 189 | - 190 | - 191 | - 192 | - 193 | - Blank 194 | - M2M:django.contrib.auth.models.Permission (through: django.contrib.auth.models.Group_permissions) 195 | 196 | 197 | Options:: 198 | 199 | default_permissions : ('add', 'change', 'delete') 200 | 201 | 202 | user-group relationship(django.contrib.auth.models.User_groups) 203 | --------------------------------------------------------------- 204 | 205 | :: 206 | 207 | User_groups(id, user, group) 208 | 209 | .. list-table:: 210 | :header-rows: 1 211 | 212 | * - Fullname 213 | - Name 214 | - Type 215 | - PK 216 | - Unique 217 | - Index 218 | - Null/Blank 219 | - Comment 220 | * - ID 221 | - id 222 | - integer 223 | - True 224 | - True 225 | - 226 | - Blank 227 | - 228 | * - user 229 | - user 230 | - integer 231 | - 232 | - 233 | - True 234 | - 235 | - FK:django.contrib.auth.models.User 236 | * - group 237 | - group 238 | - integer 239 | - 240 | - 241 | - True 242 | - 243 | - FK:django.contrib.auth.models.Group 244 | 245 | 246 | Options:: 247 | 248 | unique_together : (('user', 'group'),) 249 | default_permissions : ('add', 'change', 'delete') 250 | 251 | 252 | user-permission relationship(django.contrib.auth.models.User_user_permissions) 253 | ------------------------------------------------------------------------------ 254 | 255 | :: 256 | 257 | User_user_permissions(id, user, permission) 258 | 259 | .. list-table:: 260 | :header-rows: 1 261 | 262 | * - Fullname 263 | - Name 264 | - Type 265 | - PK 266 | - Unique 267 | - Index 268 | - Null/Blank 269 | - Comment 270 | * - ID 271 | - id 272 | - integer 273 | - True 274 | - True 275 | - 276 | - Blank 277 | - 278 | * - user 279 | - user 280 | - integer 281 | - 282 | - 283 | - True 284 | - 285 | - FK:django.contrib.auth.models.User 286 | * - permission 287 | - permission 288 | - integer 289 | - 290 | - 291 | - True 292 | - 293 | - FK:django.contrib.auth.models.Permission 294 | 295 | 296 | Options:: 297 | 298 | unique_together : (('user', 'permission'),) 299 | default_permissions : ('add', 'change', 'delete') 300 | 301 | 302 | user(django.contrib.auth.models.User) 303 | ------------------------------------- 304 | 305 | :: 306 | 307 | 308 | Users within the Django authentication system are represented by this 309 | model. 310 | 311 | Username, password and email are required. Other fields are optional. 312 | 313 | 314 | .. list-table:: 315 | :header-rows: 1 316 | 317 | * - Fullname 318 | - Name 319 | - Type 320 | - PK 321 | - Unique 322 | - Index 323 | - Null/Blank 324 | - Comment 325 | * - ID 326 | - id 327 | - integer 328 | - True 329 | - True 330 | - 331 | - Blank 332 | - 333 | * - password 334 | - password 335 | - varchar(128) 336 | - 337 | - 338 | - 339 | - 340 | - 341 | * - last login 342 | - last_login 343 | - datetime 344 | - 345 | - 346 | - 347 | - Both 348 | - 349 | * - superuser status 350 | - is_superuser 351 | - bool 352 | - 353 | - 354 | - 355 | - Blank 356 | - 357 | * - username 358 | - username 359 | - varchar(150) 360 | - 361 | - True 362 | - 363 | - 364 | - 365 | * - first name 366 | - first_name 367 | - varchar(30) 368 | - 369 | - 370 | - 371 | - Blank 372 | - 373 | * - last name 374 | - last_name 375 | - varchar(150) 376 | - 377 | - 378 | - 379 | - Blank 380 | - 381 | * - email address 382 | - email 383 | - varchar(254) 384 | - 385 | - 386 | - 387 | - Blank 388 | - 389 | * - staff status 390 | - is_staff 391 | - bool 392 | - 393 | - 394 | - 395 | - Blank 396 | - 397 | * - active 398 | - is_active 399 | - bool 400 | - 401 | - 402 | - 403 | - Blank 404 | - 405 | * - date joined 406 | - date_joined 407 | - datetime 408 | - 409 | - 410 | - 411 | - 412 | - 413 | * - groups 414 | - groups 415 | - 416 | - 417 | - 418 | - 419 | - Blank 420 | - M2M:django.contrib.auth.models.Group (through: django.contrib.auth.models.User_groups) 421 | * - user permissions 422 | - user_permissions 423 | - 424 | - 425 | - 426 | - 427 | - Blank 428 | - M2M:django.contrib.auth.models.Permission (through: django.contrib.auth.models.User_user_permissions) 429 | 430 | 431 | Options:: 432 | 433 | swappable : AUTH_USER_MODEL 434 | default_permissions : ('add', 'change', 'delete') 435 | 436 | 437 | content type(django.contrib.contenttypes.models.ContentType) 438 | ------------------------------------------------------------ 439 | 440 | :: 441 | 442 | ContentType(id, app_label, model) 443 | 444 | .. list-table:: 445 | :header-rows: 1 446 | 447 | * - Fullname 448 | - Name 449 | - Type 450 | - PK 451 | - Unique 452 | - Index 453 | - Null/Blank 454 | - Comment 455 | * - ID 456 | - id 457 | - integer 458 | - True 459 | - True 460 | - 461 | - Blank 462 | - 463 | * - app label 464 | - app_label 465 | - varchar(100) 466 | - 467 | - 468 | - 469 | - 470 | - 471 | * - python model class name 472 | - model 473 | - varchar(100) 474 | - 475 | - 476 | - 477 | - 478 | - 479 | 480 | 481 | Options:: 482 | 483 | unique_together : (('app_label', 'model'),) 484 | default_permissions : ('add', 'change', 'delete') 485 | 486 | 487 | site(django.contrib.sites.models.Site) 488 | -------------------------------------- 489 | 490 | :: 491 | 492 | Site(id, domain, name) 493 | 494 | .. list-table:: 495 | :header-rows: 1 496 | 497 | * - Fullname 498 | - Name 499 | - Type 500 | - PK 501 | - Unique 502 | - Index 503 | - Null/Blank 504 | - Comment 505 | * - ID 506 | - id 507 | - integer 508 | - True 509 | - True 510 | - 511 | - Blank 512 | - 513 | * - domain name 514 | - domain 515 | - varchar(100) 516 | - 517 | - True 518 | - 519 | - 520 | - 521 | * - display name 522 | - name 523 | - varchar(50) 524 | - 525 | - 526 | - 527 | - 528 | - 529 | 530 | 531 | Options:: 532 | 533 | ordering : ('domain',) 534 | default_permissions : ('add', 'change', 'delete') 535 | 536 | 537 | Poll(tests.models.Poll) 538 | ----------------------- 539 | 540 | :: 541 | 542 | Poll 543 | 544 | * Poll has question and description fields 545 | 546 | 547 | .. list-table:: 548 | :header-rows: 1 549 | 550 | * - Fullname 551 | - Name 552 | - Type 553 | - PK 554 | - Unique 555 | - Index 556 | - Null/Blank 557 | - Comment 558 | * - ID 559 | - id 560 | - integer 561 | - True 562 | - True 563 | - 564 | - Blank 565 | - 566 | * - Question Name 567 | - question 568 | - varchar(255) 569 | - 570 | - 571 | - 572 | - 573 | - 574 | * - Description 575 | - description 576 | - text 577 | - 578 | - 579 | - 580 | - Blank 581 | - Description field allows Blank 582 | * - Null Test 583 | - null_field 584 | - varchar(255) 585 | - 586 | - 587 | - 588 | - Null 589 | - 590 | * - Blank Test 591 | - blank_field 592 | - varchar(255) 593 | - 594 | - 595 | - 596 | - Blank 597 | - 598 | * - Both Test 599 | - both_field 600 | - varchar(255) 601 | - 602 | - 603 | - 604 | - Both 605 | - 606 | * - Index Test 607 | - index_field 608 | - varchar(255) 609 | - 610 | - 611 | - True 612 | - 613 | - 614 | 615 | 616 | Options:: 617 | 618 | default_permissions : ('add', 'change', 'delete') 619 | 620 | 621 | Genre(tests.models.Genre) 622 | ------------------------- 623 | 624 | :: 625 | 626 | Genre 627 | 628 | * Choice has genre 629 | 630 | 631 | .. list-table:: 632 | :header-rows: 1 633 | 634 | * - Fullname 635 | - Name 636 | - Type 637 | - PK 638 | - Unique 639 | - Index 640 | - Null/Blank 641 | - Comment 642 | * - ID 643 | - id 644 | - integer 645 | - True 646 | - True 647 | - 648 | - Blank 649 | - 650 | * - Genre name 651 | - name 652 | - varchar(255) 653 | - 654 | - 655 | - 656 | - 657 | - 658 | 659 | 660 | Options:: 661 | 662 | default_permissions : ('add', 'change', 'delete') 663 | 664 | 665 | choice-genre relationship(tests.models.Choice_genres) 666 | ----------------------------------------------------- 667 | 668 | :: 669 | 670 | Choice_genres(id, choice, genre) 671 | 672 | .. list-table:: 673 | :header-rows: 1 674 | 675 | * - Fullname 676 | - Name 677 | - Type 678 | - PK 679 | - Unique 680 | - Index 681 | - Null/Blank 682 | - Comment 683 | * - ID 684 | - id 685 | - integer 686 | - True 687 | - True 688 | - 689 | - Blank 690 | - 691 | * - choice 692 | - choice 693 | - integer 694 | - 695 | - 696 | - True 697 | - 698 | - FK:tests.models.Choice 699 | * - genre 700 | - genre 701 | - integer 702 | - 703 | - 704 | - True 705 | - 706 | - FK:tests.models.Genre 707 | 708 | 709 | Options:: 710 | 711 | unique_together : (('choice', 'genre'),) 712 | default_permissions : ('add', 'change', 'delete') 713 | 714 | 715 | Choice(tests.models.Choice) 716 | --------------------------- 717 | 718 | :: 719 | 720 | Choice 721 | 722 | * Choice has poll reference 723 | * Choice has choices field 724 | 725 | 726 | .. list-table:: 727 | :header-rows: 1 728 | 729 | * - Fullname 730 | - Name 731 | - Type 732 | - PK 733 | - Unique 734 | - Index 735 | - Null/Blank 736 | - Comment 737 | * - ID 738 | - id 739 | - integer 740 | - True 741 | - True 742 | - 743 | - Blank 744 | - 745 | * - Poll 746 | - poll 747 | - integer 748 | - 749 | - 750 | - True 751 | - 752 | - FK:tests.models.Poll 753 | * - Choice 754 | - choice 755 | - smallint 756 | - 757 | - 758 | - 759 | - 760 | - 1:test1, 2:test2, 3:test3 761 | * - Genre 762 | - genres 763 | - 764 | - 765 | - 766 | - 767 | - 768 | - M2M:tests.models.Genre (through: tests.models.Choice_genres) 769 | 770 | 771 | Options:: 772 | 773 | default_permissions : ('add', 'change', 'delete') 774 | 775 | 776 | Vote(tests.models.Vote) 777 | ----------------------- 778 | 779 | :: 780 | 781 | Vote 782 | 783 | * Vote has user reference 784 | * Vote has poll reference 785 | * Vote has choice reference 786 | 787 | 788 | .. list-table:: 789 | :header-rows: 1 790 | 791 | * - Fullname 792 | - Name 793 | - Type 794 | - PK 795 | - Unique 796 | - Index 797 | - Null/Blank 798 | - Comment 799 | * - ID 800 | - id 801 | - integer 802 | - True 803 | - True 804 | - 805 | - Blank 806 | - 807 | * - Voted User 808 | - user 809 | - integer 810 | - 811 | - 812 | - True 813 | - 814 | - FK:django.contrib.auth.models.User 815 | * - Voted Poll 816 | - poll 817 | - integer 818 | - 819 | - 820 | - True 821 | - 822 | - FK:tests.models.Poll 823 | * - Voted Choice 824 | - choice 825 | - integer 826 | - 827 | - 828 | - True 829 | - 830 | - FK:tests.models.Choice 831 | 832 | 833 | Options:: 834 | 835 | unique_together : (('user', 'poll'),) 836 | default_permissions : ('add', 'change', 'delete') 837 | 838 | 839 | 840 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from django import VERSION 2 | 3 | DEBUG = False 4 | 5 | if VERSION < (1, 10): 6 | TEMPLATE_DEBUG = False 7 | else: 8 | TEMPLATES = [ 9 | { 10 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 11 | 'DIRS': [], 12 | 'APP_DIRS': True, 13 | 'OPTIONS': { 14 | 'context_processors': [ 15 | 'django.template.context_processors.debug', 16 | 'django.template.context_processors.request', 17 | 'django.contrib.auth.context_processors.auth', 18 | 'django.contrib.messages.context_processors.messages', 19 | ], 20 | }, 21 | }, 22 | ] 23 | 24 | 25 | # for django 1.5 26 | if VERSION < (1, 6): 27 | ROOT_URLCONF = 'urls' 28 | 29 | SITE_ID = 1 30 | 31 | INSTALLED_APPS = ( 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.sites', 35 | 36 | 'modelsdoc', 37 | 'tests', 38 | ) 39 | DATABASES = { 40 | 'default': { 41 | 'ENGINE': 'django.db.backends.sqlite3', 42 | 'NAME': ':memory:', 43 | } 44 | } 45 | CACHES = { 46 | 'default': { 47 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 48 | } 49 | } 50 | 51 | if VERSION >= (1, 6): 52 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 53 | else: 54 | TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' 55 | 56 | SECRET_KEY = 'secret_key_for_testing' 57 | MIDDLEWARE_CLASSES = [] 58 | 59 | PASSWORD_HASHERS = ( 60 | 'django.contrib.auth.hashers.MD5PasswordHasher', 61 | ) 62 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.tests.test_utils import * # NOQA 2 | from tests.tests.test_commands import * # NOQA 3 | from tests.tests.test_wrappers import * # NOQA 4 | from tests.tests.test_templatetags import * # NOQA 5 | -------------------------------------------------------------------------------- /tests/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | tests.test_commands.py 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | from django.test import TestCase 12 | from testfixtures import OutputCapture 13 | 14 | 15 | class TestModels2rst(TestCase): 16 | 17 | def _callCommand(self, *args, **kwargs): 18 | from django.core.management import call_command 19 | return call_command('listing_models', **kwargs) 20 | 21 | def test_all_models(self): 22 | with OutputCapture() as o: 23 | self._callCommand() 24 | 25 | self.assertTrue('Poll(tests.models.Poll)' in o.captured) 26 | self.assertTrue('Choice(tests.models.Choice)' in o.captured) 27 | self.assertTrue('Vote(tests.models.Vote)' in o.captured) 28 | self.assertTrue('* - Question Name' in o.captured) 29 | self.assertTrue('- varchar(255)' in o.captured) 30 | self.assertTrue("unique_together : (('user', 'poll'),)" in o.captured) 31 | self.assertTrue('Description field allows Blank' in o.captured) 32 | 33 | def test_option_app(self): 34 | with OutputCapture() as o: 35 | self._callCommand(app='tests') 36 | 37 | self.assertTrue('Poll(tests.models.Poll)' in o.captured) 38 | self.assertTrue('Choice(tests.models.Choice)' in o.captured) 39 | self.assertTrue('Vote(tests.models.Vote)' in o.captured) 40 | self.assertTrue('* - Question Name' in o.captured) 41 | self.assertTrue('- varchar(255)' in o.captured) 42 | self.assertTrue("unique_together : (('user', 'poll'),)" in o.captured) 43 | self.assertTrue('Description field allows Blank' in o.captured) 44 | 45 | with OutputCapture() as o: 46 | self._callCommand(app='missing_app') 47 | 48 | self.assertTrue('Cannot find models' in o.captured) 49 | 50 | def test_option_output(self): 51 | import tempfile 52 | temp = tempfile.NamedTemporaryFile() 53 | 54 | with OutputCapture() as o: 55 | self._callCommand(output_file=temp.name) 56 | 57 | self.assertTrue('Complete! Create the output file.' in o.captured) 58 | 59 | with open(temp.name) as fp: 60 | body = fp.read() 61 | 62 | self.assertTrue('Poll(tests.models.Poll)' in body) 63 | self.assertTrue('Choice(tests.models.Choice)' in body) 64 | self.assertTrue('Vote(tests.models.Vote)' in body) 65 | self.assertTrue('* - Question Name' in body) 66 | self.assertTrue('- varchar(255)' in body) 67 | self.assertTrue("unique_together : (('user', 'poll'),)" in body) 68 | self.assertTrue('Description field allows Blank' in body) 69 | 70 | def test_option_format(self): 71 | 72 | with OutputCapture() as o: 73 | self._callCommand(output_format='rst') 74 | 75 | self.assertTrue('Poll(tests.models.Poll)' in o.captured) 76 | self.assertTrue('Choice(tests.models.Choice)' in o.captured) 77 | self.assertTrue('Vote(tests.models.Vote)' in o.captured) 78 | self.assertTrue('* - Question Name' in o.captured) 79 | self.assertTrue('- varchar(255)' in o.captured) 80 | self.assertTrue("unique_together : (('user', 'poll'),)" in o.captured) 81 | self.assertTrue('Description field allows Blank' in o.captured) 82 | 83 | with OutputCapture() as o: 84 | self._callCommand(output_format='md') 85 | 86 | self.assertTrue('## Poll(tests.models.Poll)' in o.captured) 87 | self.assertTrue('## Choice(tests.models.Choice)' in o.captured) 88 | self.assertTrue('## Vote(tests.models.Vote)' in o.captured) 89 | self.assertTrue("unique_together : (('user', 'poll'),)" in o.captured) 90 | self.assertTrue('Description field allows Blank' in o.captured) 91 | 92 | self.assertTrue( 93 | '|Question Name |question |varchar(255) | | | | | |' in o.captured 94 | ) 95 | 96 | with OutputCapture() as o: 97 | self._callCommand(output_format='unknown') 98 | 99 | self.assertTrue('Cannot find the output template file' in o.captured) 100 | -------------------------------------------------------------------------------- /tests/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | #! -*- coding: utf-8 -*- 2 | """ 3 | tests.test_templatetags 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | from django.template import Template, Context 12 | from django.test import TestCase 13 | 14 | 15 | class TestEmptylineless(TestCase): 16 | 17 | def _getTargetTags(self): 18 | return """ 19 | {% load modelsdoc_tags %} 20 | {% emptylineless %} 21 | test1 22 | 23 | test2 24 | 25 | test3 26 | {% endemptylineless %} 27 | """ 28 | 29 | def _callFUT(self): 30 | t = Template(self._getTargetTags()) 31 | return t.render(Context()) 32 | 33 | def test_remove_emptyline(self): 34 | self.assertEqual('\n\ntest1\ntest2\ntest3\n', self._callFUT()) 35 | 36 | 37 | class TestGetAttr(TestCase): 38 | 39 | def _getTargetTags(self): 40 | return '{% load modelsdoc_tags %}{{ obj|get_attr:attr_name }}' 41 | 42 | def _callFUT(self, obj, attr_name): 43 | t = Template(self._getTargetTags()) 44 | return t.render(Context({'obj': obj, 'attr_name': attr_name})) 45 | 46 | def test_get_attr(self): 47 | 48 | class Dummy(object): 49 | 50 | test = 'test' 51 | 52 | self.assertEqual('test', self._callFUT(Dummy(), 'test')) 53 | self.assertEqual('', self._callFUT(Dummy(), 'non_exists_attr')) 54 | 55 | 56 | class TestStrRepeat(TestCase): 57 | 58 | def _getTargetTags(self): 59 | return '{% load modelsdoc_tags %}{{ num|str_repeat:"-" }}' 60 | 61 | def _callFUT(self, num): 62 | t = Template(self._getTargetTags()) 63 | return t.render(Context({'num': num})) 64 | 65 | def test_repeat(self): 66 | self.assertEqual('-----', self._callFUT(5)) 67 | 68 | def test_repeat_zero_length(self): 69 | self.assertEqual('', self._callFUT(0)) 70 | -------------------------------------------------------------------------------- /tests/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | tests.test_utils 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | from django.test import TestCase 12 | import unittest 13 | 14 | 15 | class TestGetModelAttr(unittest.TestCase): 16 | 17 | def _callFUT(self, option_model, django_version): 18 | from modelsdoc.utils import get_model_attr 19 | return get_model_attr(option_model, django_version) 20 | 21 | def _getDummyMetaOption(self): 22 | class DummyMetaOption(object): 23 | 24 | concrete_model = 'concrete_model' 25 | model = 'model' 26 | return DummyMetaOption() 27 | 28 | def test_django_ver16_over(self): 29 | self.assertEqual( 30 | 'model', 31 | self._callFUT(self._getDummyMetaOption(), (1, 6)) 32 | ) 33 | 34 | def test_django_ver15(self): 35 | self.assertEqual( 36 | 'concrete_model', 37 | self._callFUT(self._getDummyMetaOption(), (1, 5)) 38 | ) 39 | 40 | 41 | class TestGetFieldsAttr(unittest.TestCase): 42 | 43 | def _getDummyMetaOption(self): 44 | class DummyMetaOption(object): 45 | 46 | concrete_fields = ('concrete_fields',) 47 | fields = ('fields',) 48 | many_to_many = ('many2many',) 49 | 50 | return DummyMetaOption() 51 | 52 | def _callFUT(self, option_model, django_version): 53 | from modelsdoc.utils import get_fields_attr 54 | return get_fields_attr(option_model, django_version) 55 | 56 | def test_django_ver16_over(self): 57 | self.assertEqual( 58 | ['concrete_fields', 'many2many'], 59 | self._callFUT(self._getDummyMetaOption(), (1, 6)) 60 | ) 61 | 62 | def test_django_ver15(self): 63 | self.assertEqual( 64 | ['fields', 'many2many'], 65 | self._callFUT(self._getDummyMetaOption(), (1, 5)) 66 | ) 67 | 68 | 69 | class TestGetParentModelAttr(unittest.TestCase): 70 | 71 | def _callFUT(self, related_field, django_version): 72 | from modelsdoc.utils import get_parent_model_attr 73 | return get_parent_model_attr(related_field, django_version) 74 | 75 | def _getDummyMetaOption(self): 76 | class DummyMetaOption(object): 77 | 78 | parent_model = 'parent_model' 79 | model = 'model' 80 | 81 | return DummyMetaOption() 82 | 83 | def test_django_ver17_lower(self): 84 | self.assertEqual( 85 | 'parent_model', 86 | self._callFUT(self._getDummyMetaOption(), (1, 7)) 87 | ) 88 | 89 | def test_django_ver18(self): 90 | self.assertEqual( 91 | 'model', 92 | self._callFUT(self._getDummyMetaOption(), (1, 8)) 93 | ) 94 | 95 | 96 | class TestClassToString(unittest.TestCase): 97 | 98 | def _callFUT(self, model): 99 | from modelsdoc.utils import class_to_string 100 | return class_to_string(model) 101 | 102 | def _getDummyModel(self): 103 | class DummyModel(object): 104 | __name__ = 'DummyModel' 105 | __module__ = 'test_mod' 106 | return DummyModel() 107 | 108 | def test_to_string(self): 109 | self.assertEqual( 110 | 'test_mod.DummyModel', 111 | self._callFUT(self._getDummyModel()) 112 | ) 113 | 114 | 115 | class TestGetNullBlank(TestCase): 116 | 117 | def _callFUT(self, field): 118 | from modelsdoc.utils import get_null_blank 119 | return get_null_blank(field) 120 | 121 | def _getDummyField(self, null, blank): 122 | class DummyField(object): 123 | 124 | def __init__(self, null, blank): 125 | self.null = null 126 | self.blank = blank 127 | return DummyField(null, blank) 128 | 129 | def test_not_allow_null_and_blank(self): 130 | self.assertEqual( 131 | '', 132 | self._callFUT(self._getDummyField(null=False, blank=False)) 133 | ) 134 | 135 | def test_allow_blank_only(self): 136 | self.assertEqual( 137 | 'Blank', 138 | self._callFUT(self._getDummyField(null=False, blank=True)) 139 | ) 140 | 141 | def test_allow_null_only(self): 142 | self.assertEqual( 143 | 'Null', 144 | self._callFUT(self._getDummyField(null=True, blank=False)) 145 | ) 146 | 147 | def test_allow_both(self): 148 | self.assertEqual( 149 | 'Both', 150 | self._callFUT(self._getDummyField(null=True, blank=True)) 151 | ) 152 | 153 | 154 | class TestGetForeignkey(TestCase): 155 | 156 | def _callFUT(self, field): 157 | from modelsdoc.utils import get_foreignkey 158 | return get_foreignkey(field) 159 | 160 | def _getTargetField(self, field_name): 161 | from tests.models import Choice 162 | return Choice._meta.get_field(field_name) 163 | 164 | def test_is_foreignkey(self): 165 | self.assertEqual( 166 | 'FK:tests.models.Poll', 167 | self._callFUT(self._getTargetField('poll')) 168 | ) 169 | 170 | def test_not_foreignkey(self): 171 | self.assertEqual( 172 | '', 173 | self._callFUT(self._getTargetField('choice')) 174 | ) 175 | 176 | def test_many_to_many_field(self): 177 | self.assertEqual( 178 | 'M2M:tests.models.Genre (through: tests.models.Choice_genres)', 179 | self._callFUT(self._getTargetField('genres')) 180 | ) 181 | 182 | 183 | class TestGetChoices(TestCase): 184 | 185 | def _callFUT(self, field): 186 | from modelsdoc.utils import get_choices 187 | return get_choices(field) 188 | 189 | def _getTargetField(self, field_name): 190 | from tests.models import Choice 191 | for f in Choice._meta.fields: 192 | if f.name != field_name: 193 | continue 194 | return f 195 | 196 | def test_is_choices(self): 197 | self.assertEqual( 198 | '1:test1, 2:test2, 3:test3', 199 | self._callFUT(self._getTargetField('choice')) 200 | ) 201 | 202 | def test_not_choices(self): 203 | self.assertEqual( 204 | '', 205 | self._callFUT(self._getTargetField('poll')) 206 | ) 207 | 208 | 209 | class TestImportClass(unittest.TestCase): 210 | 211 | def _callFUT(self, cl): 212 | from modelsdoc.utils import import_class 213 | return import_class(cl) 214 | 215 | def test_import_ok(self): 216 | self.assertEqual( 217 | "", 218 | str(self._callFUT('modelsdoc.wrappers.ModelWrapper')) 219 | ) 220 | 221 | def test_raise_attribute_error(self): 222 | with self.assertRaises(AttributeError): 223 | self._callFUT('modelsdoc.wrappers.NonExistsWrapper') 224 | 225 | def test_raise_import_error(self): 226 | with self.assertRaises(ImportError): 227 | self._callFUT('modelsdoc.nonexists.Hoge') 228 | 229 | 230 | class TestGetRelatedField(unittest.TestCase): 231 | 232 | def _callFUT(self, field, django_version): 233 | from modelsdoc.utils import get_related_field 234 | return get_related_field(field, django_version) 235 | 236 | def _getDummyField(self): 237 | class DummyField(object): 238 | 239 | related = 'related' 240 | remote_field = 'remote_field' 241 | 242 | return DummyField() 243 | 244 | def test_django_ver18_lower(self): 245 | self.assertEqual( 246 | 'related', 247 | self._callFUT(self._getDummyField(), (1, 8)) 248 | ) 249 | 250 | def test_django_ver19(self): 251 | self.assertEqual( 252 | 'remote_field', 253 | self._callFUT(self._getDummyField(), (1, 9)) 254 | ) 255 | 256 | 257 | class TestGetThrough(unittest.TestCase): 258 | 259 | def _callFUT(self, field, django_version): 260 | from modelsdoc.utils import get_through 261 | return get_through(field, django_version) 262 | 263 | def _getDummyField(self): 264 | 265 | class DummyRel(object): 266 | 267 | def __init__(self, through): 268 | self.through = through 269 | 270 | class DummyField(object): 271 | 272 | rel = DummyRel('rel') 273 | remote_field = DummyRel('remote_field') 274 | 275 | return DummyField() 276 | 277 | def test_django_ver18_lower(self): 278 | self.assertEqual( 279 | 'rel', 280 | self._callFUT(self._getDummyField(), (1, 8)) 281 | ) 282 | 283 | def test_django_ver19(self): 284 | self.assertEqual( 285 | 'remote_field', 286 | self._callFUT(self._getDummyField(), (1, 9)) 287 | ) 288 | -------------------------------------------------------------------------------- /tests/tests/test_wrappers.py: -------------------------------------------------------------------------------- 1 | #! -*- coding:utf-8 -*- 2 | """ 3 | tests.test_wrappers 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :author: tell-k 7 | :copyright: tell-k. All Rights Reserved. 8 | """ 9 | from __future__ import division, print_function, absolute_import, unicode_literals # NOQA 10 | 11 | import mock 12 | 13 | from django.test import TestCase 14 | 15 | 16 | class TestFieldWrapper(TestCase): 17 | 18 | def _getTargetClass(self): 19 | from modelsdoc.wrappers import FieldWrapper 20 | return FieldWrapper 21 | 22 | def _makeOne(self, *args, **kwargs): 23 | return self._getTargetClass()(*args, **kwargs) 24 | 25 | def _getDummyModel(self, model=None): 26 | class DummyModel(object): 27 | 28 | __name__ = 'DummyModel' 29 | 30 | def __init__(self, model): 31 | self._model = model 32 | return DummyModel(model) 33 | 34 | def _getDummyField(self): 35 | class DummyField(object): 36 | 37 | name = 'test_field' 38 | proxy_attr = 'test' 39 | 40 | def db_type(self, con): 41 | return 'test' if con else None 42 | 43 | return DummyField() 44 | 45 | @mock.patch('modelsdoc.wrappers.get_null_blank', 46 | return_value='dummy_field') 47 | def test_null_blank(self, mock): 48 | target = self._makeOne('dummy_field', 'dummy', 'dummy', 'dummy') 49 | self.assertEqual('dummy_field', target.null_blank) 50 | mock.assert_called_with('dummy_field') 51 | 52 | def test_comment(self): 53 | target = self._makeOne( 54 | self._getDummyField(), 55 | self._getDummyModel(model=self._getDummyModel()), 56 | 'connection', 57 | {('DummyModel', 'test_field'): ['test1', 'test2']} 58 | ) 59 | self.assertEqual('test1 test2', target.comment) 60 | 61 | def test_db_type(self): 62 | target = self._makeOne(self._getDummyField(), 63 | 'dummy', 'connection', 'dummy') 64 | self.assertEqual('test', target.db_type) 65 | 66 | target = self._makeOne(self._getDummyField(), 'dummy', '', 'dummy') 67 | self.assertEqual('', target.db_type) 68 | 69 | def test_getattr(self): 70 | target = self._makeOne(self._getDummyField(), 71 | 'dummy', 'connection', 'dummy') 72 | self.assertEqual('test', target.proxy_attr) 73 | self.assertEqual('', target.non_exists_attr) 74 | 75 | 76 | class TestModelWrapper(TestCase): 77 | 78 | def _getTargetClass(self): 79 | from modelsdoc.wrappers import ModelWrapper 80 | return ModelWrapper 81 | 82 | def _makeOne(self, *args, **kwargs): 83 | return self._getTargetClass()(*args, **kwargs) 84 | 85 | def _getDummyMeta(self, verbose_name='name'): 86 | vname = verbose_name 87 | 88 | class DummyMeta(object): 89 | verbose_name = vname 90 | model = 'TestModel' 91 | concrete_model = 'TestModel' 92 | concrete_fields = ['field1', 'field2', 'field3'] 93 | fields = concrete_fields 94 | 95 | return DummyMeta() 96 | 97 | def _getDummyModel(self, model=None, meta=None): 98 | class DummyModel(object): 99 | """ TEST DOC STRING """ 100 | 101 | __module__ = 'dummy_module' 102 | __name__ = 'DummyModel' 103 | 104 | def __init__(self, model=None, meta=None): 105 | self._model = model 106 | self._meta = meta 107 | self.proxy_attr = 'test' 108 | 109 | if meta is None: 110 | meta = self._getDummyMeta() 111 | return DummyModel(model, meta) 112 | 113 | def _getDummyAnalyizer(self): 114 | class DummyAnalyizer(object): 115 | 116 | def find_attr_docs(self): 117 | return 'dummy' 118 | return DummyAnalyizer() 119 | 120 | @mock.patch('modelsdoc.wrappers.class_to_string', return_value='dummy') 121 | def test_class_fullname(self, mock): 122 | 123 | target = self._makeOne(self._getDummyModel(), 'connection') 124 | self.assertEqual('dummy', target.class_fullname) 125 | mock.assert_called_with('TestModel') 126 | 127 | def test_class_name(self): 128 | target = self._makeOne( 129 | self._getDummyModel(self._getDummyModel()), 130 | 'connection' 131 | ) 132 | self.assertEqual('DummyModel', target.class_name) 133 | 134 | @mock.patch('modelsdoc.wrappers.class_to_string', return_value='dummy') 135 | def test_display_name(self, mock): 136 | target = self._makeOne( 137 | self._getDummyModel(self._getDummyModel()), 138 | 'connection' 139 | ) 140 | self.assertEqual('name(dummy)', target.display_name) 141 | 142 | @mock.patch('modelsdoc.wrappers.class_to_string', return_value='dummy') 143 | def test_display_name_length(self, mock): 144 | meta = self._getDummyMeta(verbose_name='name') 145 | target = self._makeOne(self._getDummyModel(meta=meta), 'connection') 146 | self.assertEqual(11, target.display_name_length) 147 | 148 | @mock.patch('modelsdoc.wrappers.class_to_string', return_value='dummy') 149 | def test_display_name_length_for_multibyte(self, mock): 150 | meta = self._getDummyMeta(verbose_name=u'日本') 151 | target = self._makeOne(self._getDummyModel(meta=meta), 'connection') 152 | self.assertEqual(13, target.display_name_length) 153 | 154 | def test_doc(self): 155 | target = self._makeOne(self._getDummyModel(), 'connection') 156 | self.assertEqual(' TEST DOC STRING ', target.doc) 157 | 158 | def test_name(self): 159 | target = self._makeOne(self._getDummyModel(), 'connection') 160 | self.assertEqual('name', target.name) 161 | 162 | def test_fields(self): 163 | 164 | with mock.patch('modelsdoc.wrappers.ModuleAnalyzer.for_module', 165 | return_value=self._getDummyAnalyizer()) as m: 166 | 167 | target = self._makeOne(self._getDummyModel(), 'connection') 168 | self.assertEqual(3, len(target.fields)) 169 | m.assert_called_once_with('dummy_module') 170 | 171 | def test_attrdocs(self): 172 | with mock.patch('modelsdoc.wrappers.ModuleAnalyzer.for_module', 173 | return_value=self._getDummyAnalyizer()) as m: 174 | 175 | target = self._makeOne(self._getDummyModel(), 'connection') 176 | self.assertEqual('dummy', target.attrdocs) 177 | m.assert_called_once_with('dummy_module') 178 | 179 | def test_getattr(self): 180 | target = self._makeOne(self._getDummyModel(), 'connection') 181 | self.assertEqual('test', target.proxy_attr) 182 | self.assertEqual('', target.non_exists_attr) 183 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tell-k/django-modelsdoc/da8ef1c1ba4f20cecac98ca963d55352e6b3bba7/tests/urls.py -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = flake8,py{27,py,33,34,35,36,37}-django{15,16,17,18,19,110,111,20,21,22} 3 | 4 | [testenv] 5 | deps = 6 | coverage 7 | mock 8 | pbr<1.4 9 | django15: Django>=1.5,<1.6 10 | django16: Django>=1.6,<1.7 11 | django17: Django>=1.7,<1.8 12 | django18: Django>=1.8,<1.9 13 | django19: Django>=1.9,<1.10 14 | django110: Django>=1.10,<1.11 15 | django111: Django>=1.11,<1.12 16 | django20: Django>=2.0,<2.1 17 | django21: Django>=2.1,<2.2 18 | django22: Django>=2.2,<2.3 19 | commands = 20 | coverage run --source modelsdoc setup.py test 21 | coverage report -m 22 | 23 | [testenv:flake8] 24 | deps=flake8 25 | commands= 26 | flake8 modelsdoc tests 27 | --------------------------------------------------------------------------------