Skip to content

Commit df847c9

Browse files
kennethlovemelaniearbor
andauthoredNov 18, 2021
Interrogate (#286)
Co-authored-by: Melanie Crutchfield <[email protected]>
1 parent b465915 commit df847c9

17 files changed

+530
-359
lines changed
 

‎.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ jobs:
3434
flags: unittests
3535
name: codecov-umbrella
3636
verbose: true
37+
- name: Python Interrogate Check
38+
uses: JackMcKew/python-interrogate-check@main
39+
with:
40+
path: 'braces'

‎braces/views/_access.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def get_redirect_field_name(self):
5858
return self.redirect_field_name
5959

6060
def handle_no_permission(self, request):
61+
"""What should happen if the user doesn't have permission?"""
6162
if self.raise_exception:
6263
if (
6364
self.redirect_unauthenticated_users
@@ -102,6 +103,7 @@ class LoginRequiredMixin(AccessMixin):
102103
"""
103104

104105
def dispatch(self, request, *args, **kwargs):
106+
"""Call the appropriate method after checking authentication"""
105107
if not request.user.is_authenticated:
106108
return self.handle_no_permission(request)
107109

@@ -127,6 +129,7 @@ class SomeView(AnonymousRequiredMixin, ListView):
127129
authenticated_redirect_url = settings.LOGIN_REDIRECT_URL
128130

129131
def dispatch(self, request, *args, **kwargs):
132+
"""Call the appropriate handler after guaranteeing anonymity"""
130133
if request.user.is_authenticated:
131134
return HttpResponseRedirect(self.get_authenticated_redirect_url())
132135
return super().dispatch(request, *args, **kwargs)
@@ -263,10 +266,12 @@ class SomeView(MultiplePermissionsRequiredMixin, ListView):
263266
permissions = None # Default required perms to none
264267

265268
def get_permission_required(self, request=None):
269+
"""Get which permission is required"""
266270
self._check_permissions_attr()
267271
return self.permissions
268272

269273
def check_permissions(self, request):
274+
"""Get the permissions, both all and any."""
270275
permissions = self.get_permission_required(request)
271276
perms_all = permissions.get("all")
272277
perms_any = permissions.get("any")
@@ -327,6 +332,7 @@ class GroupRequiredMixin(AccessMixin):
327332
group_required = None
328333

329334
def get_group_required(self):
335+
"""Get which group's membership is required"""
330336
if any([
331337
self.group_required is None,
332338
not isinstance(self.group_required, (list, tuple, str))
@@ -350,6 +356,7 @@ def check_membership(self, groups):
350356
return set(groups).intersection(set(user_groups))
351357

352358
def dispatch(self, request, *args, **kwargs):
359+
"""Call the appropriate handler if the user is a group member"""
353360
self.request = request
354361
in_group = False
355362
if request.user.is_authenticated:
@@ -374,15 +381,18 @@ class UserPassesTestMixin(AccessMixin):
374381
"""
375382

376383
def test_func(self, user):
384+
"""The function to test the user with"""
377385
raise NotImplementedError(
378386
f"{self._class_name} is missing implementation of the "
379387
"`test_func` method. A function to test the user is required."
380388
)
381389

382390
def get_test_func(self):
391+
"""Get the test function"""
383392
return getattr(self, "test_func")
384393

385394
def dispatch(self, request, *args, **kwargs):
395+
"""Call the appropriate handler if the users passes the test"""
386396
user_test_result = self.get_test_func()(request.user)
387397

388398
if not user_test_result:
@@ -397,6 +407,7 @@ class SuperuserRequiredMixin(AccessMixin):
397407
"""
398408

399409
def dispatch(self, request, *args, **kwargs):
410+
"""Call the appropriate handler if the user is a superuser"""
400411
if not request.user.is_superuser:
401412
return self.handle_no_permission(request)
402413

@@ -409,6 +420,7 @@ class StaffuserRequiredMixin(AccessMixin):
409420
"""
410421

411422
def dispatch(self, request, *args, **kwargs):
423+
"""Call the appropriate handler if the user is a staff member"""
412424
if not request.user.is_staff:
413425
return self.handle_no_permission(request)
414426

@@ -423,6 +435,7 @@ class SSLRequiredMixin:
423435
raise_exception = False
424436

425437
def dispatch(self, request, *args, **kwargs):
438+
"""Call the appropriate handler if the connection is secure"""
426439
if getattr(settings, "DEBUG", False):
427440
# Don't enforce the check during development
428441
return super().dispatch(request, *args, **kwargs)
@@ -453,6 +466,7 @@ class RecentLoginRequiredMixin(LoginRequiredMixin):
453466
max_last_login_delta = 1800 # Defaults to 30 minutes
454467

455468
def dispatch(self, request, *args, **kwargs):
469+
"""Call the appropriate method if the user's login is recent"""
456470
resp = super().dispatch(request, *args, **kwargs)
457471

458472
if resp.status_code == 200:

‎braces/views/_ajax.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class JSONResponseMixin:
1818
json_encoder_class = None
1919

2020
def get_content_type(self):
21+
"""Get the appropriate content type for the response"""
2122
if self.content_type is not None and not isinstance(
2223
self.content_type, str
2324
):
@@ -29,11 +30,13 @@ def get_content_type(self):
2930
return self.content_type or "application/json"
3031

3132
def get_json_dumps_kwargs(self):
33+
"""Get kwargs for custom JSON compilation"""
3234
dumps_kwargs = getattr(self, "json_dumps_kwargs", None) or {}
3335
dumps_kwargs.setdefault("ensure_ascii", False)
3436
return dumps_kwargs
3537

3638
def get_json_encoder_class(self):
39+
"""Get the encoder class to use"""
3740
if self.json_encoder_class is None:
3841
self.json_encoder_class = DjangoJSONEncoder
3942
return self.json_encoder_class
@@ -74,6 +77,7 @@ class AjaxResponseMixin:
7477
"""
7578

7679
def dispatch(self, request, *args, **kwargs):
80+
"""Call the appropriate handler method"""
7781
if all([
7882
request.headers.get("x-requested-with") == "XMLHttpRequest",
7983
request.method.lower() in self.http_method_names
@@ -91,15 +95,19 @@ def dispatch(self, request, *args, **kwargs):
9195
return super().dispatch(request, *args, **kwargs)
9296

9397
def get_ajax(self, request, *args, **kwargs):
98+
"""Handle a GET request made with AJAX"""
9499
return self.get(request, *args, **kwargs)
95100

96101
def post_ajax(self, request, *args, **kwargs):
102+
"""Handle a POST request made with AJAX"""
97103
return self.post(request, *args, **kwargs)
98104

99105
def put_ajax(self, request, *args, **kwargs):
106+
"""Handle a PUT request made with AJAX"""
100107
return self.get(request, *args, **kwargs)
101108

102109
def delete_ajax(self, request, *args, **kwargs):
110+
"""Handle a DELETE request made with AJAX"""
103111
return self.get(request, *args, **kwargs)
104112

105113

@@ -129,6 +137,7 @@ def post(self, request, *args, **kwargs):
129137
error_response_dict = {"errors": ["Improperly formatted request"]}
130138

131139
def render_bad_request_response(self, error_dict=None):
140+
"""Generate errors for bad content"""
132141
if error_dict is None:
133142
error_dict = self.error_response_dict
134143
json_context = json.dumps(
@@ -141,12 +150,14 @@ def render_bad_request_response(self, error_dict=None):
141150
)
142151

143152
def get_request_json(self):
153+
"""Get the JSON included in the body"""
144154
try:
145155
return json.loads(self.request.body.decode("utf-8"))
146156
except (json.JSONDecodeError, ValueError):
147157
return None
148158

149159
def dispatch(self, request, *args, **kwargs):
160+
"""Trigger the appropriate method"""
150161
self.request = request
151162
self.args = args
152163
self.kwargs = kwargs
@@ -164,4 +175,4 @@ def dispatch(self, request, *args, **kwargs):
164175

165176

166177
class JSONRequestResponseMixin(JsonRequestResponseMixin):
167-
pass
178+
"""Convenience alias"""

‎braces/views/_other.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ class SetHeadlineMixin:
1414
headline = None # Default the headline to none
1515

1616
def get_context_data(self, **kwargs):
17+
"""Add the headline to the context"""
1718
kwargs = super().get_context_data(**kwargs)
1819
kwargs.update({"headline": self.get_headline()})
1920
return kwargs
2021

2122
def get_headline(self):
23+
"""Fetch the headline from the instance"""
2224
if self.headline is None:
2325
class_name = self.__class__.__name__
2426
raise ImproperlyConfigured(
@@ -36,6 +38,7 @@ class StaticContextMixin:
3638
static_context = None
3739

3840
def get_context_data(self, **kwargs):
41+
"""Update the context to include the static content"""
3942
kwargs = super().get_context_data(**kwargs)
4043

4144
try:
@@ -49,6 +52,7 @@ def get_context_data(self, **kwargs):
4952
return kwargs
5053

5154
def get_static_context(self):
55+
"""Fetch the static content from the view"""
5256
if self.static_context is None:
5357
class_name = self.__class__.__name__
5458
raise ImproperlyConfigured(
@@ -68,28 +72,29 @@ class CanonicalSlugDetailMixin:
6872
"""
6973

7074
def dispatch(self, request, *args, **kwargs):
71-
# Set up since we need to super() later instead of earlier.
75+
"""
76+
Redirect to the appropriate URL if necessary.
77+
Otherwise, trigger HTTP-method-appropriate handler.
78+
"""
7279
self.request = request
7380
self.args = args
7481
self.kwargs = kwargs
7582

76-
# Get the current object, url slug, and
77-
# urlpattern name (namespace aware).
83+
# Get the current object, url slug, and url name.
7884
obj = self.get_object()
7985
slug = self.kwargs.get(self.slug_url_kwarg, None)
8086
match = resolve(request.path_info)
8187
url_parts = match.namespaces
8288
url_parts.append(match.url_name)
8389
current_urlpattern = ":".join(url_parts)
8490

85-
# Figure out what the slug is supposed to be.
91+
# Find the canonical slug for the object
8692
if hasattr(obj, "get_canonical_slug"):
8793
canonical_slug = obj.get_canonical_slug()
8894
else:
8995
canonical_slug = self.get_canonical_slug()
9096

91-
# If there's a discrepancy between the slug in the url and the
92-
# canonical slug, redirect to the canonical slug.
97+
# Redirect if current slug is not the canonical one
9398
if canonical_slug != slug:
9499
params = {
95100
self.pk_url_kwarg: obj.pk,
@@ -102,17 +107,14 @@ def dispatch(self, request, *args, **kwargs):
102107

103108
def get_canonical_slug(self):
104109
"""
105-
Override this method to customize what slug should be considered
106-
canonical.
107-
108-
Alternatively, define the get_canonical_slug method on this view's
109-
object class. In that case, this method will never be called.
110+
Provide a method to return the correct slug for this object.
110111
"""
111112
return self.get_object().slug
112113

113114

114115
class AllVerbsMixin:
115-
"""Call a single method for all HTTP verbs.
116+
"""
117+
Call a single method for all HTTP verbs.
116118
117119
The name of the method should be specified using the class attribute
118120
`all_handler`. The default value of this attribute is 'all'.
@@ -121,6 +123,7 @@ class AllVerbsMixin:
121123
all_handler = "all"
122124

123125
def dispatch(self, request, *args, **kwargs):
126+
"""Call the all handler"""
124127
if not self.all_handler:
125128
raise ImproperlyConfigured(
126129
f"{self.__class__.__name__} requires the all_handler attribute to be set."
@@ -178,6 +181,7 @@ class CacheControlMixin:
178181

179182
@classmethod
180183
def get_cachecontrol_options(cls):
184+
"""Compile a dictionary of selected cache options"""
181185
opts = (
182186
'public', 'private', 'no_cache', 'no_transform',
183187
'must_revalidate', 'proxy_revalidate', 'max_age',
@@ -192,6 +196,7 @@ def get_cachecontrol_options(cls):
192196

193197
@classmethod
194198
def as_view(cls, *args, **kwargs):
199+
"""Wrap the view with appropriate cache controls"""
195200
view_func = super().as_view(*args, **kwargs)
196201
options = cls.get_cachecontrol_options()
197202
return cache_control(**options)(view_func)
@@ -204,5 +209,8 @@ class NeverCacheMixin:
204209
"""
205210
@classmethod
206211
def as_view(cls, *args, **kwargs):
212+
"""
213+
Wrap the view with the `never_cache` decorator.
214+
"""
207215
view_func = super().as_view(*args, **kwargs)
208216
return never_cache(view_func)

‎braces/views/_queries.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class SelectRelatedMixin:
1111
select_related = None # Default related fields to none
1212

1313
def get_queryset(self):
14+
"""Apply select_related, with appropriate fields, to the queryset"""
1415
if self.select_related is None:
1516
# If no fields were provided, raise a configuration error
1617
raise ImproperlyConfigured(
@@ -43,6 +44,7 @@ class PrefetchRelatedMixin:
4344
prefetch_related = None # Default prefetch fields to none
4445

4546
def get_queryset(self):
47+
"""Apply prefetch_related, with appropriate fields, to the queryset"""
4648
if self.prefetch_related is None:
4749
# If no fields were provided, raise a configuration error
4850
raise ImproperlyConfigured(
@@ -90,20 +92,23 @@ def get_context_data(self, **kwargs):
9092
return context
9193

9294
def get_orderable_columns(self):
95+
"""Check that the orderable columns are set and return them"""
9396
if not self.orderable_columns:
9497
raise ImproperlyConfigured(
9598
f"{self.__class__.__name__} needs the ordering columns defined."
9699
)
97100
return self.orderable_columns
98101

99102
def get_orderable_columns_default(self):
103+
"""Which column(s) should be sorted by, by default?"""
100104
if not self.orderable_columns_default:
101105
raise ImproperlyConfigured(
102106
f"{self.__class__.__name__} needs the default ordering column defined."
103107
)
104108
return self.orderable_columns_default
105109

106110
def get_ordering_default(self):
111+
"""Which direction should things be sorted?"""
107112
if not self.ordering_default:
108113
return "asc"
109114
else:

‎conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44

55

66
def pytest_configure():
7+
"""Setup Django settings"""
78
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
89
settings.configure(default_settings=test_settings)

‎pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,22 @@ line-length = 79
77

88
[tool.pytest.ini_options]
99
addopts = "--cov --nomigrations"
10+
11+
[tool.interrogate]
12+
ignore-init-method = true
13+
ignore-init-module = false
14+
ignore-magic = false
15+
ignore-semiprivate = false
16+
ignore-private = false
17+
ignore-property-decorators = false
18+
ignore-module = true
19+
ignore-nested-functions = false
20+
ignore-nested-classes = true
21+
fail-under = 75
22+
exclude = ["setup.py", "conftest.py", "docs", "build"]
23+
ignore-regex = ["^get$", "^mock_.*"]
24+
# possible values: 0 (minimal output), 1 (-v), 2 (-vv)
25+
verbose = 1
26+
quiet = false
27+
color = true
28+
omit-covered-files = true

‎setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
def _add_default(m):
9+
"""Add on a default"""
910
attr_name, attr_value = m.groups()
1011
return ((attr_name, attr_value.strip("\"'")),)
1112

‎tests/factories.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,30 @@ def _get_perm(perm_name):
1818

1919

2020
class ArticleFactory(factory.django.DjangoModelFactory):
21-
title = factory.Sequence(lambda n: "Article number {0}".format(n))
22-
body = factory.Sequence(lambda n: "Body of article {0}".format(n))
21+
"""Generates Articles"""
22+
title = factory.Sequence(lambda n: f"Article number {n}")
23+
body = factory.Sequence(lambda n: "Body of article {n}")
2324

2425
class Meta:
2526
model = Article
2627
abstract = False
2728

2829

2930
class GroupFactory(factory.django.DjangoModelFactory):
30-
name = factory.Sequence(lambda n: "group{0}".format(n))
31+
"""Artificial divides as a service"""
32+
name = factory.Sequence(lambda n: f"group{n}")
3133

3234
class Meta:
3335
model = Group
3436
abstract = False
3537

3638

3739
class UserFactory(factory.django.DjangoModelFactory):
38-
username = factory.Sequence(lambda n: "user{0}".format(n))
39-
first_name = factory.Sequence(lambda n: "John {0}".format(n))
40-
last_name = factory.Sequence(lambda n: "Doe {0}".format(n))
41-
email = factory.Sequence(lambda n: "user{0}@example.com".format(n))
40+
"""The people who make it all possible"""
41+
username = factory.Sequence(lambda n: f"user{n}")
42+
first_name = factory.Sequence(lambda n: f"John {n}")
43+
last_name = factory.Sequence(lambda n: f"Doe {n}")
44+
email = factory.Sequence(lambda n: f"user{n}@example.com")
4245
password = factory.PostGenerationMethodCall("set_password", "asdf1234")
4346

4447
class Meta:
@@ -47,6 +50,7 @@ class Meta:
4750

4851
@factory.post_generation
4952
def permissions(self, create, extracted, **kwargs):
53+
"""Give the user some permissions"""
5054
if create and extracted:
5155
# We have a saved object and a list of permission names
5256
self.user_permissions.add(*[_get_perm(pn) for pn in extracted])

‎tests/forms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77

88
class FormWithUserKwarg(UserKwargModelFormMixin, forms.Form):
9+
"""This form will get a `user` kwarg"""
910
field1 = forms.CharField()
1011

1112

1213
class ArticleForm(forms.ModelForm):
14+
"""This form represents an Article"""
1315
class Meta:
1416
model = Article
1517
fields = ["author", "title", "body", "slug"]

‎tests/helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.core.serializers.json import DjangoJSONEncoder
44

55

6-
class TestViewHelper(object):
6+
class TestViewHelper:
77
"""
88
Helper class for unit-testing class based views.
99
"""
@@ -12,7 +12,7 @@ class TestViewHelper(object):
1212
request_factory_class = test.RequestFactory
1313

1414
def setUp(self):
15-
super(TestViewHelper, self).setUp()
15+
super().setUp()
1616
self.factory = self.request_factory_class()
1717

1818
def build_request(self, method="GET", path="/test/", user=None, **kwargs):
@@ -61,6 +61,7 @@ class SetJSONEncoder(DjangoJSONEncoder):
6161
"""
6262

6363
def default(self, obj):
64+
"""Control default methods of encoding data"""
6465
if isinstance(obj, set):
6566
return list(obj)
6667
return super(DjangoJSONEncoder, self).default(obj)

‎tests/models.py

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

33

44
class Article(models.Model):
5+
"""
6+
A small but useful model for testing most features
7+
"""
58
author = models.ForeignKey(
69
"auth.User", null=True, blank=True, on_delete=models.CASCADE
710
)
@@ -11,6 +14,9 @@ class Article(models.Model):
1114

1215

1316
class CanonicalArticle(models.Model):
17+
"""
18+
Model specifically for testing the canonical slug mixins
19+
"""
1420
author = models.ForeignKey(
1521
"auth.User", null=True, blank=True, on_delete=models.CASCADE
1622
)
@@ -19,6 +25,7 @@ class CanonicalArticle(models.Model):
1925
slug = models.SlugField(blank=True)
2026

2127
def get_canonical_slug(self):
28+
"""Required by mixin to use the model as the source of truth"""
2229
if self.author:
23-
return "{0.author.username}-{0.slug}".format(self)
24-
return "unauthored-{0.slug}".format(self)
30+
return f"{self.author.username}-{self.slug}"
31+
return f"unauthored-{self.slug}"

‎tests/test_access_mixins.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -235,31 +235,33 @@ def test_redirect_unauthenticated_false(self):
235235

236236
@pytest.mark.django_db
237237
class TestLoginRequiredMixin(TestViewHelper, test.TestCase):
238-
"""
239-
Tests for LoginRequiredMixin.
240-
"""
238+
"""Scenarios around requiring an authenticated session"""
241239

242240
view_class = LoginRequiredView
243241
view_url = "/login_required/"
244242

245243
def test_anonymous(self):
244+
"""Anonymous users should be redirected"""
246245
resp = self.client.get(self.view_url)
247246
self.assertRedirects(resp, "/accounts/login/?next=/login_required/")
248247

249248
def test_anonymous_raises_exception(self):
249+
"""Anonymous users should raise an exception"""
250250
with self.assertRaises(PermissionDenied):
251251
self.dispatch_view(
252252
self.build_request(path=self.view_url), raise_exception=True
253253
)
254254

255255
def test_authenticated(self):
256+
"""Authenticated users should get 'OK'"""
256257
user = UserFactory()
257258
self.client.login(username=user.username, password="asdf1234")
258259
resp = self.client.get(self.view_url)
259260
assert resp.status_code == 200
260261
assert force_str(resp.content) == "OK"
261262

262263
def test_anonymous_redirects(self):
264+
"""Anonymous users are redirected with a 302"""
263265
resp = self.dispatch_view(
264266
self.build_request(path=self.view_url),
265267
raise_exception=True,
@@ -361,7 +363,7 @@ def test_anonymous(self):
361363
def test_authenticated(self):
362364
"""
363365
Check that the authenticated user has been successfully directed
364-
to the approparite view.
366+
to the appropriate view.
365367
"""
366368
user = UserFactory()
367369
self.client.login(username=user.username, password="asdf1234")
@@ -372,13 +374,15 @@ def test_authenticated(self):
372374
self.assertRedirects(resp, "/authenticated_view/")
373375

374376
def test_no_url(self):
377+
"""View should raise an exception if no URL is provided"""
375378
self.view_class.authenticated_redirect_url = None
376379
user = UserFactory()
377380
self.client.login(username=user.username, password="asdf1234")
378381
with self.assertRaises(ImproperlyConfigured):
379382
self.client.get(self.view_url)
380383

381384
def test_bad_url(self):
385+
"""Redirection can be misconfigured"""
382386
self.view_class.authenticated_redirect_url = "/epicfailurl/"
383387
user = UserFactory()
384388
self.client.login(username=user.username, password="asdf1234")
@@ -388,17 +392,17 @@ def test_bad_url(self):
388392

389393
@pytest.mark.django_db
390394
class TestPermissionRequiredMixin(_TestAccessBasicsMixin, test.TestCase):
391-
"""
392-
Tests for PermissionRequiredMixin.
393-
"""
395+
"""Scenarios around requiring a permission"""
394396

395397
view_class = PermissionRequiredView
396398
view_url = "/permission_required/"
397399

398400
def build_authorized_user(self):
401+
"""Create a user with permissions"""
399402
return UserFactory(permissions=["auth.add_user"])
400403

401404
def build_unauthorized_user(self):
405+
"""Create a user without permissions"""
402406
return UserFactory()
403407

404408
def test_invalid_permission(self):
@@ -414,10 +418,12 @@ def test_invalid_permission(self):
414418
class TestMultiplePermissionsRequiredMixin(
415419
_TestAccessBasicsMixin, test.TestCase
416420
):
421+
"""Scenarios around requiring multiple permissions"""
417422
view_class = MultiplePermissionsRequiredView
418423
view_url = "/multiple_permissions_required/"
419424

420425
def build_authorized_user(self):
426+
"""Get a user with permissions"""
421427
return UserFactory(
422428
permissions=[
423429
"tests.add_article",
@@ -427,6 +433,7 @@ def build_authorized_user(self):
427433
)
428434

429435
def build_unauthorized_user(self):
436+
"""Get a user without the important permissions"""
430437
return UserFactory(permissions=["tests.add_article"])
431438

432439
def test_redirects_to_login(self):
@@ -530,49 +537,61 @@ def test_any_permissions_key(self):
530537

531538
@pytest.mark.django_db
532539
class TestSuperuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase):
540+
"""Scenarios requiring a superuser"""
533541
view_class = SuperuserRequiredView
534542
view_url = "/superuser_required/"
535543

536544
def build_authorized_user(self):
545+
"""Make a superuser"""
537546
return UserFactory(is_superuser=True, is_staff=True)
538547

539548
def build_unauthorized_user(self):
549+
"""Make a non-superuser"""
540550
return UserFactory()
541551

542552

543553
@pytest.mark.django_db
544554
class TestStaffuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase):
555+
"""Scenarios requiring a staff user"""
545556
view_class = StaffuserRequiredView
546557
view_url = "/staffuser_required/"
547558

548559
def build_authorized_user(self):
560+
"""Hire a user"""
549561
return UserFactory(is_staff=True)
550562

551563
def build_unauthorized_user(self):
564+
"""Get a customer"""
552565
return UserFactory()
553566

554567

555568
@pytest.mark.django_db
556569
class TestGroupRequiredMixin(_TestAccessBasicsMixin, test.TestCase):
570+
"""Scenarios requiring membership in a certain group"""
571+
557572
view_class = GroupRequiredView
558573
view_url = "/group_required/"
559574

560575
def build_authorized_user(self):
576+
"""Get a user with the right group"""
561577
user = UserFactory()
562578
group = GroupFactory(name="test_group")
563579
user.groups.add(group)
564580
return user
565581

566582
def build_superuser(self):
583+
"""Get a superuser"""
567584
user = UserFactory()
568585
user.is_superuser = True
569586
user.save()
570587
return user
571588

572589
def build_unauthorized_user(self):
590+
"""Just a normal users, not super and no groups"""
573591
return UserFactory()
574592

575593
def test_with_string(self):
594+
"""A group name as a string should restrict access"""
576595
self.assertEqual("test_group", self.view_class.group_required)
577596
user = self.build_authorized_user()
578597
self.client.login(username=user.username, password="asdf1234")
@@ -581,6 +600,7 @@ def test_with_string(self):
581600
self.assertEqual("OK", force_str(resp.content))
582601

583602
def test_with_group_list(self):
603+
"""A list of group names should restrict access"""
584604
group_list = ["test_group", "editors"]
585605
# the test client will instantiate a new view on request, so we have to
586606
# modify the class variable (and restore it when the test finished)
@@ -595,13 +615,15 @@ def test_with_group_list(self):
595615
self.assertEqual("test_group", self.view_class.group_required)
596616

597617
def test_superuser_allowed(self):
618+
"""Superusers should always be allowed, regardless of group rules"""
598619
user = self.build_superuser()
599620
self.client.login(username=user.username, password="asdf1234")
600621
resp = self.client.get(self.view_url)
601622
self.assertEqual(200, resp.status_code)
602623
self.assertEqual("OK", force_str(resp.content))
603624

604625
def test_improperly_configured(self):
626+
"""No group(s) specified should raise ImproperlyConfigured"""
605627
view = self.view_class()
606628
view.group_required = None
607629
with self.assertRaises(ImproperlyConfigured):
@@ -612,6 +634,7 @@ def test_improperly_configured(self):
612634
view.get_group_required()
613635

614636
def test_with_unicode(self):
637+
"""Unicode in group names should restrict access"""
615638
self.view_class.group_required = "niño"
616639
self.assertEqual("niño", self.view_class.group_required)
617640

@@ -631,21 +654,25 @@ def test_with_unicode(self):
631654

632655
@pytest.mark.django_db
633656
class TestUserPassesTestMixin(_TestAccessBasicsMixin, test.TestCase):
657+
"""Scenarios requiring a user to pass a test"""
634658
view_class = UserPassesTestView
635659
view_url = "/user_passes_test/"
636660
view_not_implemented_class = UserPassesTestNotImplementedView
637661
view_not_implemented_url = "/user_passes_test_not_implemented/"
638662

639663
# for testing with passing and not passsing func_test
640664
def build_authorized_user(self, is_superuser=False):
665+
"""Get a test-passing user"""
641666
return UserFactory(
642667
is_superuser=is_superuser, is_staff=True, email="user@mydomain.com"
643668
)
644669

645670
def build_unauthorized_user(self):
671+
"""Get a blank user"""
646672
return UserFactory()
647673

648674
def test_with_user_pass(self):
675+
"""Valid username and password should pass the test"""
649676
user = self.build_authorized_user()
650677
self.client.login(username=user.username, password="asdf1234")
651678
resp = self.client.get(self.view_url)
@@ -654,19 +681,22 @@ def test_with_user_pass(self):
654681
self.assertEqual("OK", force_str(resp.content))
655682

656683
def test_with_user_not_pass(self):
684+
"""A failing user should be redirected"""
657685
user = self.build_authorized_user(is_superuser=True)
658686
self.client.login(username=user.username, password="asdf1234")
659687
resp = self.client.get(self.view_url)
660688

661689
self.assertRedirects(resp, "/accounts/login/?next=/user_passes_test/")
662690

663691
def test_with_user_raise_exception(self):
692+
"""PermissionDenied should be raised"""
664693
with self.assertRaises(PermissionDenied):
665694
self.dispatch_view(
666695
self.build_request(path=self.view_url), raise_exception=True
667696
)
668697

669698
def test_not_implemented(self):
699+
"""NotImplemented should be raised"""
670700
view = self.view_not_implemented_class()
671701
with self.assertRaises(NotImplementedError):
672702
view.dispatch(
@@ -677,11 +707,13 @@ def test_not_implemented(self):
677707

678708
@pytest.mark.django_db
679709
class TestSSLRequiredMixin(test.TestCase):
710+
"""Scenarios around requiring SSL"""
680711
view_class = SSLRequiredView
681712
view_url = "/sslrequired/"
682713

683714
def test_ssl_redirection(self):
684-
self.view_url = "https://testserver" + self.view_url
715+
"""Should redirect if not SSL"""
716+
self.view_url = f"https://testserver{self.view_url}"
685717
self.view_class.raise_exception = False
686718
resp = self.client.get(self.view_url)
687719
self.assertRedirects(resp, self.view_url, status_code=301)
@@ -690,17 +722,20 @@ def test_ssl_redirection(self):
690722
self.assertEqual("https", resp.request.get("wsgi.url_scheme"))
691723

692724
def test_raises_exception(self):
725+
"""Should return 404"""
693726
self.view_class.raise_exception = True
694727
resp = self.client.get(self.view_url)
695728
self.assertEqual(404, resp.status_code)
696729

697730
@override_settings(DEBUG=True)
698731
def test_debug_bypasses_redirect(self):
732+
"""Debug mode should not require SSL"""
699733
self.view_class.raise_exception = False
700734
resp = self.client.get(self.view_url)
701735
self.assertEqual(200, resp.status_code)
702736

703737
def test_https_does_not_redirect(self):
738+
"""SSL requests should not redirect"""
704739
self.view_class.raise_exception = False
705740
resp = self.client.get(self.view_url, secure=True)
706741
self.assertEqual(200, resp.status_code)
@@ -709,15 +744,14 @@ def test_https_does_not_redirect(self):
709744

710745
@pytest.mark.django_db
711746
class TestRecentLoginRequiredMixin(test.TestCase):
712-
"""
713-
Tests for RecentLoginRequiredMixin.
714-
"""
747+
""" Scenarios requiring a recent login"""
715748

716749
view_class = RecentLoginRequiredView
717750
recent_view_url = "/recent_login/"
718751
outdated_view_url = "/outdated_login/"
719752

720753
def test_recent_login(self):
754+
"""A recent login should get a 200"""
721755
self.view_class.max_last_login_delta = 1800
722756
last_login = datetime.datetime.now()
723757
last_login = make_aware(last_login, get_current_timezone())
@@ -728,6 +762,7 @@ def test_recent_login(self):
728762
assert force_str(resp.content) == "OK"
729763

730764
def test_outdated_login(self):
765+
"""An outdated login should get a 302"""
731766
self.view_class.max_last_login_delta = 0
732767
last_login = datetime.datetime.now() - datetime.timedelta(hours=2)
733768
last_login = make_aware(last_login, get_current_timezone())
@@ -737,8 +772,8 @@ def test_outdated_login(self):
737772
assert resp.status_code == 302
738773

739774
def test_not_logged_in(self):
775+
"""Anonymous requests should be handled appropriately"""
740776
last_login = datetime.datetime.now()
741777
last_login = make_aware(last_login, get_current_timezone())
742-
user = UserFactory(last_login=last_login)
743778
resp = self.client.get(self.recent_view_url)
744779
assert resp.status_code != 200

‎tests/test_other_mixins.py

Lines changed: 79 additions & 311 deletions
Large diffs are not rendered by default.

‎tests/test_queries.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
from unittest import mock
2+
import pytest
3+
4+
from django.core.exceptions import ImproperlyConfigured
5+
from django import test
6+
7+
from .helpers import TestViewHelper
8+
from .models import Article
9+
from .views import (
10+
ArticleListView,
11+
ArticleListViewWithCustomQueryset,
12+
AuthorDetailView,
13+
OrderableListView
14+
)
15+
16+
17+
class TestSelectRelatedMixin(TestViewHelper, test.TestCase):
18+
"""Scenarios related to adding select_related to queries"""
19+
view_class = ArticleListView
20+
21+
def test_missing_select_related(self):
22+
"""If select_related is unset, raise ImproperlyConfigured"""
23+
with self.assertRaises(ImproperlyConfigured):
24+
self.dispatch_view(self.build_request(), select_related=None)
25+
26+
def test_invalid_select_related(self):
27+
"""If select_related is not a list or tuple, raise ImproperlyConfigured"""
28+
with self.assertRaises(ImproperlyConfigured):
29+
self.dispatch_view(self.build_request(), select_related={"a": 1})
30+
31+
@mock.patch("django.db.models.query.QuerySet.select_related")
32+
def test_select_related_called(self, m):
33+
"""QuerySet.select_related should be called with the correct arguments"""
34+
qs = Article.objects.all()
35+
m.return_value = qs.select_related("author")
36+
qs.select_related = m
37+
m.reset_mock()
38+
39+
resp = self.dispatch_view(self.build_request())
40+
self.assertEqual(200, resp.status_code)
41+
m.assert_called_once_with("author")
42+
43+
@mock.patch("django.db.models.query.QuerySet.select_related")
44+
def test_select_related_keeps_select_related_from_queryset(self, m):
45+
"""
46+
Checks that an empty select_related attribute does not
47+
cancel a select_related provided by queryset.
48+
"""
49+
qs = Article.objects.all()
50+
qs.select_related = m
51+
m.reset_mock()
52+
53+
with pytest.warns(UserWarning):
54+
resp = self.dispatch_view(
55+
self.build_request(),
56+
view_class=ArticleListViewWithCustomQueryset,
57+
)
58+
self.assertEqual(200, resp.status_code)
59+
self.assertEqual(0, m.call_count)
60+
61+
62+
class TestPrefetchRelatedMixin(TestViewHelper, test.TestCase):
63+
"""Scenarios related to adding prefetch_related to queries"""
64+
view_class = AuthorDetailView
65+
66+
def test_missing_prefetch_related(self):
67+
"""If prefetch_related is missing/None, raise ImproperlyConfigured"""
68+
with self.assertRaises(ImproperlyConfigured):
69+
self.dispatch_view(self.build_request(), prefetch_related=None)
70+
71+
def test_invalid_prefetch_related(self):
72+
"""If prefetch_related is not a list or tuple, raise ImproperlyConfigured"""
73+
with self.assertRaises(ImproperlyConfigured):
74+
self.dispatch_view(self.build_request(), prefetch_related={"a": 1})
75+
76+
@mock.patch("django.db.models.query.QuerySet.prefetch_related")
77+
def test_prefetch_related_called(self, m):
78+
"""QuerySet.prefetch_related() should be called with correct arguments"""
79+
qs = Article.objects.all()
80+
m.return_value = qs.prefetch_related("article_set")
81+
qs.prefetch_related = m
82+
m.reset_mock()
83+
84+
resp = self.dispatch_view(self.build_request())
85+
self.assertEqual(200, resp.status_code)
86+
m.assert_called_once_with("article_set")
87+
88+
@mock.patch("django.db.models.query.QuerySet.prefetch_related")
89+
def test_prefetch_related_keeps_select_related_from_queryset(self, m):
90+
"""
91+
Checks that an empty prefetch_related attribute does not
92+
cancel a prefetch_related provided by queryset.
93+
"""
94+
qs = Article.objects.all()
95+
qs.prefetch_related = m
96+
m.reset_mock()
97+
98+
with pytest.warns(UserWarning):
99+
resp = self.dispatch_view(
100+
self.build_request(),
101+
view_class=ArticleListViewWithCustomQueryset,
102+
)
103+
self.assertEqual(200, resp.status_code)
104+
self.assertEqual(0, m.call_count)
105+
106+
107+
class TestOrderableListMixin(TestViewHelper, test.TestCase):
108+
"""Scenarios involving ordering records"""
109+
view_class = OrderableListView
110+
111+
def __make_test_articles(self):
112+
"""Generate a couple of articles"""
113+
a1 = Article.objects.create(title="Alpha", body="Zet")
114+
a2 = Article.objects.create(title="Zet", body="Alpha")
115+
return a1, a2
116+
117+
def test_correct_order(self):
118+
"""Valid column and order query arguments should order the objects"""
119+
a1, a2 = self.__make_test_articles()
120+
121+
resp = self.dispatch_view(
122+
self.build_request(path="?order_by=title&ordering=asc"),
123+
orderable_columns=None,
124+
get_orderable_columns=lambda: (
125+
"id",
126+
"title",
127+
),
128+
)
129+
self.assertEqual(list(resp.context_data["object_list"]), [a1, a2])
130+
131+
resp = self.dispatch_view(
132+
self.build_request(path="?order_by=id&ordering=desc"),
133+
orderable_columns=None,
134+
get_orderable_columns=lambda: (
135+
"id",
136+
"title",
137+
),
138+
)
139+
self.assertEqual(list(resp.context_data["object_list"]), [a2, a1])
140+
141+
def test_correct_order_with_default_ordering(self):
142+
"""A valid order_by query argument should sort the default direction"""
143+
a1, a2 = self.__make_test_articles()
144+
145+
resp = self.dispatch_view(
146+
self.build_request(path="?order_by=id"),
147+
orderable_columns=None,
148+
ordering_default=None,
149+
get_orderable_columns=lambda: (
150+
"id",
151+
"title",
152+
),
153+
)
154+
self.assertEqual(list(resp.context_data["object_list"]), [a1, a2])
155+
156+
resp = self.dispatch_view(
157+
self.build_request(path="?order_by=id"),
158+
orderable_columns=None,
159+
ordering_default="asc",
160+
get_orderable_columns=lambda: (
161+
"id",
162+
"title",
163+
),
164+
)
165+
self.assertEqual(list(resp.context_data["object_list"]), [a1, a2])
166+
167+
resp = self.dispatch_view(
168+
self.build_request(path="?order_by=id"),
169+
orderable_columns=None,
170+
ordering_default="desc",
171+
get_orderable_columns=lambda: (
172+
"id",
173+
"title",
174+
),
175+
)
176+
self.assertEqual(list(resp.context_data["object_list"]), [a2, a1])
177+
178+
def test_correct_order_with_param_not_default_ordering(self):
179+
"""
180+
Objects must be properly ordered if requested with valid column names
181+
and ordering option in the query params.
182+
In this case, the ordering_default will be overwritten.
183+
"""
184+
a1, a2 = self.__make_test_articles()
185+
186+
resp = self.dispatch_view(
187+
self.build_request(path="?order_by=id&ordering=asc"),
188+
orderable_columns=None,
189+
ordering_default="desc",
190+
get_orderable_columns=lambda: (
191+
"id",
192+
"title",
193+
),
194+
)
195+
self.assertEqual(list(resp.context_data["object_list"]), [a1, a2])
196+
197+
def test_correct_order_with_incorrect_default_ordering(self):
198+
"""
199+
Objects must be properly ordered if requested with valid column names
200+
and with the default ordering
201+
"""
202+
view = self.view_class()
203+
view.ordering_default = "improper_default_value"
204+
self.assertRaises(
205+
ImproperlyConfigured, lambda: view.get_ordering_default()
206+
)
207+
208+
def test_default_column(self):
209+
"""
210+
When no ordering specified in GET, use
211+
View.get_orderable_columns_default()
212+
"""
213+
a1, a2 = self.__make_test_articles()
214+
215+
resp = self.dispatch_view(self.build_request())
216+
self.assertEqual(list(resp.context_data["object_list"]), [a1, a2])
217+
218+
def test_get_orderable_columns_returns_correct_values(self):
219+
"""
220+
OrderableListMixin.get_orderable_columns() should return
221+
View.orderable_columns attribute by default or raise
222+
ImproperlyConfigured exception if the attribute is None
223+
"""
224+
view = self.view_class()
225+
self.assertEqual(view.get_orderable_columns(), view.orderable_columns)
226+
view.orderable_columns = None
227+
self.assertRaises(
228+
ImproperlyConfigured, lambda: view.get_orderable_columns()
229+
)
230+
231+
def test_get_orderable_columns_default_returns_correct_values(self):
232+
"""
233+
OrderableListMixin.get_orderable_columns_default() should return
234+
View.orderable_columns_default attribute by default or raise
235+
ImproperlyConfigured exception if the attribute is None
236+
"""
237+
view = self.view_class()
238+
self.assertEqual(
239+
view.get_orderable_columns_default(),
240+
view.orderable_columns_default,
241+
)
242+
view.orderable_columns_default = None
243+
self.assertRaises(
244+
ImproperlyConfigured, lambda: view.get_orderable_columns_default()
245+
)
246+
247+
def test_only_allowed_columns(self):
248+
"""
249+
If column is not in Model.Orderable.columns iterable, the objects
250+
should be ordered by default column.
251+
"""
252+
a1, a2 = self.__make_test_articles()
253+
254+
resp = self.dispatch_view(
255+
self.build_request(path="?order_by=body&ordering=asc"),
256+
orderable_columns_default=None,
257+
get_orderable_columns_default=lambda: "title",
258+
)
259+
self.assertEqual(list(resp.context_data["object_list"]), [a1, a2])

‎tests/urls_namespaced.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from django.urls import include, re_path
1+
from django.urls import re_path
22

33
from . import views
44

5+
56
urlpatterns = [
6-
# CanonicalSlugDetailMixin namespace tests
77
re_path(
88
r"^article/(?P<pk>\d+)-(?P<slug>[\w-]+)/$",
99
views.CanonicalSlugDetailView.as_view(),

‎tests/views.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,19 @@ class OkView(View):
2626
"""
2727

2828
def get(self, request):
29+
"""Everything is going to be OK"""
2930
return HttpResponse("OK")
3031

3132
def post(self, request):
33+
"""Get it?"""
3234
return self.get(request)
3335

3436
def put(self, request):
37+
"""Get it?"""
3538
return self.get(request)
3639

3740
def delete(self, request):
41+
"""Get it?"""
3842
return self.get(request)
3943

4044

@@ -67,15 +71,19 @@ class AjaxResponseView(views.AjaxResponseMixin, OkView):
6771
"""
6872

6973
def get_ajax(self, request):
74+
"""Everything will eventually be OK"""
7075
return HttpResponse("AJAX_OK")
7176

7277
def post_ajax(self, request):
78+
"""Get it?"""
7379
return self.get_ajax(request)
7480

7581
def put_ajax(self, request):
82+
"""Get it?"""
7683
return self.get_ajax(request)
7784

7885
def delete_ajax(self, request):
86+
"""Get it?"""
7987
return self.get_ajax(request)
8088

8189

@@ -85,6 +93,7 @@ class SimpleJsonView(views.JSONResponseMixin, View):
8593
"""
8694

8795
def get(self, request):
96+
"""Send back some JSON"""
8897
object = {"username": request.user.username}
8998
return self.render_json_response(object)
9099

@@ -98,6 +107,7 @@ class CustomJsonEncoderView(views.JSONResponseMixin, View):
98107
json_encoder_class = SetJSONEncoder
99108

100109
def get(self, request):
110+
"""Send back some JSON"""
101111
object = {"numbers": set([1, 2, 3])}
102112
return self.render_json_response(object)
103113

@@ -109,6 +119,7 @@ class SimpleJsonBadRequestView(views.JSONResponseMixin, View):
109119
"""
110120

111121
def get(self, request):
122+
"""Send back some JSON"""
112123
object = {"username": request.user.username}
113124
return self.render_json_response(object, status=400)
114125

@@ -120,6 +131,7 @@ class ArticleListJsonView(views.JSONResponseMixin, View):
120131
"""
121132

122133
def get(self, request):
134+
"""Send back some JSON"""
123135
queryset = Article.objects.all()
124136
return self.render_json_object_response(queryset, fields=("title",))
125137

@@ -130,6 +142,7 @@ class JsonRequestResponseView(views.JsonRequestResponseMixin, View):
130142
"""
131143

132144
def post(self, request):
145+
"""Send back some JSON"""
133146
return self.render_json_response(self.request_json)
134147

135148

@@ -142,6 +155,7 @@ class JsonBadRequestView(views.JsonRequestResponseMixin, View):
142155
require_json = True
143156

144157
def post(self, request, *args, **kwargs):
158+
"""Send back some JSON"""
145159
return self.render_json_response(self.request_json)
146160

147161

@@ -152,6 +166,7 @@ class JsonCustomBadRequestView(views.JsonRequestResponseMixin, View):
152166
"""
153167

154168
def post(self, request, *args, **kwargs):
169+
"""Handle the POST request"""
155170
if not self.request_json:
156171
return self.render_bad_request_response({"error": "you messed up"})
157172
return self.render_json_response(self.request_json)
@@ -229,7 +244,8 @@ class FormWithUserKwargView(views.UserFormKwargsMixin, FormView):
229244
template_name = "form.html"
230245

231246
def form_valid(self, form):
232-
return HttpResponse("username: %s" % form.user.username)
247+
"""A simple response to watch for"""
248+
return HttpResponse(f"username: {form.user.username}")
233249

234250

235251
class HeadlineView(views.SetHeadlineMixin, TemplateView):
@@ -265,6 +281,7 @@ class DynamicHeadlineView(views.SetHeadlineMixin, TemplateView):
265281
template_name = "blank.html"
266282

267283
def get_headline(self):
284+
"""Return the headline passed in via kwargs"""
268285
return self.kwargs["s"]
269286

270287

@@ -286,24 +303,26 @@ class MultiplePermissionsRequiredView(
286303

287304

288305
class SuperuserRequiredView(views.SuperuserRequiredMixin, OkView):
289-
pass
306+
"""Require a superuser"""
290307

291308

292309
class StaffuserRequiredView(views.StaffuserRequiredMixin, OkView):
293-
pass
310+
"""Require a user marked as `is_staff`"""
294311

295312

296313
class CsrfExemptView(views.CsrfExemptMixin, OkView):
297-
pass
314+
"""Ignore CSRF"""
298315

299316

300317
class AuthorDetailView(views.PrefetchRelatedMixin, ListView):
318+
"""A basic detail view to test prefetching"""
301319
model = User
302320
prefetch_related = ["article_set"]
303321
template_name = "blank.html"
304322

305323

306324
class OrderableListView(views.OrderableListMixin, ListView):
325+
"""A basic list view to test ordering the output"""
307326
model = Article
308327
orderable_columns = (
309328
"id",
@@ -313,35 +332,37 @@ class OrderableListView(views.OrderableListMixin, ListView):
313332

314333

315334
class CanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView):
335+
"""A basic detail view to test a canonical slug"""
316336
model = Article
317337
template_name = "blank.html"
318338

319339

320-
class OverriddenCanonicalSlugDetailView(
321-
views.CanonicalSlugDetailMixin, DetailView
322-
):
340+
class OverriddenCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView):
341+
"""A basic detail view to test an overridden slug"""
323342
model = Article
324343
template_name = "blank.html"
325344

326345
def get_canonical_slug(self):
346+
"""Give back a different, encoded slug. My slug secrets are safe"""
327347
return codecs.encode(self.get_object().slug, "rot_13")
328348

329349

330-
class CanonicalSlugDetailCustomUrlKwargsView(
331-
views.CanonicalSlugDetailMixin, DetailView
332-
):
350+
class CanonicalSlugDetailCustomUrlKwargsView(views.CanonicalSlugDetailMixin, DetailView):
351+
"""A basic detail view to test a slug with custom URL stuff"""
333352
model = Article
334353
template_name = "blank.html"
335354
pk_url_kwarg = "my_pk"
336355
slug_url_kwarg = "my_slug"
337356

338357

339358
class ModelCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView):
359+
"""A basic detail view to test a model with a canonical slug"""
340360
model = CanonicalArticle
341361
template_name = "blank.html"
342362

343363

344364
class FormMessagesView(views.FormMessagesMixin, CreateView):
365+
"""A basic form view to test valid/invalid messages"""
345366
form_class = ArticleForm
346367
form_invalid_message = _("Invalid")
347368
form_valid_message = _("Valid")
@@ -351,10 +372,12 @@ class FormMessagesView(views.FormMessagesMixin, CreateView):
351372

352373

353374
class GroupRequiredView(views.GroupRequiredMixin, OkView):
375+
"""Is everything OK in this group?"""
354376
group_required = "test_group"
355377

356378

357379
class UserPassesTestView(views.UserPassesTestMixin, OkView):
380+
"""Did I pass a test?"""
358381
def test_func(self, user):
359382
return (
360383
user.is_staff
@@ -366,6 +389,7 @@ def test_func(self, user):
366389
class UserPassesTestLoginRequiredView(
367390
views.LoginRequiredMixin, views.UserPassesTestMixin, OkView
368391
):
392+
"""Am I logged in _and_ passing a test?"""
369393
def test_func(self, user):
370394
return (
371395
user.is_staff
@@ -375,15 +399,18 @@ def test_func(self, user):
375399

376400

377401
class UserPassesTestNotImplementedView(views.UserPassesTestMixin, OkView):
402+
"""The test went missing?"""
378403
pass
379404

380405

381406
class AllVerbsView(views.AllVerbsMixin, View):
407+
"""I know, like, all the verbs"""
382408
def all(self, request, *args, **kwargs):
383409
return HttpResponse("All verbs return this!")
384410

385411

386412
class SSLRequiredView(views.SSLRequiredMixin, OkView):
413+
"""Speak friend and enter"""
387414
pass
388415

389416

@@ -394,13 +421,15 @@ class RecentLoginRequiredView(views.RecentLoginRequiredMixin, OkView):
394421

395422

396423
class AttributeHeaderView(views.HeaderMixin, OkView):
424+
"""Set headers in an attribute w/o a template render class"""
397425
headers = {
398426
"X-DJANGO-BRACES-1": 1,
399427
"X-DJANGO-BRACES-2": 2,
400428
}
401429

402430

403431
class MethodHeaderView(views.HeaderMixin, OkView):
432+
"""Set headers in a method w/o a template render class"""
404433
def get_headers(self, request):
405434
return {
406435
"X-DJANGO-BRACES-1": 1,
@@ -409,19 +438,22 @@ def get_headers(self, request):
409438

410439

411440
class AuxiliaryHeaderView(View):
441+
"""A view with a header already set"""
412442
def dispatch(self, request, *args, **kwargs):
413443
response = HttpResponse("OK with headers")
414444
response["X-DJANGO-BRACES-EXISTING"] = "value"
415445
return response
416446

417447

418448
class ExistingHeaderView(views.HeaderMixin, AuxiliaryHeaderView):
449+
"""A view trying to override a parent's header"""
419450
headers = {
420451
'X-DJANGO-BRACES-EXISTING': 'other value'
421452
}
422453

423454

424455
class CacheControlPublicView(views.CacheControlMixin, OkView):
456+
"""A public-cached page with a 60 second timeout"""
425457
cachecontrol_public = True
426458
cachecontrol_max_age = 60
427459

0 commit comments

Comments
 (0)
Please sign in to comment.