df_site 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- df_site/__init__.py +1 -0
- df_site/__main__.py +37 -0
- df_site/admin.py +130 -0
- df_site/apps.py +57 -0
- df_site/components/__init__.py +1 -0
- df_site/components/base.py +82 -0
- df_site/components/detail.py +191 -0
- df_site/components/list.py +446 -0
- df_site/components/list_filters.py +74 -0
- df_site/components/registry.py +55 -0
- df_site/constants.py +71 -0
- df_site/context_processors.py +61 -0
- df_site/defaults.py +319 -0
- df_site/dynamic_settings.py +37 -0
- df_site/form_fields.py +138 -0
- df_site/management/__init__.py +1 -0
- df_site/management/commands/__init__.py +1 -0
- df_site/management/commands/add_image.py +104 -0
- df_site/management/commands/generate_favicon.py +47 -0
- df_site/middleware.py +20 -0
- df_site/migrations/0001_initial.py +220 -0
- df_site/migrations/0002_alter_alertribbon_message_alter_alertribbon_summary.py +23 -0
- df_site/migrations/__init__.py +0 -0
- df_site/model_fields.py +35 -0
- df_site/models.py +130 -0
- df_site/postman/__init__.py +1 -0
- df_site/postman/forms.py +38 -0
- df_site/postman/urls.py +75 -0
- df_site/postman/views.py +65 -0
- df_site/static/css/app.css +0 -0
- df_site/static/css/base.css +22208 -0
- df_site/static/css/ckeditor5.css +422 -0
- df_site/static/favicon/android-chrome-192x192.png +0 -0
- df_site/static/favicon/android-chrome-512x512.png +0 -0
- df_site/static/favicon/apple-touch-icon.png +0 -0
- df_site/static/favicon/favicon-16x16.png +0 -0
- df_site/static/favicon/favicon-32x32.png +0 -0
- df_site/static/favicon/favicon.ico +0 -0
- df_site/static/favicon/mstile-150x150.png +0 -0
- df_site/static/favicon/safari-pinned-tab.svg +46 -0
- df_site/static/images/accessibility.svg +1 -0
- df_site/static/images/align-bottom.svg +1 -0
- df_site/static/images/align-center.svg +1 -0
- df_site/static/images/align-justify.svg +1 -0
- df_site/static/images/align-left.svg +1 -0
- df_site/static/images/align-middle.svg +1 -0
- df_site/static/images/align-right.svg +1 -0
- df_site/static/images/align-top.svg +1 -0
- df_site/static/images/bold.svg +1 -0
- df_site/static/images/browse-files.svg +1 -0
- df_site/static/images/bulletedlist.svg +1 -0
- df_site/static/images/cancel.svg +1 -0
- df_site/static/images/caption.svg +1 -0
- df_site/static/images/check.svg +1 -0
- df_site/static/images/code.svg +1 -0
- df_site/static/images/codeblock.svg +1 -0
- df_site/static/images/cog.svg +1 -0
- df_site/static/images/color-palette.svg +1 -0
- df_site/static/images/color-tile-check.svg +1 -0
- df_site/static/images/drag-handle.svg +1 -0
- df_site/static/images/drag-indicator.svg +1 -0
- df_site/static/images/dropdown-arrow.svg +1 -0
- df_site/static/images/eraser.svg +1 -0
- df_site/static/images/file-arrow-up-solid.svg +1 -0
- df_site/static/images/find-replace.svg +1 -0
- df_site/static/images/font-background.svg +1 -0
- df_site/static/images/font-color.svg +1 -0
- df_site/static/images/font-family.svg +1 -0
- df_site/static/images/font-size.svg +1 -0
- df_site/static/images/heading1.svg +1 -0
- df_site/static/images/heading2.svg +1 -0
- df_site/static/images/heading3.svg +1 -0
- df_site/static/images/heading4.svg +1 -0
- df_site/static/images/heading5.svg +1 -0
- df_site/static/images/heading6.svg +1 -0
- df_site/static/images/history.svg +1 -0
- df_site/static/images/horizontalline.svg +1 -0
- df_site/static/images/html.svg +1 -0
- df_site/static/images/image-asset-manager.svg +1 -0
- df_site/static/images/image-upload.svg +1 -0
- df_site/static/images/image-url.svg +1 -0
- df_site/static/images/image.svg +1 -0
- df_site/static/images/importexport.svg +1 -0
- df_site/static/images/indent.svg +1 -0
- df_site/static/images/italic.svg +1 -0
- df_site/static/images/link.svg +1 -0
- df_site/static/images/liststylecircle.svg +1 -0
- df_site/static/images/liststyledecimal.svg +1 -0
- df_site/static/images/liststyledecimalleadingzero.svg +1 -0
- df_site/static/images/liststyledisc.svg +1 -0
- df_site/static/images/liststylelowerlatin.svg +1 -0
- df_site/static/images/liststylelowerroman.svg +1 -0
- df_site/static/images/liststylesquare.svg +1 -0
- df_site/static/images/liststyleupperlatin.svg +1 -0
- df_site/static/images/liststyleupperroman.svg +1 -0
- df_site/static/images/loupe.svg +1 -0
- df_site/static/images/low-vision.svg +1 -0
- df_site/static/images/marker.svg +1 -0
- df_site/static/images/media-placeholder.svg +1 -0
- df_site/static/images/media.svg +1 -0
- df_site/static/images/next-arrow.svg +1 -0
- df_site/static/images/numberedlist.svg +1 -0
- df_site/static/images/object-center.svg +1 -0
- df_site/static/images/object-full-width.svg +1 -0
- df_site/static/images/object-inline-left.svg +1 -0
- df_site/static/images/object-inline-right.svg +1 -0
- df_site/static/images/object-inline.svg +1 -0
- df_site/static/images/object-left.svg +1 -0
- df_site/static/images/object-right.svg +1 -0
- df_site/static/images/object-size-custom.svg +1 -0
- df_site/static/images/object-size-full.svg +1 -0
- df_site/static/images/object-size-large.svg +1 -0
- df_site/static/images/object-size-medium.svg +1 -0
- df_site/static/images/object-size-small.svg +1 -0
- df_site/static/images/outdent.svg +1 -0
- df_site/static/images/paragraph.svg +1 -0
- df_site/static/images/pen.svg +1 -0
- df_site/static/images/pencil.svg +1 -0
- df_site/static/images/pilcrow.svg +1 -0
- df_site/static/images/plus.svg +1 -0
- df_site/static/images/previous-arrow.svg +1 -0
- df_site/static/images/project-logo.svg +1 -0
- df_site/static/images/quote.svg +1 -0
- df_site/static/images/redo.svg +1 -0
- df_site/static/images/remove-format.svg +1 -0
- df_site/static/images/return-arrow.svg +1 -0
- df_site/static/images/select-all.svg +1 -0
- df_site/static/images/show-blocks.svg +1 -0
- df_site/static/images/source-editing.svg +1 -0
- df_site/static/images/specialcharacters.svg +1 -0
- df_site/static/images/strikethrough.svg +1 -0
- df_site/static/images/subscript.svg +1 -0
- df_site/static/images/superscript.svg +1 -0
- df_site/static/images/table-cell-properties.svg +1 -0
- df_site/static/images/table-column.svg +1 -0
- df_site/static/images/table-merge-cell.svg +1 -0
- df_site/static/images/table-properties.svg +1 -0
- df_site/static/images/table-row.svg +1 -0
- df_site/static/images/table.svg +1 -0
- df_site/static/images/text-alternative.svg +1 -0
- df_site/static/images/text.svg +1 -0
- df_site/static/images/three-vertical-dots.svg +1 -0
- df_site/static/images/todolist.svg +1 -0
- df_site/static/images/underline.svg +1 -0
- df_site/static/images/undo.svg +1 -0
- df_site/static/images/unlink.svg +1 -0
- df_site/static/js/app.js +98 -0
- df_site/static/js/app.js.map +1 -0
- df_site/static/js/base.js +161181 -0
- df_site/static/js/base.js.map +1 -0
- df_site/static/translations/af.js +1 -0
- df_site/static/translations/ar.js +1 -0
- df_site/static/translations/ast.js +1 -0
- df_site/static/translations/az.js +1 -0
- df_site/static/translations/bg.js +1 -0
- df_site/static/translations/bn.js +1 -0
- df_site/static/translations/bs.js +1 -0
- df_site/static/translations/ca.js +1 -0
- df_site/static/translations/cs.js +1 -0
- df_site/static/translations/da.js +1 -0
- df_site/static/translations/de-ch.js +1 -0
- df_site/static/translations/de.js +1 -0
- df_site/static/translations/el.js +1 -0
- df_site/static/translations/en-au.js +1 -0
- df_site/static/translations/en-gb.js +1 -0
- df_site/static/translations/en.js +1 -0
- df_site/static/translations/eo.js +1 -0
- df_site/static/translations/es-co.js +1 -0
- df_site/static/translations/es.js +1 -0
- df_site/static/translations/et.js +1 -0
- df_site/static/translations/eu.js +1 -0
- df_site/static/translations/fa.js +1 -0
- df_site/static/translations/fi.js +1 -0
- df_site/static/translations/gl.js +1 -0
- df_site/static/translations/gu.js +1 -0
- df_site/static/translations/he.js +1 -0
- df_site/static/translations/hi.js +1 -0
- df_site/static/translations/hr.js +1 -0
- df_site/static/translations/hu.js +1 -0
- df_site/static/translations/hy.js +1 -0
- df_site/static/translations/id.js +1 -0
- df_site/static/translations/it.js +1 -0
- df_site/static/translations/ja.js +1 -0
- df_site/static/translations/jv.js +1 -0
- df_site/static/translations/kk.js +1 -0
- df_site/static/translations/km.js +1 -0
- df_site/static/translations/kn.js +1 -0
- df_site/static/translations/ko.js +1 -0
- df_site/static/translations/ku.js +1 -0
- df_site/static/translations/lt.js +1 -0
- df_site/static/translations/lv.js +1 -0
- df_site/static/translations/ms.js +1 -0
- df_site/static/translations/nb.js +1 -0
- df_site/static/translations/ne.js +1 -0
- df_site/static/translations/nl.js +1 -0
- df_site/static/translations/no.js +1 -0
- df_site/static/translations/oc.js +1 -0
- df_site/static/translations/pl.js +1 -0
- df_site/static/translations/pt-br.js +1 -0
- df_site/static/translations/pt.js +1 -0
- df_site/static/translations/ro.js +1 -0
- df_site/static/translations/ru.js +1 -0
- df_site/static/translations/si.js +1 -0
- df_site/static/translations/sk.js +1 -0
- df_site/static/translations/sl.js +1 -0
- df_site/static/translations/sq.js +1 -0
- df_site/static/translations/sr-latn.js +1 -0
- df_site/static/translations/sr.js +1 -0
- df_site/static/translations/sv.js +1 -0
- df_site/static/translations/th.js +1 -0
- df_site/static/translations/ti.js +1 -0
- df_site/static/translations/tk.js +1 -0
- df_site/static/translations/tr.js +1 -0
- df_site/static/translations/tt.js +1 -0
- df_site/static/translations/ug.js +1 -0
- df_site/static/translations/uk.js +1 -0
- df_site/static/translations/ur.js +1 -0
- df_site/static/translations/uz.js +1 -0
- df_site/static/translations/vi.js +1 -0
- df_site/static/translations/zh-cn.js +1 -0
- df_site/static/translations/zh.js +1 -0
- df_site/static/webfonts/fa-brands-400.ttf +0 -0
- df_site/static/webfonts/fa-brands-400.woff2 +0 -0
- df_site/static/webfonts/fa-regular-400.ttf +0 -0
- df_site/static/webfonts/fa-regular-400.woff2 +0 -0
- df_site/static/webfonts/fa-solid-900.ttf +0 -0
- df_site/static/webfonts/fa-solid-900.woff2 +0 -0
- df_site/static/webfonts/fa-v4compatibility.ttf +0 -0
- df_site/static/webfonts/fa-v4compatibility.woff2 +0 -0
- df_site/templates/account/email.html +78 -0
- df_site/templates/account/password_change.html +28 -0
- df_site/templates/account/snippets/warn_no_email.html +6 -0
- df_site/templates/allauth/elements/alert.html +6 -0
- df_site/templates/allauth/elements/badge.html +4 -0
- df_site/templates/allauth/elements/button.html +14 -0
- df_site/templates/allauth/elements/button_group.html +5 -0
- df_site/templates/allauth/elements/field.html +72 -0
- df_site/templates/allauth/elements/fields.html +3 -0
- df_site/templates/allauth/elements/form.html +10 -0
- df_site/templates/allauth/elements/h1.html +1 -0
- df_site/templates/allauth/elements/h2.html +1 -0
- df_site/templates/allauth/elements/img.html +4 -0
- df_site/templates/allauth/elements/p.html +1 -0
- df_site/templates/allauth/elements/panel.html +14 -0
- df_site/templates/allauth/elements/provider.html +6 -0
- df_site/templates/allauth/elements/provider_list.html +5 -0
- df_site/templates/allauth/elements/table.html +5 -0
- df_site/templates/allauth/layouts/base.html +14 -0
- df_site/templates/allauth/layouts/entrance.html +20 -0
- df_site/templates/allauth/layouts/manage.html +1 -0
- df_site/templates/cookie_consent/_cookie_group.html +64 -0
- df_site/templates/cookie_consent/cookiegroup_list.html +23 -0
- df_site/templates/df_components/base.html +0 -0
- df_site/templates/df_components/detail.html +12 -0
- df_site/templates/df_components/detail_fieldset.html +46 -0
- df_site/templates/df_components/list.html +42 -0
- df_site/templates/df_components/list_filter.html +13 -0
- df_site/templates/df_components/list_filters.html +36 -0
- df_site/templates/df_components/list_hierarchy.html +25 -0
- df_site/templates/df_components/list_pagination.html +39 -0
- df_site/templates/df_components/list_search_form.html +38 -0
- df_site/templates/df_components/list_table.html +35 -0
- df_site/templates/df_site/app.html +1 -0
- df_site/templates/df_site/base.html +221 -0
- df_site/templates/df_site/detail.html +8 -0
- df_site/templates/df_site/humans.txt +11 -0
- df_site/templates/df_site/manage_base.html +51 -0
- df_site/templates/df_site/popup_app.html +1 -0
- df_site/templates/df_site/popup_base.html +29 -0
- df_site/templates/df_site/security.txt +5 -0
- df_site/templates/django_bootstrap5/breadcrumb.html +17 -0
- df_site/templates/django_bootstrap5/pagination.html +40 -0
- df_site/templates/django_ckeditor_5/widget.html +13 -0
- df_site/templates/favicon/browserconfig.xml +9 -0
- df_site/templates/mfa/index.html +115 -0
- df_site/templates/mfa/recovery_codes/index.html +33 -0
- df_site/templates/mfa/webauthn/authenticator_list.html +74 -0
- df_site/templates/pipeline/css.html +1 -0
- df_site/templates/pipeline/js.html +1 -0
- df_site/templates/postman/archives.html +8 -0
- df_site/templates/postman/base.html +20 -0
- df_site/templates/postman/base_folder.html +71 -0
- df_site/templates/postman/base_write.html +26 -0
- df_site/templates/postman/inbox.html +7 -0
- df_site/templates/postman/inc_subject_ex.html +21 -0
- df_site/templates/postman/trash.html +12 -0
- df_site/templates/postman/view.html +64 -0
- df_site/templates/users/settings.html +26 -0
- df_site/templates/usersessions/usersession_list.html +70 -0
- df_site/templatetags/__init__.py +1 -0
- df_site/templatetags/df_site.py +241 -0
- df_site/templatetags/images.py +515 -0
- df_site/templatetags/pipeline_sri.py +97 -0
- df_site/testing/__init__.py +1 -0
- df_site/testing/multiple_views.py +369 -0
- df_site/testing/requests.py +299 -0
- df_site/urls.py +41 -0
- df_site/user_settings.py +69 -0
- df_site/users/__init__.py +1 -0
- df_site/users/forms.py +35 -0
- df_site/users/notifications.py +14 -0
- df_site/users/urls.py +17 -0
- df_site/users/views.py +75 -0
- df_site/views.py +122 -0
- df_site-0.1.0.dist-info/LICENSE +519 -0
- df_site-0.1.0.dist-info/METADATA +217 -0
- df_site-0.1.0.dist-info/RECORD +309 -0
- df_site-0.1.0.dist-info/WHEEL +4 -0
- df_site-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,369 @@
|
|
1
|
+
"""A generic class testing several views with different users.
|
2
|
+
|
3
|
+
The expected response can by defined by the response code, a HttpResponse, an exception or a callable.
|
4
|
+
In these case, a RequestTester will be built with this value as first argument.
|
5
|
+
Otherwise, you can also provide a RequestTester (especially to provide GET or POST arguments).
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Dict, List, Optional, Tuple, Type, TypeAlias, Union
|
10
|
+
from urllib.parse import urlencode
|
11
|
+
|
12
|
+
from django.conf import settings
|
13
|
+
from django.contrib import admin
|
14
|
+
from django.contrib.admin.sites import site as default_site
|
15
|
+
from django.contrib.auth import get_user_model
|
16
|
+
from django.contrib.auth.models import AbstractUser, AnonymousUser
|
17
|
+
from django.contrib.messages.middleware import MessageMiddleware
|
18
|
+
from django.contrib.sessions.middleware import SessionMiddleware
|
19
|
+
from django.core.exceptions import PermissionDenied
|
20
|
+
from django.db import models
|
21
|
+
from django.http import Http404, HttpResponse, SimpleCookie
|
22
|
+
from django.middleware.csrf import get_token
|
23
|
+
from django.test import RequestFactory, TestCase
|
24
|
+
from django.urls import URLPattern
|
25
|
+
from django.urls.resolvers import RoutePattern
|
26
|
+
from django.utils.encoding import iri_to_uri
|
27
|
+
|
28
|
+
from df_site.testing.requests import RequestTester
|
29
|
+
|
30
|
+
logger = logging.getLogger(__name__)
|
31
|
+
ExpectedResponse: TypeAlias = Union[int, Exception, Type[Exception], HttpResponse, callable, RequestTester]
|
32
|
+
ExpectedResponses = Union[List[ExpectedResponse], ExpectedResponse]
|
33
|
+
|
34
|
+
|
35
|
+
class TestMultipleViews(TestCase):
|
36
|
+
"""Define some methods for testing a set of views with different users (identified by its username).
|
37
|
+
|
38
|
+
`expected_responses` is a dict such that:
|
39
|
+
- the key is the view name
|
40
|
+
- the value is a dict such that:
|
41
|
+
- the key is a USER_KEY (can be anything, like its privilege level)
|
42
|
+
- the value is any of:
|
43
|
+
- an integer (the expected HTTP status code — then this is the only checked thing)
|
44
|
+
- an exception (only the type of the raised exception is checked)
|
45
|
+
- a HttpResponse (only status_codes are checked)
|
46
|
+
- a callable that will be given the TestCase and the HttpResponse as arguments
|
47
|
+
- a RequestTester to deeply customize the test (including the request args/kwargs)
|
48
|
+
- a list of any of the previous values
|
49
|
+
|
50
|
+
You must override `get_user_keys` to provide a list of values to create different users.
|
51
|
+
(`str(value)` is used to get the username)
|
52
|
+
Your test_* methods must call
|
53
|
+
check_url_patterns(url_patterns, common_kwargs, expected_responses, display_prefix=display_prefix)
|
54
|
+
"""
|
55
|
+
|
56
|
+
views_test_counter = 0
|
57
|
+
call_test_counter = 0
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def setUpClass(cls):
|
61
|
+
"""Create some users and a request factory for testing."""
|
62
|
+
super().setUpClass()
|
63
|
+
# Every test needs access to the request factory.
|
64
|
+
cls.request_factory = RequestFactory()
|
65
|
+
cls.created_users: List[AbstractUser] = cls.create_users()
|
66
|
+
|
67
|
+
def setUp(self):
|
68
|
+
"""Counts the number of run tests."""
|
69
|
+
TestMultipleViews.call_test_counter += 1
|
70
|
+
return super().setUp()
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def get_users(cls) -> List[AbstractUser]:
|
74
|
+
"""Teturn a list of values to identify different users.
|
75
|
+
|
76
|
+
The special value `None` is dedicated to AnonymousUser.
|
77
|
+
"""
|
78
|
+
return cls.created_users
|
79
|
+
|
80
|
+
@classmethod
|
81
|
+
def create_users(cls):
|
82
|
+
"""Create some users for testing."""
|
83
|
+
return [
|
84
|
+
AnonymousUser(),
|
85
|
+
cls.create_user("staff", is_staff=True),
|
86
|
+
cls.create_user("admin", is_staff=True, is_superuser=True),
|
87
|
+
]
|
88
|
+
|
89
|
+
@classmethod
|
90
|
+
def create_user(cls, name, is_active=True, **kwargs):
|
91
|
+
"""Create a user with the provided name."""
|
92
|
+
user = get_user_model().objects.filter(username=name).first()
|
93
|
+
if user is not None:
|
94
|
+
return user
|
95
|
+
user_kwargs = {
|
96
|
+
"email": f"{name}@test.com",
|
97
|
+
"is_active": is_active,
|
98
|
+
}
|
99
|
+
user_kwargs.update(kwargs)
|
100
|
+
return get_user_model().objects.create_user(username=name, **user_kwargs)
|
101
|
+
|
102
|
+
def create_request(
|
103
|
+
self,
|
104
|
+
url,
|
105
|
+
*args,
|
106
|
+
method=None,
|
107
|
+
user=None,
|
108
|
+
form_data=None,
|
109
|
+
get_data=None,
|
110
|
+
post_data=None,
|
111
|
+
**kwargs,
|
112
|
+
):
|
113
|
+
"""Return a request object for the provided URL."""
|
114
|
+
if form_data:
|
115
|
+
kwargs["data"] = urlencode(form_data)
|
116
|
+
kwargs["content_type"] = "application/x-www-form-urlencoded"
|
117
|
+
if method:
|
118
|
+
method = method.lower()
|
119
|
+
if method and method in ("get", "head") and get_data:
|
120
|
+
kwargs["data"] = get_data
|
121
|
+
elif get_data:
|
122
|
+
kwargs["QUERY_STRING"] = urlencode(get_data, doseq=True)
|
123
|
+
if post_data:
|
124
|
+
kwargs["data"] = post_data
|
125
|
+
method = method or "post"
|
126
|
+
request = self.request_factory.generic(method.upper(), url, *args, **kwargs)
|
127
|
+
else:
|
128
|
+
method = method or "get"
|
129
|
+
request = getattr(self.request_factory, method)(url, *args, **kwargs)
|
130
|
+
request.user = user or AnonymousUser()
|
131
|
+
request.cookies = SimpleCookie()
|
132
|
+
setattr(request, "_dont_enforce_csrf_checks", True)
|
133
|
+
request.cookies["csrftoken"] = get_token(request)
|
134
|
+
|
135
|
+
def get_response():
|
136
|
+
pass
|
137
|
+
|
138
|
+
SessionMiddleware(get_response).process_request(request)
|
139
|
+
MessageMiddleware(get_response).process_request(request)
|
140
|
+
return request
|
141
|
+
|
142
|
+
def check_url_patterns(
|
143
|
+
self,
|
144
|
+
url_patterns: List[Tuple[str, URLPattern]],
|
145
|
+
common_kwargs: Dict[str, str],
|
146
|
+
expected_responses: Dict[str, Dict[Optional[str], ExpectedResponses]],
|
147
|
+
display_prefix: str = "",
|
148
|
+
):
|
149
|
+
"""Check all provided URL patterns.
|
150
|
+
|
151
|
+
:param url_patterns: list of ("namespace:", URLPattern) (leave "" if no namespace)
|
152
|
+
:param common_kwargs: kwargs common to all URL patterns
|
153
|
+
(use `expected_responses` to provided kwargs specific to a URL pattern)
|
154
|
+
:param expected_responses:
|
155
|
+
:param display_prefix: prefix added to displayed error messages
|
156
|
+
:return:
|
157
|
+
"""
|
158
|
+
failures = [] # list of (url_pattern, ps, exception)
|
159
|
+
for namespace, url_pattern in url_patterns:
|
160
|
+
if url_pattern.name:
|
161
|
+
view_name = f"{namespace}{url_pattern.name}"
|
162
|
+
else:
|
163
|
+
view_name = None
|
164
|
+
url_name = self.format_url_pattern(url_pattern)
|
165
|
+
view = url_pattern.callback
|
166
|
+
provided_view_kwargs = url_pattern.default_args
|
167
|
+
# "kwargs" argument provided to path()
|
168
|
+
if isinstance(url_pattern.pattern, RoutePattern):
|
169
|
+
arg_names = list(url_pattern.pattern.converters.keys())
|
170
|
+
else:
|
171
|
+
arg_names = list(url_pattern.pattern.regex.groupindex)
|
172
|
+
reverse_kwargs = {k: common_kwargs[k] for k in arg_names if k in common_kwargs}
|
173
|
+
for user in self.get_users():
|
174
|
+
username = None if user.is_anonymous else user.username
|
175
|
+
request_testers_: ExpectedResponses = expected_responses.get(url_name, {}).get(username, Http404)
|
176
|
+
if not isinstance(request_testers_, list):
|
177
|
+
request_testers: List[ExpectedResponse] = [request_testers_]
|
178
|
+
else:
|
179
|
+
request_testers: List[ExpectedResponse] = request_testers_
|
180
|
+
for request_tester in request_testers:
|
181
|
+
if not isinstance(request_tester, RequestTester):
|
182
|
+
request_tester = RequestTester(request_tester)
|
183
|
+
# actual view kwargs are the kwargs provided in the get_urls()
|
184
|
+
self.__class__.views_test_counter += 1
|
185
|
+
exc = request_tester.evaluate(
|
186
|
+
self,
|
187
|
+
view_name,
|
188
|
+
view,
|
189
|
+
reverse_kwargs,
|
190
|
+
provided_view_kwargs,
|
191
|
+
user,
|
192
|
+
)
|
193
|
+
if exc is not None:
|
194
|
+
failed_url = request_tester.request.path + (
|
195
|
+
"?" + iri_to_uri(request_tester.request.META.get("QUERY_STRING", ""))
|
196
|
+
if request_tester.request.META.get("QUERY_STRING", "")
|
197
|
+
else ""
|
198
|
+
)
|
199
|
+
failure = (
|
200
|
+
url_pattern,
|
201
|
+
username,
|
202
|
+
exc,
|
203
|
+
failed_url,
|
204
|
+
)
|
205
|
+
failures.append(failure)
|
206
|
+
for url_pattern, username, e, url in failures:
|
207
|
+
pattern = self.format_url_pattern(url_pattern)
|
208
|
+
if username is None:
|
209
|
+
username = "AnonymousUser"
|
210
|
+
else:
|
211
|
+
username = f"user {username}"
|
212
|
+
base_url = settings.SERVER_BASE_URL[:-1]
|
213
|
+
msg = f"{display_prefix}Exception in {pattern} with {username}: {e} ({base_url}{url})."
|
214
|
+
print(msg)
|
215
|
+
self.assertEqual(0, len(failures))
|
216
|
+
|
217
|
+
@staticmethod
|
218
|
+
def format_url_pattern(url_pattern) -> str:
|
219
|
+
"""Return a string representation of the provided URL pattern."""
|
220
|
+
if url_pattern.name:
|
221
|
+
return url_pattern.name
|
222
|
+
if isinstance(url_pattern.pattern, RoutePattern):
|
223
|
+
return url_pattern.pattern._route
|
224
|
+
return url_pattern.pattern._regex
|
225
|
+
|
226
|
+
|
227
|
+
class TestModelAdmin(TestMultipleViews):
|
228
|
+
"""Define some methods for testing *all* views defined in the get_urls() of a ModelAdmin.
|
229
|
+
|
230
|
+
Each view is tested with different users.
|
231
|
+
. A RequestTester define more precisely how to test a given view with a given user.
|
232
|
+
|
233
|
+
Many of these views require some kwargs for the `reverse` function, but most of them have always the same meaning.
|
234
|
+
So, we always reuse the same kwargs for all views (but we can override them in a RequestTester).
|
235
|
+
|
236
|
+
`expected_responses` is a dict such that:
|
237
|
+
- the key is the view name
|
238
|
+
- the value is a dict such that:
|
239
|
+
- the key is a username (or None for AnonymousUser)
|
240
|
+
- the value is any of:
|
241
|
+
- an integer (the expected HTTP status code — then this is the only checked thing)
|
242
|
+
- an exception (only the type of the raised exception is checked)
|
243
|
+
- a HttpResponse (only status_codes are checked)
|
244
|
+
- a callable that will be given the TestCase and the HttpResponse as arguments
|
245
|
+
- a RequestTester to deeply customize the test (including the request args/kwargs)
|
246
|
+
- a list of any of the previous values
|
247
|
+
|
248
|
+
"""
|
249
|
+
|
250
|
+
checked_model_admins = set()
|
251
|
+
placeholders = "%(app_label)s_%(model_name)s_"
|
252
|
+
expected_responses: Dict[str, Dict[Optional[str], ExpectedResponses]] = {
|
253
|
+
f"{placeholders}changelist": {
|
254
|
+
None: 302,
|
255
|
+
"staff": PermissionDenied,
|
256
|
+
"admin": 200,
|
257
|
+
},
|
258
|
+
f"{placeholders}add": {
|
259
|
+
None: 302,
|
260
|
+
"staff": PermissionDenied,
|
261
|
+
"admin": 200,
|
262
|
+
},
|
263
|
+
f"{placeholders}history": {
|
264
|
+
None: 302,
|
265
|
+
"staff": PermissionDenied,
|
266
|
+
"admin": 200,
|
267
|
+
},
|
268
|
+
f"{placeholders}delete": {
|
269
|
+
None: 302,
|
270
|
+
"staff": PermissionDenied,
|
271
|
+
"admin": 200,
|
272
|
+
},
|
273
|
+
f"{placeholders}change": {
|
274
|
+
None: 302,
|
275
|
+
"staff": PermissionDenied,
|
276
|
+
"admin": 200,
|
277
|
+
},
|
278
|
+
"<path:object_id>/": {
|
279
|
+
None: 302,
|
280
|
+
"staff": 302,
|
281
|
+
"admin": 302,
|
282
|
+
},
|
283
|
+
}
|
284
|
+
|
285
|
+
# expected_responses["view_name"]["staff"] = 200
|
286
|
+
# %(app_label)s and %(model_name)s can
|
287
|
+
# be used in the view names to avoid repeating the same values.
|
288
|
+
def check_model_admin(
|
289
|
+
self,
|
290
|
+
model_admin: Union[Type[admin.ModelAdmin], Type[models.Model]],
|
291
|
+
common_kwargs: Dict[str, str],
|
292
|
+
display_prefix: str = "",
|
293
|
+
expected_responses: Dict[str, Dict[Optional[str], ExpectedResponses]] = None,
|
294
|
+
):
|
295
|
+
"""Check all admin views.
|
296
|
+
|
297
|
+
:param model_admin: the models.Model or admin.ModelAdmin to check
|
298
|
+
:param common_kwargs: dict of values for reversing URLs
|
299
|
+
:param display_prefix: prefix to display in front of each error
|
300
|
+
:param expected_responses: extra responses, only for this ModelAdmin
|
301
|
+
(same format as self.expected_responses)
|
302
|
+
:return:
|
303
|
+
"""
|
304
|
+
if isinstance(model_admin, type) and issubclass(model_admin, models.Model):
|
305
|
+
model_admin = default_site.get_model_admin(model_admin)
|
306
|
+
|
307
|
+
TestModelAdmin.checked_model_admins.add(model_admin)
|
308
|
+
base_expected_responses = self.get_expected_responses(model_admin, raw_expected_responses=expected_responses)
|
309
|
+
# with app_label="app", model_name="model", if we have both
|
310
|
+
# "%(app_label)s_%(model_name)s_viewname" and "app_model_viewname" keys in raw_expected_responses
|
311
|
+
# we do not want to override the initial values.
|
312
|
+
url_patterns = self.get_admin_urls(model_admin)
|
313
|
+
self.check_url_patterns(
|
314
|
+
url_patterns,
|
315
|
+
common_kwargs,
|
316
|
+
base_expected_responses,
|
317
|
+
display_prefix=display_prefix,
|
318
|
+
)
|
319
|
+
|
320
|
+
def get_expected_responses(
|
321
|
+
self,
|
322
|
+
model_admin: admin.ModelAdmin,
|
323
|
+
raw_expected_responses: Dict[str, Dict[Optional[str], ExpectedResponses]] = None,
|
324
|
+
) -> Dict[str, Dict[Optional[str], ExpectedResponses]]:
|
325
|
+
"""Return the expected responses for this model_admin.
|
326
|
+
|
327
|
+
Replace %(app_label)s and %(model_name)s placeholders in the view names and use
|
328
|
+
them when these view names are missing from the expected_responses.
|
329
|
+
"""
|
330
|
+
base_expected_responses = {}
|
331
|
+
base_expected_responses.update(self.expected_responses)
|
332
|
+
if raw_expected_responses is not None:
|
333
|
+
base_expected_responses.update(raw_expected_responses)
|
334
|
+
|
335
|
+
opts = {
|
336
|
+
"app_label": model_admin.model._meta.app_label,
|
337
|
+
"model_name": model_admin.model._meta.model_name,
|
338
|
+
}
|
339
|
+
expected_responses = {url_name % opts: v for (url_name, v) in base_expected_responses.items() if url_name}
|
340
|
+
expected_responses.update(base_expected_responses)
|
341
|
+
return expected_responses
|
342
|
+
|
343
|
+
# noinspection PyMethodMayBeStatic
|
344
|
+
def get_admin_urls(self, model_admin: admin.ModelAdmin) -> List[Tuple[str, URLPattern]]:
|
345
|
+
"""Return the list of all URLs in this model_admin."""
|
346
|
+
return [("admin:", x) for x in model_admin.get_urls()]
|
347
|
+
|
348
|
+
|
349
|
+
class TestModel(TestModelAdmin):
|
350
|
+
"""Define some methods for testing all views defined in the get_urls() of a ModelAdmin."""
|
351
|
+
|
352
|
+
def get_object(self):
|
353
|
+
"""Return an object of the model to test."""
|
354
|
+
raise NotImplementedError("You must override this method to return an object of the model to test.")
|
355
|
+
|
356
|
+
def test_model_admin(self):
|
357
|
+
"""Test all views of the model admin correspoding to the provided object."""
|
358
|
+
try:
|
359
|
+
obj = self.get_object()
|
360
|
+
except NotImplementedError as e:
|
361
|
+
self.skipTest(str(e))
|
362
|
+
if obj._state.adding:
|
363
|
+
obj.save()
|
364
|
+
common_kwargs = self.get_common_kwargs_for_object(obj)
|
365
|
+
self.check_model_admin(obj.__class__, common_kwargs=common_kwargs, display_prefix="")
|
366
|
+
|
367
|
+
def get_common_kwargs_for_object(self, obj) -> Dict[str, str]:
|
368
|
+
"""Return the common kwargs for all views of the provided object."""
|
369
|
+
return {"object_id": str(obj.pk)}
|
@@ -0,0 +1,299 @@
|
|
1
|
+
"""Check if a response from a Django view meets the expectations.
|
2
|
+
|
3
|
+
These expectations can be as low as a simple HTTP code.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import traceback
|
8
|
+
from html.parser import HTMLParser
|
9
|
+
from typing import Callable, Dict, List, Optional, Set, Tuple, Type, Union
|
10
|
+
from unittest import TestCase
|
11
|
+
|
12
|
+
from django.contrib.auth.models import AbstractUser
|
13
|
+
from django.core.exceptions import PermissionDenied
|
14
|
+
from django.core.files.uploadedfile import SimpleUploadedFile
|
15
|
+
from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
|
16
|
+
from django.template.response import TemplateResponse
|
17
|
+
from django.urls import reverse
|
18
|
+
|
19
|
+
|
20
|
+
class FormHTMLParser(HTMLParser):
|
21
|
+
"""Fetch all field names in a HTML document."""
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
form_id: Optional[str] = None,
|
26
|
+
convert_charrefs=True,
|
27
|
+
ignored_fields: Optional[Set[str]] = None,
|
28
|
+
ignored_types: Optional[Set[str]] = None,
|
29
|
+
):
|
30
|
+
"""Initialize the parser."""
|
31
|
+
super().__init__(convert_charrefs=convert_charrefs)
|
32
|
+
self.form_id = form_id
|
33
|
+
self.form_fields: Set[str] = set()
|
34
|
+
self.is_in_selected_form = False
|
35
|
+
if ignored_fields is None:
|
36
|
+
ignored_fields = {"csrfmiddlewaretoken"}
|
37
|
+
if ignored_types is None:
|
38
|
+
ignored_types = {"submit", "reset", "button", "image"}
|
39
|
+
self.ignored_fields = ignored_fields
|
40
|
+
self.ignored_types = ignored_types
|
41
|
+
|
42
|
+
def reset(self, form_id=None):
|
43
|
+
"""Reset the parser."""
|
44
|
+
super().reset()
|
45
|
+
self.form_fields: Set[str] = set()
|
46
|
+
self.is_in_selected_form = False
|
47
|
+
|
48
|
+
def handle_starttag(self, tag: str, attrs: List[Tuple[str, str]]):
|
49
|
+
"""Check if we are in the form and if we look at a field."""
|
50
|
+
attrs = dict(attrs)
|
51
|
+
if tag == "form" and (self.form_id is None or attrs.get("id") == self.form_id):
|
52
|
+
self.is_in_selected_form = True
|
53
|
+
if self.is_in_selected_form and tag in {"input", "select", "textarea"} and "name" in attrs:
|
54
|
+
if attrs["name"] in self.ignored_fields:
|
55
|
+
return
|
56
|
+
elif tag == "input" and attrs.get("type") in self.ignored_types:
|
57
|
+
return
|
58
|
+
self.form_fields.add(attrs["name"])
|
59
|
+
|
60
|
+
def handle_endtag(self, tag):
|
61
|
+
"""Check if we are still in the form."""
|
62
|
+
if tag == "form":
|
63
|
+
self.is_in_selected_form = False
|
64
|
+
|
65
|
+
def error(self, message):
|
66
|
+
"""Does nothing when there is an error."""
|
67
|
+
pass
|
68
|
+
|
69
|
+
|
70
|
+
class RequestTester:
|
71
|
+
"""Check if the response of a Django view meets the expectation."""
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
expected_response: Union[int, Exception, Type[Exception], HttpResponse, callable] = 200,
|
76
|
+
method="get",
|
77
|
+
form_data: Union[Dict[str, str], Callable] = None,
|
78
|
+
get_data: Union[Dict[str, str], Callable] = None,
|
79
|
+
post_data: Union[bytes, Dict[str, str], Callable] = None,
|
80
|
+
files_data: Dict[str, SimpleUploadedFile] = None,
|
81
|
+
raise_error: bool = False,
|
82
|
+
reverse_kwargs: Union[Dict, Callable] = None,
|
83
|
+
request_kwargs: Dict = None,
|
84
|
+
view_kwargs: Union[Dict, Callable] = None,
|
85
|
+
validators=None,
|
86
|
+
headers: Optional[Dict[str, str]] = None,
|
87
|
+
**response_attr,
|
88
|
+
):
|
89
|
+
"""Initialize the object."""
|
90
|
+
self.method = method.lower()
|
91
|
+
self.form_data = form_data
|
92
|
+
self.headers = headers or {}
|
93
|
+
self.post_data = post_data
|
94
|
+
self.files_data = files_data or {}
|
95
|
+
self.expected_response = expected_response
|
96
|
+
self.raise_error = raise_error
|
97
|
+
self.request_kwargs = request_kwargs or {} # extra kwargs for building the HttpRequest
|
98
|
+
self.reverse_kwargs = (
|
99
|
+
reverse_kwargs or {}
|
100
|
+
) # extra kwargs provided to the callable and to the `reverse` function
|
101
|
+
self.view_kwargs = view_kwargs or {} # extra kwargs provided to the callabble
|
102
|
+
self.get_data = get_data or {} # provide the GET QueryDict of the created HttpRequest
|
103
|
+
self.validators = validators or [] # list of callable(testCase, HttpResponse)
|
104
|
+
self.response_attr = response_attr # extra response attrs
|
105
|
+
self.request = None
|
106
|
+
|
107
|
+
@staticmethod
|
108
|
+
def callable_value(value, test_case: TestCase, complete_url_name: str, user: AbstractUser):
|
109
|
+
"""Call the callable if possible, otherwise return the value."""
|
110
|
+
if callable(value):
|
111
|
+
return value(test_case, complete_url_name, user)
|
112
|
+
return value
|
113
|
+
|
114
|
+
# noinspection PyUnusedLocal
|
115
|
+
def get_request_kwargs(self, test_case: TestCase, complete_url_name: str, user: AbstractUser) -> Dict:
|
116
|
+
"""Build all kwargs provided to the request factory."""
|
117
|
+
merged_request_kwargs = {}
|
118
|
+
merged_request_kwargs.update(self.request_kwargs)
|
119
|
+
if self.form_data:
|
120
|
+
merged_request_kwargs["form_data"] = self.callable_value(self.form_data, test_case, complete_url_name, user)
|
121
|
+
if self.get_data:
|
122
|
+
merged_request_kwargs["get_data"] = self.callable_value(self.get_data, test_case, complete_url_name, user)
|
123
|
+
if self.post_data:
|
124
|
+
merged_request_kwargs["post_data"] = self.callable_value(self.post_data, test_case, complete_url_name, user)
|
125
|
+
if self.headers:
|
126
|
+
merged_request_kwargs["headers"] = self.headers
|
127
|
+
return merged_request_kwargs
|
128
|
+
|
129
|
+
def get_request_method(self, test_case: TestCase, complete_url_name: str, user: AbstractUser) -> str:
|
130
|
+
"""Return the HTTP method."""
|
131
|
+
return self.callable_value(self.method, test_case, complete_url_name, user)
|
132
|
+
|
133
|
+
def get_view_kwargs(self, test_case: TestCase, complete_url_name: str, user: AbstractUser) -> Dict:
|
134
|
+
"""Return extra kwargs provided to the view."""
|
135
|
+
return self.callable_value(self.view_kwargs, test_case, complete_url_name, user)
|
136
|
+
|
137
|
+
def get_reverse_kwargs(self, test_case: TestCase, complete_url_name: str, user: AbstractUser) -> Dict:
|
138
|
+
"""Return the kwargs used for reversing the URL."""
|
139
|
+
return self.callable_value(self.reverse_kwargs, test_case, complete_url_name, user)
|
140
|
+
|
141
|
+
def get_http_request(self, test_case, view_name: str, reverse_kwargs: Dict, user: AbstractUser) -> HttpRequest:
|
142
|
+
"""Return the HttpRequest."""
|
143
|
+
if view_name:
|
144
|
+
url = reverse(view_name, kwargs=reverse_kwargs)
|
145
|
+
else:
|
146
|
+
url = "/unnamed/view"
|
147
|
+
method = self.get_request_method(test_case, view_name, user)
|
148
|
+
request_kwargs = self.get_request_kwargs(test_case, view_name, user)
|
149
|
+
request = test_case.create_request(url, method=method, **request_kwargs)
|
150
|
+
if self.files_data:
|
151
|
+
request.FILES.update(self.files_data)
|
152
|
+
request.user = user
|
153
|
+
return request
|
154
|
+
|
155
|
+
def evaluate(
|
156
|
+
self,
|
157
|
+
test_case,
|
158
|
+
view_name: str,
|
159
|
+
view: callable,
|
160
|
+
reverse_kwargs: Dict,
|
161
|
+
view_kwargs: Dict,
|
162
|
+
user: AbstractUser,
|
163
|
+
) -> Optional[Exception]:
|
164
|
+
"""Evaluate a single view with the provided reverse kwargs and view kwargs.
|
165
|
+
|
166
|
+
:param test_case: the test case evaluating this function (maybe some attributes are required)
|
167
|
+
:param view_name: the name of the tested view
|
168
|
+
:param view: the tested view itself
|
169
|
+
:param reverse_kwargs: kwargs used for the `reverse` function (updated by `get_reverse_kwargs`)
|
170
|
+
:param view_kwargs: extra args passed to the view (updated by `get_view_kwargs`)
|
171
|
+
:param user: the tested permission set
|
172
|
+
"""
|
173
|
+
merged_reverse_kwargs = {}
|
174
|
+
merged_reverse_kwargs.update(reverse_kwargs)
|
175
|
+
merged_reverse_kwargs.update(self.get_reverse_kwargs(test_case, view_name, user))
|
176
|
+
|
177
|
+
request = self.get_http_request(test_case, view_name, merged_reverse_kwargs, user)
|
178
|
+
self.request = request
|
179
|
+
|
180
|
+
merged_view_kwargs = merged_reverse_kwargs
|
181
|
+
merged_view_kwargs.update(view_kwargs)
|
182
|
+
merged_view_kwargs.update(self.get_view_kwargs(test_case, view_name, user))
|
183
|
+
if self.raise_error:
|
184
|
+
self.check_response(test_case, request, view, merged_view_kwargs, user)
|
185
|
+
else:
|
186
|
+
try:
|
187
|
+
self.check_response(test_case, request, view, merged_view_kwargs, user)
|
188
|
+
except Exception as e:
|
189
|
+
if not isinstance(e, PermissionDenied) and not isinstance(e, Http404):
|
190
|
+
traceback.print_exc()
|
191
|
+
return e
|
192
|
+
|
193
|
+
def check_response(
|
194
|
+
self,
|
195
|
+
test_case: TestCase,
|
196
|
+
request: HttpRequest,
|
197
|
+
view: callable,
|
198
|
+
view_kwargs: Dict,
|
199
|
+
user: AbstractUser,
|
200
|
+
):
|
201
|
+
"""Check if the response meets the expectation."""
|
202
|
+
expected_response = self.expected_response
|
203
|
+
# noinspection PyUnusedLocal
|
204
|
+
user = user
|
205
|
+
if isinstance(expected_response, type) and issubclass(expected_response, Exception):
|
206
|
+
test_case.assertRaises(expected_response, lambda: view(request, **view_kwargs))
|
207
|
+
return
|
208
|
+
http_response = view(request, **view_kwargs)
|
209
|
+
if isinstance(http_response, TemplateResponse):
|
210
|
+
http_response.render()
|
211
|
+
for validator in self.validators:
|
212
|
+
validator(test_case, http_response)
|
213
|
+
if isinstance(expected_response, int):
|
214
|
+
if expected_response != http_response.status_code:
|
215
|
+
# do not use test_case.assertEqual to have a more explicit error
|
216
|
+
msg = f"HTTP {http_response.status_code} (while expecting {expected_response})"
|
217
|
+
msg += self.get_description(request)
|
218
|
+
raise AssertionError(msg)
|
219
|
+
elif isinstance(expected_response, HttpResponse):
|
220
|
+
if expected_response.status_code != http_response.status_code:
|
221
|
+
# do not use test_case.assertEqual to have a more explicit error
|
222
|
+
msg = f"HTTP {http_response.status_code} (while expecting {expected_response.status_code})"
|
223
|
+
msg += self.get_description(request)
|
224
|
+
raise AssertionError(msg)
|
225
|
+
test_case.assertIsInstance(http_response, expected_response.__class__)
|
226
|
+
elif callable(expected_response):
|
227
|
+
expected_response(test_case, http_response)
|
228
|
+
else:
|
229
|
+
raise ValueError(f"unknown type of expected response: {expected_response!r}")
|
230
|
+
for attr_name, attr_value in self.response_attr.items():
|
231
|
+
if isinstance(attr_value, type):
|
232
|
+
test_case.assertIsInstance(attr_value, getattr(http_response, attr_name))
|
233
|
+
else:
|
234
|
+
test_case.assertEqual(attr_value, getattr(http_response, attr_name))
|
235
|
+
|
236
|
+
def get_description(self, request: HttpRequest) -> str:
|
237
|
+
"""Return a string representation of a RequestTester."""
|
238
|
+
msg = f" url = {request.path}"
|
239
|
+
if self.method != "get":
|
240
|
+
msg += f" method={self.method}"
|
241
|
+
if self.headers:
|
242
|
+
msg += f" headers={self.headers}"
|
243
|
+
if self.post_data:
|
244
|
+
post_data = str(self.post_data)[:30]
|
245
|
+
msg += f" post_data={post_data}"
|
246
|
+
if self.form_data or self.get_data:
|
247
|
+
post_data = str(self.form_data)[:30]
|
248
|
+
msg += f" form_data={post_data}"
|
249
|
+
if self.get_data:
|
250
|
+
post_data = str(self.get_data)[:30]
|
251
|
+
msg += f" get_data={post_data}"
|
252
|
+
return msg
|
253
|
+
|
254
|
+
|
255
|
+
class HTMLFormRequestTester(RequestTester):
|
256
|
+
"""Validate a form in the HTML response, for example to check if required fields are present."""
|
257
|
+
|
258
|
+
def __init__(
|
259
|
+
self,
|
260
|
+
required_fields: Set[str],
|
261
|
+
*args,
|
262
|
+
form_id: Optional[str] = None,
|
263
|
+
ignored_fields: Optional[Set[str]] = None,
|
264
|
+
ignored_types: Optional[Set[str]] = None,
|
265
|
+
**kwargs,
|
266
|
+
):
|
267
|
+
"""Initialize the object with expected values."""
|
268
|
+
self.required_fields = required_fields
|
269
|
+
kwargs.setdefault("validators", [])
|
270
|
+
kwargs["validators"].append(self.check_required_fields)
|
271
|
+
self.form_id = form_id
|
272
|
+
self.ignored_fields = ignored_fields
|
273
|
+
self.ignored_types = ignored_types
|
274
|
+
super().__init__(*args, **kwargs)
|
275
|
+
|
276
|
+
def check_required_fields(self, test_case: TestCase, response: HttpResponse):
|
277
|
+
"""Check if the response contains the expected form values."""
|
278
|
+
parser = FormHTMLParser(
|
279
|
+
form_id=self.form_id,
|
280
|
+
ignored_types=self.ignored_types,
|
281
|
+
ignored_fields=self.ignored_fields,
|
282
|
+
)
|
283
|
+
parser.feed(response.content.decode())
|
284
|
+
test_case.assertEqual(self.required_fields, parser.form_fields)
|
285
|
+
|
286
|
+
|
287
|
+
class JsonValidator:
|
288
|
+
"""Validate a JSONResponse."""
|
289
|
+
|
290
|
+
def __init__(self, value):
|
291
|
+
"""Initialize the object with the expected JSON."""
|
292
|
+
self.value = value
|
293
|
+
|
294
|
+
def __call__(self, test_case: TestCase, response: JsonResponse):
|
295
|
+
"""Check the content of a JsonResponse."""
|
296
|
+
content = json.loads(response.content)
|
297
|
+
if self.value != content:
|
298
|
+
print(content)
|
299
|
+
test_case.assertEqual(self.value, content)
|