import sys import unittest from django.conf import settings from django.contrib import admin from django.contrib.admindocs import utils, views from django.contrib.admindocs.views import get_return_data_type, simplify_regex from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.db import models from django.db.models import fields from django.test import SimpleTestCase, modify_settings, override_settings from django.test.utils import captured_stderr from django.urls import include, path, reverse from django.utils.functional import SimpleLazyObject from .models import Company, Person from .tests import AdminDocsTestCase, TestDataMixin @unittest.skipUnless(utils.docutils_is_available, "no docutils installed.") class AdminDocViewTests(TestDataMixin, AdminDocsTestCase): def setUp(self): self.client.force_login(self.superuser) def test_index(self): response = self.client.get(reverse("django-admindocs-docroot")) self.assertContains(response, "

Documentation

", html=True) self.assertContains( response, '
Django administration
', ) self.client.logout() response = self.client.get(reverse("django-admindocs-docroot"), follow=True) # Should display the login screen self.assertContains( response, '', html=True ) def test_bookmarklets(self): response = self.client.get(reverse("django-admindocs-bookmarklets")) self.assertContains(response, "/admindocs/views/") def test_templatetag_index(self): response = self.client.get(reverse("django-admindocs-tags")) self.assertContains( response, '

extends

', html=True ) def test_templatefilter_index(self): response = self.client.get(reverse("django-admindocs-filters")) self.assertContains(response, '

first

', html=True) def test_view_index(self): response = self.client.get(reverse("django-admindocs-views-index")) self.assertContains( response, '

/admindocs/

', html=True, ) self.assertContains(response, "Views by namespace test") self.assertContains(response, "Name: test:func.") self.assertContains( response, '

' "/xview/callable_object_without_xview/

", html=True, ) def test_view_index_with_method(self): """ Views that are methods are listed correctly. """ response = self.client.get(reverse("django-admindocs-views-index")) self.assertContains( response, "

" '' "/admin/

", html=True, ) def test_view_detail(self): url = reverse( "django-admindocs-views-detail", args=["django.contrib.admindocs.views.BaseAdminDocsView"], ) response = self.client.get(url) # View docstring self.assertContains(response, "Base view for admindocs views.") def testview_docstring_links(self): summary = ( '

This is a view for ' '' "myapp.Company

" ) url = reverse( "django-admindocs-views-detail", args=["admin_docs.views.CompanyView"] ) response = self.client.get(url) self.assertContains(response, summary, html=True) @override_settings(ROOT_URLCONF="admin_docs.namespace_urls") def test_namespaced_view_detail(self): url = reverse( "django-admindocs-views-detail", args=["admin_docs.views.XViewClass"] ) response = self.client.get(url) self.assertContains(response, "

admin_docs.views.XViewClass

") def test_view_detail_illegal_import(self): url = reverse( "django-admindocs-views-detail", args=["urlpatterns_reverse.nonimported_module.view"], ) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules) def test_view_detail_as_method(self): """ Views that are methods can be displayed. """ url = reverse( "django-admindocs-views-detail", args=["django.contrib.admin.sites.AdminSite.index"], ) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_model_index(self): response = self.client.get(reverse("django-admindocs-models-index")) self.assertContains( response, '

Authentication and Authorization (django.contrib.auth)' "

", html=True, ) def test_template_detail(self): response = self.client.get( reverse( "django-admindocs-templates", args=["admin_doc/template_detail.html"] ) ) self.assertContains( response, "

Template: admin_doc/template_detail.html

", html=True, ) def test_template_detail_loader(self): response = self.client.get( reverse("django-admindocs-templates", args=["view_for_loader_test.html"]) ) self.assertContains(response, "view_for_loader_test.html") def test_missing_docutils(self): utils.docutils_is_available = False try: response = self.client.get(reverse("django-admindocs-docroot")) self.assertContains( response, "

The admin documentation system requires Python’s " 'docutils ' "library.

" "

Please ask your administrators to install " 'docutils.

', html=True, ) self.assertContains( response, '
Django administration
', ) finally: utils.docutils_is_available = True @modify_settings(INSTALLED_APPS={"remove": "django.contrib.sites"}) @override_settings(SITE_ID=None) # will restore SITE_ID after the test def test_no_sites_framework(self): """ Without the sites framework, should not access SITE_ID or Site objects. Deleting settings is fine here as UserSettingsHolder is used. """ Site.objects.all().delete() del settings.SITE_ID response = self.client.get(reverse("django-admindocs-views-index")) self.assertContains(response, "View documentation") def test_callable_urlconf(self): """ Index view should correctly resolve view patterns when ROOT_URLCONF is not a string. """ def urlpatterns(): return ( path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), ) with self.settings(ROOT_URLCONF=SimpleLazyObject(urlpatterns)): response = self.client.get(reverse("django-admindocs-views-index")) self.assertEqual(response.status_code, 200) @unittest.skipUnless(utils.docutils_is_available, "no docutils installed.") class AdminDocViewDefaultEngineOnly(TestDataMixin, AdminDocsTestCase): def setUp(self): self.client.force_login(self.superuser) def test_template_detail_path_traversal(self): cases = ["/etc/passwd", "../passwd"] for fpath in cases: with self.subTest(path=fpath): response = self.client.get( reverse("django-admindocs-templates", args=[fpath]), ) self.assertEqual(response.status_code, 400) @override_settings( TEMPLATES=[ { "NAME": "ONE", "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, }, { "NAME": "TWO", "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, }, ] ) @unittest.skipUnless(utils.docutils_is_available, "no docutils installed.") class AdminDocViewWithMultipleEngines(AdminDocViewTests): def test_templatefilter_index(self): # Overridden because non-trivial TEMPLATES settings aren't supported # but the page shouldn't crash (#24125). response = self.client.get(reverse("django-admindocs-filters")) self.assertContains(response, "Template filters", html=True) def test_templatetag_index(self): # Overridden because non-trivial TEMPLATES settings aren't supported # but the page shouldn't crash (#24125). response = self.client.get(reverse("django-admindocs-tags")) self.assertContains(response, "Template tags", html=True) @unittest.skipUnless(utils.docutils_is_available, "no docutils installed.") class TestModelDetailView(TestDataMixin, AdminDocsTestCase): def setUp(self): self.client.force_login(self.superuser) with captured_stderr() as self.docutils_stderr: self.response = self.client.get( reverse("django-admindocs-models-detail", args=["admin_docs", "Person"]) ) def test_table_headers(self): tests = [ ("Method", 1), ("Arguments", 1), ("Description", 2), ("Field", 1), ("Type", 1), ("Method", 1), ] for table_header, count in tests: self.assertContains( self.response, f'{table_header}', count=count ) def test_method_excludes(self): """ Methods that begin with strings defined in ``django.contrib.admindocs.views.MODEL_METHODS_EXCLUDE`` shouldn't be displayed in the admin docs. """ self.assertContains(self.response, "get_full_name") self.assertNotContains(self.response, "_get_full_name") self.assertNotContains(self.response, "add_image") self.assertNotContains(self.response, "delete_image") self.assertNotContains(self.response, "set_status") self.assertNotContains(self.response, "save_changes") def test_methods_with_arguments(self): """ Methods that take arguments should also displayed. """ self.assertContains(self.response, "

Methods with arguments

") self.assertContains(self.response, "rename_company") self.assertContains(self.response, "dummy_function") self.assertContains(self.response, "dummy_function_keyword_only_arg") self.assertContains(self.response, "all_kinds_arg_function") self.assertContains(self.response, "suffix_company_name") def test_methods_with_arguments_display_arguments(self): """ Methods with arguments should have their arguments displayed. """ self.assertContains(self.response, "new_name") self.assertContains(self.response, "keyword_only_arg") def test_methods_with_arguments_display_arguments_default_value(self): """ Methods with keyword arguments should have their arguments displayed. """ self.assertContains(self.response, "suffix='ltd'") def test_methods_with_multiple_arguments_display_arguments(self): """ Methods with multiple arguments should have all their arguments displayed, but omitting 'self'. """ self.assertContains( self.response, "baz, rox, *some_args, **some_kwargs" ) self.assertContains(self.response, "position_only_arg, arg, kwarg") def test_instance_of_property_methods_are_displayed(self): """Model properties are displayed as fields.""" self.assertContains(self.response, "a_property") def test_instance_of_cached_property_methods_are_displayed(self): """Model cached properties are displayed as fields.""" self.assertContains(self.response, "a_cached_property") def test_method_data_types(self): company = Company.objects.create(name="Django") person = Person.objects.create( first_name="Human", last_name="User", company=company ) self.assertEqual( get_return_data_type(person.get_status_count.__name__), "Integer" ) self.assertEqual(get_return_data_type(person.get_groups_list.__name__), "List") def test_descriptions_render_correctly(self): """ The ``description`` field should render correctly for each field type. """ # help text in fields self.assertContains( self.response, "first name - The person's first name" ) self.assertContains( self.response, "last name - The person's last name" ) # method docstrings self.assertContains(self.response, "

Get the full name of the person

") link = '%s' markup = "

the related %s object

" company_markup = markup % (link % ("admin_docs.company", "admin_docs.Company")) # foreign keys self.assertContains(self.response, company_markup) # foreign keys with help text self.assertContains(self.response, "%s\n - place of work" % company_markup) # many to many fields self.assertContains( self.response, "number of related %s objects" % (link % ("admin_docs.group", "admin_docs.Group")), ) self.assertContains( self.response, "all related %s objects" % (link % ("admin_docs.group", "admin_docs.Group")), ) # "raw" and "include" directives are disabled self.assertContains( self.response, "

"raw" directive disabled.

", ) self.assertContains( self.response, ".. raw:: html\n :file: admin_docs/evilfile.txt" ) self.assertContains( self.response, "

"include" directive disabled.

", ) self.assertContains(self.response, ".. include:: admin_docs/evilfile.txt") out = self.docutils_stderr.getvalue() self.assertIn('"raw" directive disabled', out) self.assertIn('"include" directive disabled', out) def test_model_with_many_to_one(self): link = '%s' response = self.client.get( reverse("django-admindocs-models-detail", args=["admin_docs", "company"]) ) self.assertContains( response, "number of related %s objects" % (link % ("admin_docs.person", "admin_docs.Person")), ) self.assertContains( response, "all related %s objects" % (link % ("admin_docs.person", "admin_docs.Person")), ) def test_model_with_no_backward_relations_render_only_relevant_fields(self): """ A model with ``related_name`` of `+` shouldn't show backward relationship links. """ response = self.client.get( reverse("django-admindocs-models-detail", args=["admin_docs", "family"]) ) fields = response.context_data.get("fields") self.assertEqual(len(fields), 2) def test_model_docstring_renders_correctly(self): summary = ( '

Stores information about a person, related to ' '' "myapp.Company.

" ) subheading = "

Notes

" body = ( '

Use save_changes() when saving this ' "object.

" ) model_body = ( '
company
Field storing ' "myapp.Company where the person works.
" ) self.assertContains(self.response, "DESCRIPTION") self.assertContains(self.response, summary, html=True) self.assertContains(self.response, subheading, html=True) self.assertContains(self.response, body, html=True) self.assertContains(self.response, model_body, html=True) def test_model_docstring_built_in_tag_links(self): summary = "Links with different link text." body = ( '

This is a line with tag extends\n' 'This is a line with model Family\n' 'This is a line with view Index\n' 'This is a line with template index template\n' 'This is a line with filter example filter

' ) url = reverse("django-admindocs-models-detail", args=["admin_docs", "family"]) response = self.client.get(url) self.assertContains(response, summary, html=True) self.assertContains(response, body, html=True) def test_model_detail_title(self): self.assertContains(self.response, "

admin_docs.Person

", html=True) def test_app_not_found(self): response = self.client.get( reverse("django-admindocs-models-detail", args=["doesnotexist", "Person"]) ) self.assertEqual(response.context["exception"], "App 'doesnotexist' not found") self.assertEqual(response.status_code, 404) def test_model_not_found(self): response = self.client.get( reverse( "django-admindocs-models-detail", args=["admin_docs", "doesnotexist"] ) ) self.assertEqual( response.context["exception"], "Model 'doesnotexist' not found in app 'admin_docs'", ) self.assertEqual(response.status_code, 404) def test_model_permission_denied(self): person_url = reverse( "django-admindocs-models-detail", args=["admin_docs", "person"] ) company_url = reverse( "django-admindocs-models-detail", args=["admin_docs", "company"] ) staff_user = User.objects.create_user( username="staff", password="secret", is_staff=True ) self.client.force_login(staff_user) response_for_person = self.client.get(person_url) response_for_company = self.client.get(company_url) # No access without permissions. self.assertEqual(response_for_person.status_code, 403) self.assertEqual(response_for_company.status_code, 403) company_content_type = ContentType.objects.get_for_model(Company) person_content_type = ContentType.objects.get_for_model(Person) view_company = Permission.objects.get( codename="view_company", content_type=company_content_type ) change_person = Permission.objects.get( codename="change_person", content_type=person_content_type ) staff_user.user_permissions.add(view_company, change_person) response_for_person = self.client.get(person_url) response_for_company = self.client.get(company_url) # View or change permission grants access. self.assertEqual(response_for_person.status_code, 200) self.assertEqual(response_for_company.status_code, 200) @unittest.skipUnless(utils.docutils_is_available, "no docutils installed.") class TestModelIndexView(TestDataMixin, AdminDocsTestCase): def test_model_index_superuser(self): self.client.force_login(self.superuser) index_url = reverse("django-admindocs-models-index") response = self.client.get(index_url) self.assertContains( response, 'Family', html=True, ) self.assertContains( response, 'Person', html=True, ) self.assertContains( response, 'Company', html=True, ) def test_model_index_with_model_permission(self): staff_user = User.objects.create_user( username="staff", password="secret", is_staff=True ) self.client.force_login(staff_user) index_url = reverse("django-admindocs-models-index") response = self.client.get(index_url) # Models are not listed without permissions. self.assertNotContains( response, 'Family', html=True, ) self.assertNotContains( response, 'Person', html=True, ) self.assertNotContains( response, 'Company', html=True, ) company_content_type = ContentType.objects.get_for_model(Company) person_content_type = ContentType.objects.get_for_model(Person) view_company = Permission.objects.get( codename="view_company", content_type=company_content_type ) change_person = Permission.objects.get( codename="change_person", content_type=person_content_type ) staff_user.user_permissions.add(view_company, change_person) response = self.client.get(index_url) # View or change permission grants access. self.assertNotContains( response, 'Family', html=True, ) self.assertContains( response, 'Person', html=True, ) self.assertContains( response, 'Company', html=True, ) class CustomField(models.Field): description = "A custom field type" class DescriptionLackingField(models.Field): pass class TestFieldType(unittest.TestCase): def test_field_name(self): with self.assertRaises(AttributeError): views.get_readable_field_data_type("NotAField") def test_builtin_fields(self): self.assertEqual( views.get_readable_field_data_type(fields.BooleanField()), "Boolean (Either True or False)", ) def test_char_fields(self): self.assertEqual( views.get_readable_field_data_type(fields.CharField(max_length=255)), "String (up to 255)", ) self.assertEqual( views.get_readable_field_data_type(fields.CharField()), "String (unlimited)", ) def test_custom_fields(self): self.assertEqual( views.get_readable_field_data_type(CustomField()), "A custom field type" ) self.assertEqual( views.get_readable_field_data_type(DescriptionLackingField()), "Field of type: DescriptionLackingField", ) class AdminDocViewFunctionsTests(SimpleTestCase): def test_simplify_regex(self): tests = ( # Named and unnamed groups. (r"^(?P\w+)/b/(?P\w+)/$", "//b//"), (r"^(?P\w+)/b/(?P\w+)$", "//b/"), (r"^(?P\w+)/b/(?P\w+)", "//b/"), (r"^(?P\w+)/b/(\w+)$", "//b/"), (r"^(?P\w+)/b/(\w+)", "//b/"), (r"^(?P\w+)/b/((x|y)\w+)$", "//b/"), (r"^(?P\w+)/b/((x|y)\w+)", "//b/"), (r"^(?P(x|y))/b/(?P\w+)$", "//b/"), (r"^(?P(x|y))/b/(?P\w+)", "//b/"), (r"^(?P(x|y))/b/(?P\w+)ab", "//b/ab"), (r"^(?P(x|y)(\(|\)))/b/(?P\w+)ab", "//b/ab"), # Non-capturing groups. (r"^a(?:\w+)b", "/ab"), (r"^a(?:(x|y))", "/a"), (r"^(?:\w+(?:\w+))a", "/a"), (r"^a(?:\w+)/b(?:\w+)", "/a/b"), (r"(?P\w+)/b/(?:\w+)c(?:\w+)", "//b/c"), (r"(?P\w+)/b/(\w+)/(?:\w+)c(?:\w+)", "//b//c"), # Single and repeated metacharacters. (r"^a", "/a"), (r"^^a", "/a"), (r"^^^a", "/a"), (r"a$", "/a"), (r"a$$", "/a"), (r"a$$$", "/a"), (r"a?", "/a"), (r"a??", "/a"), (r"a???", "/a"), (r"a*", "/a"), (r"a**", "/a"), (r"a***", "/a"), (r"a+", "/a"), (r"a++", "/a"), (r"a+++", "/a"), (r"\Aa", "/a"), (r"\A\Aa", "/a"), (r"\A\A\Aa", "/a"), (r"a\Z", "/a"), (r"a\Z\Z", "/a"), (r"a\Z\Z\Z", "/a"), (r"\ba", "/a"), (r"\b\ba", "/a"), (r"\b\b\ba", "/a"), (r"a\B", "/a"), (r"a\B\B", "/a"), (r"a\B\B\B", "/a"), # Multiple mixed metacharacters. (r"^a/?$", "/a/"), (r"\Aa\Z", "/a"), (r"\ba\B", "/a"), # Escaped single metacharacters. (r"\^a", r"/^a"), (r"\\^a", r"/\\a"), (r"\\\^a", r"/\\^a"), (r"\\\\^a", r"/\\\\a"), (r"\\\\\^a", r"/\\\\^a"), (r"a\$", r"/a$"), (r"a\\$", r"/a\\"), (r"a\\\$", r"/a\\$"), (r"a\\\\$", r"/a\\\\"), (r"a\\\\\$", r"/a\\\\$"), (r"a\?", r"/a?"), (r"a\\?", r"/a\\"), (r"a\\\?", r"/a\\?"), (r"a\\\\?", r"/a\\\\"), (r"a\\\\\?", r"/a\\\\?"), (r"a\*", r"/a*"), (r"a\\*", r"/a\\"), (r"a\\\*", r"/a\\*"), (r"a\\\\*", r"/a\\\\"), (r"a\\\\\*", r"/a\\\\*"), (r"a\+", r"/a+"), (r"a\\+", r"/a\\"), (r"a\\\+", r"/a\\+"), (r"a\\\\+", r"/a\\\\"), (r"a\\\\\+", r"/a\\\\+"), (r"\\Aa", r"/\Aa"), (r"\\\Aa", r"/\\a"), (r"\\\\Aa", r"/\\\Aa"), (r"\\\\\Aa", r"/\\\\a"), (r"\\\\\\Aa", r"/\\\\\Aa"), (r"a\\Z", r"/a\Z"), (r"a\\\Z", r"/a\\"), (r"a\\\\Z", r"/a\\\Z"), (r"a\\\\\Z", r"/a\\\\"), (r"a\\\\\\Z", r"/a\\\\\Z"), # Escaped mixed metacharacters. (r"^a\?$", r"/a?"), (r"^a\\?$", r"/a\\"), (r"^a\\\?$", r"/a\\?"), (r"^a\\\\?$", r"/a\\\\"), (r"^a\\\\\?$", r"/a\\\\?"), # Adjacent escaped metacharacters. (r"^a\?\$", r"/a?$"), (r"^a\\?\\$", r"/a\\\\"), (r"^a\\\?\\\$", r"/a\\?\\$"), (r"^a\\\\?\\\\$", r"/a\\\\\\\\"), (r"^a\\\\\?\\\\\$", r"/a\\\\?\\\\$"), # Complex examples with metacharacters and (un)named groups. (r"^\b(?P\w+)\B/(\w+)?", "//"), (r"^\A(?P\w+)\Z", "/"), ) for pattern, output in tests: with self.subTest(pattern=pattern): self.assertEqual(simplify_regex(pattern), output)