diff --git a/.gitignore b/.gitignore index fbbabe9..108a79d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ venv/ .pytest_cache/ .tool-versions pytest-coverage.txt -coverage.xml \ No newline at end of file +coverage.xml +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 8c070df..7759da5 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,26 @@ # django-braces -Mixins for Django's class-based views. +`django-braces` provides useful Mixins for Django's class-based views. +Most of these mixins replicate the behavior of Django's function-based view +decorators. Others solve common headaches with working with class-based views. +You can read more in [the documentation](https://django-braces.readthedocs.io/en/latest/index.html). ![Build](https://github.com/brack3t/django-braces/actions/workflows/ci.yml/badge.svg?branch=main) [![PyPI version](https://badge.fury.io/py/django-braces.svg)](http://badge.fury.io/py/django-braces) [![codecov](https://codecov.io/gh/brack3t/django-braces/branch/main/graph/badge.svg?token=aBhzbsyyTi)](https://codecov.io/gh/brack3t/django-braces) -## Documentation +## Notes -[Read The Docs](https://django-braces.readthedocs.io/en/latest/index.html) +`django-braces` is stable and time-tested. It does not receive a lot of updates +and is not in active development. -## Installation - -Install from PyPI with `pip`: -`pip install django-braces` +`django-braces` also only officially supports Python version that are still +receiving fixes and Django LTS versions. `django-braces` will work with most +modern version of Python and Django, however. -## Building the Docs +## Installation -1. Install docs requirements: `pip install -r requirements-docs.txt`. -2. `cd docs`. -3. `make html`. -4. Open `_build/index.html` in your browser. +Install from PyPI with `pip`: `pip install django-braces` ## Contributing @@ -30,20 +30,9 @@ Add yourself to `CONTRIBUTORS.txt` if you want. All development dependencies are available in `requirements.txt` file. -To run the test suite, please install `tox` and as many Python interpreters as you'd -like to test against. Currently we test against 2.7, 3.6, 3.7, and 3.8. We recommend -using `asdf` to install various Python versions. - -Once `tox` and Python(s) are installed, you can execute the entire suite by running -just the `tox` command. +To run the test suite, please install `pytest` and run `pytest` at the root +of the repository. ## Change Log [Changelog on Read The Docs](https://django-braces.readthedocs.io/en/latest/changelog.html) - -## Supported Django Versions - -Our policy is that `django-braces` officially supports, and is tested on, all versions -that Django [officially supports](https://www.djangoproject.com/download/#supported-versions). -You are free to use `django-braces` with any version of Django you wish (so long as it has -class-based views) but no support will be promised. diff --git a/braces/__init__.py b/braces/__init__.py index 18b39c6..2a1ed55 100644 --- a/braces/__init__.py +++ b/braces/__init__.py @@ -8,8 +8,8 @@ :license: BSD 3-clause. See LICENSE for more details """ -__title__ = 'braces' -__version__ = '1.14.0' -__author__ = 'Kenneth Love and Chris Jones' -__license__ = 'BSD 3-clause' -__copyright__ = 'Copyright 2013 Kenneth Love and Chris Jones' +__title__ = "braces" +__version__ = "1.15.0" +__author__ = "Kenneth Love and Chris Jones" +__license__ = "BSD 3-clause" +__copyright__ = "Copyright 2013 Kenneth Love and Chris Jones" diff --git a/braces/forms.py b/braces/forms.py index 2b0f858..17d3231 100644 --- a/braces/forms.py +++ b/braces/forms.py @@ -7,7 +7,8 @@ class UserKwargModelFormMixin(object): expecting these kwargs to be passed in, so they must be popped off before anything else is done. """ + def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) # Pop the user off the - # passed in kwargs. + # passed in kwargs. super(UserKwargModelFormMixin, self).__init__(*args, **kwargs) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index e249a93..304885a 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -8,13 +8,13 @@ SuperuserRequiredMixin, UserPassesTestMixin, SSLRequiredMixin, - RecentLoginRequiredMixin + RecentLoginRequiredMixin, ) from ._ajax import ( AjaxResponseMixin, JSONRequestResponseMixin, JSONResponseMixin, - JsonRequestResponseMixin + JsonRequestResponseMixin, ) from ._forms import ( CsrfExemptMixin, @@ -30,42 +30,42 @@ CanonicalSlugDetailMixin, SetHeadlineMixin, StaticContextMixin, - HeaderMixin + HeaderMixin, ) from ._queries import ( OrderableListMixin, PrefetchRelatedMixin, - SelectRelatedMixin + SelectRelatedMixin, ) __all__ = [ - 'AjaxResponseMixin', - 'AllVerbsMixin', - 'AnonymousRequiredMixin', - 'CanonicalSlugDetailMixin', - 'CsrfExemptMixin', - 'FormInvalidMessageMixin', - 'FormMessagesMixin', - 'FormValidMessageMixin', - 'GroupRequiredMixin', - 'HeaderMixin', - 'JSONRequestResponseMixin', - 'JsonRequestResponseMixin', - 'JSONResponseMixin', - 'LoginRequiredMixin', - 'MessageMixin', - 'MultiplePermissionsRequiredMixin', - 'OrderableListMixin', - 'PermissionRequiredMixin', - 'PrefetchRelatedMixin', - 'SelectRelatedMixin', - 'SetHeadlineMixin', - 'StaffuserRequiredMixin', - 'StaticContextMixin', - 'SuccessURLRedirectListMixin', - 'SuperuserRequiredMixin', - 'UserFormKwargsMixin', - 'UserPassesTestMixin', - 'SSLRequiredMixin', - 'RecentLoginRequiredMixin' + "AjaxResponseMixin", + "AllVerbsMixin", + "AnonymousRequiredMixin", + "CanonicalSlugDetailMixin", + "CsrfExemptMixin", + "FormInvalidMessageMixin", + "FormMessagesMixin", + "FormValidMessageMixin", + "GroupRequiredMixin", + "HeaderMixin", + "JSONRequestResponseMixin", + "JsonRequestResponseMixin", + "JSONResponseMixin", + "LoginRequiredMixin", + "MessageMixin", + "MultiplePermissionsRequiredMixin", + "OrderableListMixin", + "PermissionRequiredMixin", + "PrefetchRelatedMixin", + "SelectRelatedMixin", + "SetHeadlineMixin", + "StaffuserRequiredMixin", + "StaticContextMixin", + "SuccessURLRedirectListMixin", + "SuperuserRequiredMixin", + "UserFormKwargsMixin", + "UserPassesTestMixin", + "SSLRequiredMixin", + "RecentLoginRequiredMixin", ] diff --git a/braces/views/_access.py b/braces/views/_access.py index 9ce08ff..d1f74a0 100644 --- a/braces/views/_access.py +++ b/braces/views/_access.py @@ -6,8 +6,13 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login, logout_then_login from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.http import (HttpResponseRedirect, HttpResponsePermanentRedirect, - Http404, HttpResponse, StreamingHttpResponse) +from django.http import ( + HttpResponseRedirect, + HttpResponsePermanentRedirect, + Http404, + HttpResponse, + StreamingHttpResponse, +) from django.shortcuts import resolve_url from django.utils.encoding import force_str from django.utils.timezone import now @@ -18,6 +23,7 @@ class AccessMixin(object): 'Abstract' mixin that gives access mixins the same customizable functionality. """ + login_url = None raise_exception = False redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth @@ -30,8 +36,9 @@ def get_login_url(self): login_url = self.login_url or settings.LOGIN_URL if not login_url: raise ImproperlyConfigured( - 'Define {0}.login_url or settings.LOGIN_URL or override ' - '{0}.get_login_url().'.format(self.__class__.__name__)) + "Define {0}.login_url or settings.LOGIN_URL or override " + "{0}.get_login_url().".format(self.__class__.__name__) + ) return force_str(login_url) @@ -41,20 +48,25 @@ def get_redirect_field_name(self): """ if self.redirect_field_name is None: raise ImproperlyConfigured( - '{0} is missing the ' - 'redirect_field_name. Define {0}.redirect_field_name or ' - 'override {0}.get_redirect_field_name().'.format( - self.__class__.__name__)) + "{0} is missing the " + "redirect_field_name. Define {0}.redirect_field_name or " + "override {0}.get_redirect_field_name().".format( + self.__class__.__name__ + ) + ) return self.redirect_field_name def handle_no_permission(self, request): if self.raise_exception: - if (self.redirect_unauthenticated_users - and not request.user.is_authenticated): + if ( + self.redirect_unauthenticated_users + and not request.user.is_authenticated + ): return self.no_permissions_fail(request) else: - if (inspect.isclass(self.raise_exception) - and issubclass(self.raise_exception, Exception)): + if inspect.isclass(self.raise_exception) and issubclass( + self.raise_exception, Exception + ): raise self.raise_exception if callable(self.raise_exception): ret = self.raise_exception(request) @@ -71,9 +83,11 @@ def no_permissions_fail(self, request=None): By default we redirect to login. """ - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) + return redirect_to_login( + request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name(), + ) class LoginRequiredMixin(AccessMixin): @@ -85,12 +99,14 @@ class LoginRequiredMixin(AccessMixin): combined with CsrfExemptMixin - which in that case should be the left-most mixin. """ + def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission(request) return super(LoginRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class AnonymousRequiredMixin(object): @@ -110,23 +126,27 @@ class SomeView(AnonymousRequiredMixin, ListView): authenticated_redirect_url = "/accounts/profile/" ... """ + authenticated_redirect_url = settings.LOGIN_REDIRECT_URL def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated: return HttpResponseRedirect(self.get_authenticated_redirect_url()) return super(AnonymousRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) def get_authenticated_redirect_url(self): - """ Return the reversed authenticated redirect url. """ + """Return the reversed authenticated redirect url.""" if not self.authenticated_redirect_url: raise ImproperlyConfigured( - '{0} is missing an authenticated_redirect_url ' - 'url to redirect to. Define ' - '{0}.authenticated_redirect_url or override ' - '{0}.get_authenticated_redirect_url().'.format( - self.__class__.__name__)) + "{0} is missing an authenticated_redirect_url " + "url to redirect to. Define " + "{0}.authenticated_redirect_url or override " + "{0}.get_authenticated_redirect_url().".format( + self.__class__.__name__ + ) + ) return resolve_url(self.authenticated_redirect_url) @@ -154,6 +174,7 @@ class SomeView(PermissionRequiredMixin, ListView): raise_exception = True ... """ + permission_required = None # Default required perms to none object_level_permissions = False @@ -168,7 +189,8 @@ def get_permission_required(self, request=None): if self.permission_required is None: raise ImproperlyConfigured( '{0} requires the "permission_required" attribute to be ' - 'set.'.format(self.__class__.__name__)) + "set.".format(self.__class__.__name__) + ) return self.permission_required @@ -179,13 +201,19 @@ def check_permissions(self, request): perms = self.get_permission_required(request) has_permission = False - if self.object_level_permissions: - if hasattr(self, 'object') and self.object is not None: - has_permission = request.user.has_perm(self.get_permission_required(request), self.object) - elif hasattr(self, 'get_object') and callable(self.get_object): - has_permission = request.user.has_perm(self.get_permission_required(request), self.get_object()) + if self.object_level_permissions: + if hasattr(self, "object") and self.object is not None: + has_permission = request.user.has_perm( + self.get_permission_required(request), self.object + ) + elif hasattr(self, "get_object") and callable(self.get_object): + has_permission = request.user.has_perm( + self.get_permission_required(request), self.get_object() + ) else: - has_permission = request.user.has_perm(self.get_permission_required(request)) + has_permission = request.user.has_perm( + self.get_permission_required(request) + ) return has_permission def dispatch(self, request, *args, **kwargs): @@ -199,7 +227,8 @@ def dispatch(self, request, *args, **kwargs): return self.handle_no_permission(request) return super(PermissionRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class MultiplePermissionsRequiredMixin(PermissionRequiredMixin): @@ -240,6 +269,7 @@ class SomeView(MultiplePermissionsRequiredMixin, ListView): redirect_field_name = "hollaback" raise_exception = True """ + permissions = None # Default required perms to none def get_permission_required(self, request=None): @@ -248,8 +278,8 @@ def get_permission_required(self, request=None): def check_permissions(self, request): permissions = self.get_permission_required(request) - perms_all = permissions.get('all') or None - perms_any = permissions.get('any') or None + perms_all = permissions.get("all") or None + perms_any = permissions.get("any") or None self._check_permissions_keys_set(perms_all, perms_any) self._check_perms_keys("all", perms_all) @@ -280,7 +310,8 @@ def _check_permissions_attr(self): if self.permissions is None or not isinstance(self.permissions, dict): raise ImproperlyConfigured( '{0} requires the "permissions" attribute to be set as a ' - 'dict.'.format(self.__class__.__name__)) + "dict.".format(self.__class__.__name__) + ) def _check_permissions_keys_set(self, perms_all=None, perms_any=None): """ @@ -292,7 +323,9 @@ def _check_permissions_keys_set(self, perms_all=None, perms_any=None): raise ImproperlyConfigured( '{0} requires the "permissions" attribute to be set to a ' 'dict and the "any" or "all" key to be set.'.format( - self.__class__.__name__)) + self.__class__.__name__ + ) + ) def _check_perms_keys(self, key=None, perms=None): """ @@ -301,8 +334,9 @@ def _check_perms_keys(self, key=None, perms=None): """ if perms and not isinstance(perms, (list, tuple)): raise ImproperlyConfigured( - '{0} requires the permisions dict {1} value to be a ' - 'list or tuple.'.format(self.__class__.__name__, key)) + "{0} requires the permisions dict {1} value to be a " + "list or tuple.".format(self.__class__.__name__, key) + ) class GroupRequiredMixin(AccessMixin): @@ -310,20 +344,20 @@ class GroupRequiredMixin(AccessMixin): def get_group_required(self): if self.group_required is None or ( - not isinstance(self.group_required, - (list, tuple, str)) + not isinstance(self.group_required, (list, tuple, str)) ): raise ImproperlyConfigured( '{0} requires the "group_required" attribute to be set and be ' - 'one of the following types: string, unicode, list or ' - 'tuple'.format(self.__class__.__name__)) + "one of the following types: string, unicode, list or " + "tuple".format(self.__class__.__name__) + ) if not isinstance(self.group_required, (list, tuple)): self.group_required = (self.group_required,) return self.group_required def check_membership(self, groups): - """ Check required group(s) """ + """Check required group(s)""" if self.request.user.is_superuser: return True user_groups = self.request.user.groups.values_list("name", flat=True) @@ -339,7 +373,8 @@ def dispatch(self, request, *args, **kwargs): return self.handle_no_permission(request) return super(GroupRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class UserPassesTestMixin(AccessMixin): @@ -357,9 +392,11 @@ class UserPassesTestMixin(AccessMixin): def test_func(self, user): raise NotImplementedError( - '{0} is missing implementation of the ' - 'test_func method. You should write one.'.format( - self.__class__.__name__)) + "{0} is missing implementation of the " + "test_func method. You should write one.".format( + self.__class__.__name__ + ) + ) def get_test_func(self): return getattr(self, "test_func") @@ -371,31 +408,36 @@ def dispatch(self, request, *args, **kwargs): return self.handle_no_permission(request) return super(UserPassesTestMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class SuperuserRequiredMixin(AccessMixin): """ Mixin allows you to require a user with `is_superuser` set to True. """ + def dispatch(self, request, *args, **kwargs): if not request.user.is_superuser: return self.handle_no_permission(request) return super(SuperuserRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class StaffuserRequiredMixin(AccessMixin): """ Mixin allows you to require a user with `is_staff` set to True. """ + def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return self.handle_no_permission(request) return super(StaffuserRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class SSLRequiredMixin(object): @@ -403,37 +445,42 @@ class SSLRequiredMixin(object): Simple mixin that allows you to force a view to be accessed via https. """ + raise_exception = False # Default whether to raise an exception to none def dispatch(self, request, *args, **kwargs): - if getattr(settings, 'DEBUG', False): + if getattr(settings, "DEBUG", False): return super(SSLRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) if not request.is_secure(): if self.raise_exception: raise Http404 return HttpResponsePermanentRedirect( - self._build_https_url(request)) + self._build_https_url(request) + ) return super(SSLRequiredMixin, self).dispatch(request, *args, **kwargs) def _build_https_url(self, request): - """ Get the full url, replace http with https """ + """Get the full url, replace http with https""" url = request.build_absolute_uri(request.get_full_path()) - return re.sub(r'^http', 'https', url) + return re.sub(r"^http", "https", url) class RecentLoginRequiredMixin(LoginRequiredMixin): """ Mixin allows you to require a login to be within a number of seconds. """ + max_last_login_delta = 1800 # Defaults to 30 minutes def dispatch(self, request, *args, **kwargs): resp = super(RecentLoginRequiredMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) if resp.status_code == 200: delta = datetime.timedelta(seconds=self.max_last_login_delta) diff --git a/braces/views/_ajax.py b/braces/views/_ajax.py index 2e3f9fb..21c88a2 100644 --- a/braces/views/_ajax.py +++ b/braces/views/_ajax.py @@ -10,24 +10,27 @@ class JSONResponseMixin(object): A mixin that allows you to easily serialize simple data such as a dict or Django models. """ + content_type = None json_dumps_kwargs = None json_encoder_class = DjangoJSONEncoder def get_content_type(self): - if (self.content_type is not None and - not isinstance(self.content_type, - str)): + if self.content_type is not None and not isinstance( + self.content_type, str + ): raise ImproperlyConfigured( - '{0} is missing a content type. Define {0}.content_type, ' - 'or override {0}.get_content_type().'.format( - self.__class__.__name__)) + "{0} is missing a content type. Define {0}.content_type, " + "or override {0}.get_content_type().".format( + self.__class__.__name__ + ) + ) return self.content_type or "application/json" def get_json_dumps_kwargs(self): if self.json_dumps_kwargs is None: self.json_dumps_kwargs = {} - self.json_dumps_kwargs.setdefault('ensure_ascii', False) + self.json_dumps_kwargs.setdefault("ensure_ascii", False) return self.json_dumps_kwargs def render_json_response(self, context_dict, status=200): @@ -38,10 +41,11 @@ def render_json_response(self, context_dict, status=200): json_context = json.dumps( context_dict, cls=self.json_encoder_class, - **self.get_json_dumps_kwargs()).encode('utf-8') - return HttpResponse(json_context, - content_type=self.get_content_type(), - status=status) + **self.get_json_dumps_kwargs() + ).encode("utf-8") + return HttpResponse( + json_context, content_type=self.get_content_type(), status=status + ) def render_json_object_response(self, objects, **kwargs): """ @@ -58,19 +62,24 @@ class AjaxResponseMixin(object): to the normal get, post, and put methods, you can use get_ajax, post_ajax, and put_ajax. """ + def dispatch(self, request, *args, **kwargs): request_method = request.method.lower() if request.is_ajax() and request_method in self.http_method_names: - handler = getattr(self, "{0}_ajax".format(request_method), - self.http_method_not_allowed) + handler = getattr( + self, + "{0}_ajax".format(request_method), + self.http_method_not_allowed, + ) self.request = request self.args = args self.kwargs = kwargs return handler(request, *args, **kwargs) return super(AjaxResponseMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) def get_ajax(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) @@ -104,6 +113,7 @@ def post(self, request, *args, **kwargs): return self.render_json_response( {'message': 'Thanks!'}) """ + require_json = False error_response_dict = {"errors": ["Improperly formatted request"]} @@ -114,13 +124,14 @@ def render_bad_request_response(self, error_dict=None): error_dict, cls=self.json_encoder_class, **self.get_json_dumps_kwargs() - ).encode('utf-8') + ).encode("utf-8") return HttpResponseBadRequest( - json_context, content_type=self.get_content_type()) + json_context, content_type=self.get_content_type() + ) def get_request_json(self): try: - return json.loads(self.request.body.decode('utf-8')) + return json.loads(self.request.body.decode("utf-8")) except ValueError: return None @@ -130,14 +141,17 @@ def dispatch(self, request, *args, **kwargs): self.kwargs = kwargs self.request_json = self.get_request_json() - if all([ - request.method != 'OPTIONS', - self.require_json, - self.request_json is None - ]): + if all( + [ + request.method != "OPTIONS", + self.require_json, + self.request_json is None, + ] + ): return self.render_bad_request_response() return super(JsonRequestResponseMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) class JSONRequestResponseMixin(JsonRequestResponseMixin): diff --git a/braces/views/_forms.py b/braces/views/_forms.py index 6bc3a1e..18b8134 100644 --- a/braces/views/_forms.py +++ b/braces/views/_forms.py @@ -9,7 +9,6 @@ from django.urls import reverse - class CsrfExemptMixin(object): """ Exempts the view from CSRF requirements. @@ -29,6 +28,7 @@ class UserFormKwargsMixin(object): Note: Using this mixin requires you to pop the `user` kwarg out of the dict in the super of your form's `__init__`. """ + def get_form_kwargs(self): kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() # Update the existing form kwargs dict with the request's user. @@ -45,16 +45,18 @@ class SuccessURLRedirectListMixin(object): This is only to be used for redirecting to a list page. If you need to reverse the url with kwargs, this is not the mixin to use. """ + success_list_url = None # Default the success url to none def get_success_url(self): # Return the reversed success url. if self.success_list_url is None: raise ImproperlyConfigured( - '{0} is missing a success_list_url ' - 'name to reverse and redirect to. Define ' - '{0}.success_list_url or override ' - '{0}.get_success_url().'.format(self.__class__.__name__)) + "{0} is missing a success_list_url " + "name to reverse and redirect to. Define " + "{0}.success_list_url or override " + "{0}.get_success_url().".format(self.__class__.__name__) + ) return reverse(self.success_list_url) @@ -63,11 +65,20 @@ class _MessageAPIWrapper(object): Wrap the django.contrib.messages.api module to automatically pass a given request object as the first parameter of function calls. """ - API = set([ - 'add_message', 'get_messages', - 'get_level', 'set_level', - 'debug', 'info', 'success', 'warning', 'error', - ]) + + API = set( + [ + "add_message", + "get_messages", + "get_level", + "set_level", + "debug", + "info", + "success", + "warning", + "error", + ] + ) def __init__(self, request): for name in self.API: @@ -80,6 +91,7 @@ class _MessageDescriptor(object): A descriptor that binds the _MessageAPIWrapper to the view's request. """ + def __get__(self, instance, owner): return _MessageAPIWrapper(instance.request) @@ -90,6 +102,7 @@ class MessageMixin(object): `django.contrib .messages`, automatically passing the current request object. """ + messages = _MessageDescriptor() @@ -99,6 +112,7 @@ class FormValidMessageMixin(MessageMixin): Django's messages framework through a static property on the class or programmatically by overloading the get_form_valid_message method. """ + form_valid_message = None # Default to None def get_form_valid_message(self): @@ -108,16 +122,15 @@ def get_form_valid_message(self): """ if self.form_valid_message is None: raise ImproperlyConfigured( - '{0}.form_valid_message is not set. Define ' - '{0}.form_valid_message, or override ' - '{0}.get_form_valid_message().'.format(self.__class__.__name__) + "{0}.form_valid_message is not set. Define " + "{0}.form_valid_message, or override " + "{0}.get_form_valid_message().".format(self.__class__.__name__) ) - if not isinstance(self.form_valid_message, - (str, Promise)): + if not isinstance(self.form_valid_message, (str, Promise)): raise ImproperlyConfigured( - '{0}.form_valid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__) + "{0}.form_valid_message must be a str or unicode " + "object.".format(self.__class__.__name__) ) return force_str(self.form_valid_message) @@ -128,14 +141,16 @@ def form_valid(self, form): get_form_valid_message, we have access to the newly saved object. """ response = super(FormValidMessageMixin, self).form_valid(form) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) + self.messages.success( + self.get_form_valid_message(), fail_silently=True + ) return response def delete(self, *args, **kwargs): response = super(FormValidMessageMixin, self).delete(*args, **kwargs) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) + self.messages.success( + self.get_form_valid_message(), fail_silently=True + ) return response @@ -145,6 +160,7 @@ class FormInvalidMessageMixin(MessageMixin): Django's messages framework through a static property on the class or programmatically by overloading the get_form_invalid_message method. """ + form_invalid_message = None def get_form_invalid_message(self): @@ -154,23 +170,26 @@ def get_form_invalid_message(self): """ if self.form_invalid_message is None: raise ImproperlyConfigured( - '{0}.form_invalid_message is not set. Define ' - '{0}.form_invalid_message, or override ' - '{0}.get_form_invalid_message().'.format( - self.__class__.__name__)) + "{0}.form_invalid_message is not set. Define " + "{0}.form_invalid_message, or override " + "{0}.get_form_invalid_message().".format( + self.__class__.__name__ + ) + ) - if not isinstance(self.form_invalid_message, - (str, Promise)): + if not isinstance(self.form_invalid_message, (str, Promise)): raise ImproperlyConfigured( - '{0}.form_invalid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__)) + "{0}.form_invalid_message must be a str or unicode " + "object.".format(self.__class__.__name__) + ) return force_str(self.form_invalid_message) def form_invalid(self, form): response = super(FormInvalidMessageMixin, self).form_invalid(form) - self.messages.error(self.get_form_invalid_message(), - fail_silently=True) + self.messages.error( + self.get_form_invalid_message(), fail_silently=True + ) return response @@ -179,4 +198,5 @@ class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): Mixin is a shortcut to use both FormValidMessageMixin and FormInvalidMessageMixin. """ + pass diff --git a/braces/views/_other.py b/braces/views/_other.py index bf11d0b..4ffd13a 100644 --- a/braces/views/_other.py +++ b/braces/views/_other.py @@ -9,6 +9,7 @@ class SetHeadlineMixin(object): Mixin allows you to set a static headline through a static property on the class or programmatically by overloading the get_headline method. """ + headline = None # Default the headline to none def get_context_data(self, **kwargs): @@ -19,12 +20,13 @@ def get_context_data(self, **kwargs): def get_headline(self): if self.headline is None: # If no headline was provided as a view - # attribute and this method wasn't - # overridden raise a configuration error. + # attribute and this method wasn't + # overridden raise a configuration error. raise ImproperlyConfigured( - '{0} is missing a headline. ' - 'Define {0}.headline, or override ' - '{0}.get_headline().'.format(self.__class__.__name__)) + "{0} is missing a headline. " + "Define {0}.headline, or override " + "{0}.get_headline().".format(self.__class__.__name__) + ) return force_str(self.headline) @@ -33,6 +35,7 @@ class StaticContextMixin(object): Mixin allows you to set static context through a static property on the class. """ + static_context = None def get_context_data(self, **kwargs): @@ -42,17 +45,18 @@ def get_context_data(self, **kwargs): kwargs.update(self.get_static_context()) except (TypeError, ValueError): raise ImproperlyConfigured( - '{0}.static_context must be a dictionary or container ' - 'of two-tuples.'.format(self.__class__.__name__)) + "{0}.static_context must be a dictionary or container " + "of two-tuples.".format(self.__class__.__name__) + ) else: return kwargs def get_static_context(self): if self.static_context is None: raise ImproperlyConfigured( - '{0} is missing the static_context property. Define ' - '{0}.static_context, or override ' - '{0}.get_static_context()'.format(self.__class__.__name__) + "{0} is missing the static_context property. Define " + "{0}.static_context, or override " + "{0}.get_static_context()".format(self.__class__.__name__) ) return self.static_context @@ -65,6 +69,7 @@ class CanonicalSlugDetailMixin(object): argument does not equal the object's canonical slug, this mixin will redirect to the url containing the canonical slug. """ + def dispatch(self, request, *args, **kwargs): # Set up since we need to super() later instead of earlier. self.request = request @@ -89,13 +94,16 @@ def dispatch(self, request, *args, **kwargs): # If there's a discrepancy between the slug in the url and the # canonical slug, redirect to the canonical slug. if canonical_slug != slug: - params = {self.pk_url_kwarg: obj.pk, - self.slug_url_kwarg: canonical_slug, - 'permanent': True} + params = { + self.pk_url_kwarg: obj.pk, + self.slug_url_kwarg: canonical_slug, + "permanent": True, + } return redirect(current_urlpattern, **params) return super(CanonicalSlugDetailMixin, self).dispatch( - request, *args, **kwargs) + request, *args, **kwargs + ) def get_canonical_slug(self): """ @@ -114,13 +122,16 @@ class AllVerbsMixin(object): The name of the method should be specified using the class attribute ``all_handler``. The default value of this attribute is 'all'. """ - all_handler = 'all' + + all_handler = "all" def dispatch(self, request, *args, **kwargs): if not self.all_handler: raise ImproperlyConfigured( - '{0} requires the all_handler attribute to be set.'.format( - self.__class__.__name__)) + "{0} requires the all_handler attribute to be set.".format( + self.__class__.__name__ + ) + ) handler = getattr(self, self.all_handler, self.http_method_not_allowed) return handler(request, *args, **kwargs) @@ -131,6 +142,7 @@ class HeaderMixin(object): Add arbitrary HTTP headers to a response by specifying them in the ``headers`` attribute or by overriding the ``get_headers()`` method. """ + headers = {} def get_headers(self, request): diff --git a/braces/views/_queries.py b/braces/views/_queries.py index 70d089f..1f53e22 100644 --- a/braces/views/_queries.py +++ b/braces/views/_queries.py @@ -8,27 +8,30 @@ class SelectRelatedMixin(object): Mixin allows you to provide a tuple or list of related models to perform a select_related on. """ + select_related = None # Default related fields to none def get_queryset(self): if self.select_related is None: # If no fields were provided, raise a configuration error raise ImproperlyConfigured( - '{0} is missing the select_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) + "{0} is missing the select_related property. This must be " + "a tuple or list.".format(self.__class__.__name__) + ) if not isinstance(self.select_related, (tuple, list)): # If the select_related argument is *not* a tuple or list, # raise a configuration error. raise ImproperlyConfigured( "{0}'s select_related property must be a tuple or " - "list.".format(self.__class__.__name__)) + "list.".format(self.__class__.__name__) + ) # Get the current queryset of the view queryset = super(SelectRelatedMixin, self).get_queryset() if not self.select_related: - warnings.warn('The select_related attribute is empty') + warnings.warn("The select_related attribute is empty") return queryset return queryset.select_related(*self.select_related) @@ -39,27 +42,30 @@ class PrefetchRelatedMixin(object): Mixin allows you to provide a tuple or list of related models to perform a prefetch_related on. """ + prefetch_related = None # Default prefetch fields to none def get_queryset(self): if self.prefetch_related is None: # If no fields were provided, raise a configuration error raise ImproperlyConfigured( - '{0} is missing the prefetch_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) + "{0} is missing the prefetch_related property. This must be " + "a tuple or list.".format(self.__class__.__name__) + ) if not isinstance(self.prefetch_related, (tuple, list)): # If the prefetch_related argument is *not* a tuple or list, # raise a configuration error. raise ImproperlyConfigured( "{0}'s prefetch_related property must be a tuple or " - "list.".format(self.__class__.__name__)) + "list.".format(self.__class__.__name__) + ) # Get the current queryset of the view queryset = super(PrefetchRelatedMixin, self).get_queryset() if not self.prefetch_related: - warnings.warn('The prefetch_related attribute is empty') + warnings.warn("The prefetch_related attribute is empty") return queryset return queryset.prefetch_related(*self.prefetch_related) @@ -91,15 +97,19 @@ def get_context_data(self, **kwargs): def get_orderable_columns(self): if not self.orderable_columns: raise ImproperlyConfigured( - '{0} needs the ordering columns defined.'.format( - self.__class__.__name__)) + "{0} needs the ordering columns defined.".format( + self.__class__.__name__ + ) + ) return self.orderable_columns def get_orderable_columns_default(self): if not self.orderable_columns_default: raise ImproperlyConfigured( - '{0} needs the default ordering column defined.'.format( - self.__class__.__name__)) + "{0} needs the default ordering column defined.".format( + self.__class__.__name__ + ) + ) return self.orderable_columns_default def get_ordering_default(self): @@ -108,8 +118,10 @@ def get_ordering_default(self): else: if self.ordering_default not in ["asc", "desc"]: raise ImproperlyConfigured( - '{0} only allows asc or desc as ordering option'.format( - self.__class__.__name__)) + "{0} only allows asc or desc as ordering option".format( + self.__class__.__name__ + ) + ) return self.ordering_default def get_ordered_queryset(self, queryset=None): @@ -129,8 +141,10 @@ def get_ordered_queryset(self, queryset=None): self.order_by = order_by self.ordering = self.get_ordering_default() - if order_by and self.request.GET.get( - "ordering", self.ordering) == "desc": + if ( + order_by + and self.request.GET.get("ordering", self.ordering) == "desc" + ): order_by = "-" + order_by self.ordering = self.request.GET.get("ordering", self.ordering) diff --git a/conftest.py b/conftest.py index c7aabc7..a0f582b 100644 --- a/conftest.py +++ b/conftest.py @@ -4,5 +4,5 @@ def pytest_configure(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") settings.configure(default_settings=test_settings) diff --git a/docs/changelog.rst b/docs/changelog.rst index a1bbeb1..bf3d774 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog ========= +* :release:`1.15.0 <2021-11-05>` +* :support:`-` Formatted project with black +* :support:`-` Updated README +* :feature:`265` Drop old Python and Django versions, bring in newer ones * :release:`1.14.0 <2019-12-30>` * :support:`260` Fixes the goshdang Travis tests. * :support:`250` Include documentation Makefile and conf.py in source distribution. diff --git a/docs/conf.py b/docs/conf.py index 3332c83..ff9184f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,23 +13,23 @@ import sys, os -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) import braces from braces import __version__ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['releases'] +extensions = ["releases"] releases_issue_uri = "https://github.com/brack3t/django-braces/issues/%s" releases_release_uri = "https://github.com/brack3t/django-braces/tree/%s" @@ -37,20 +37,20 @@ releases_debug = True # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'django-braces' -copyright = u'2013, Kenneth Love and Chris Jones' +project = u"django-braces" +copyright = u"2013, Kenneth Love and Chris Jones" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -63,158 +63,161 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-bracesdoc' +htmlhelp_basename = "django-bracesdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-braces.tex', 'django-braces Documentation', - 'Kenneth Love and Chris Jones', 'manual'), + ( + "index", + "django-braces.tex", + "django-braces Documentation", + "Kenneth Love and Chris Jones", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -222,12 +225,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-braces', 'django-braces Documentation', - ['Kenneth Love and Chris Jones'], 1) + ( + "index", + "django-braces", + "django-braces Documentation", + ["Kenneth Love and Chris Jones"], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -236,16 +244,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-braces', 'django-braces Documentation', - 'Kenneth Love and Chris Jones', 'django-braces', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "django-braces", + "django-braces Documentation", + "Kenneth Love and Chris Jones", + "django-braces", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/contributing.rst b/docs/contributing.rst index 6aee23e..63c3d0a 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -43,11 +43,12 @@ Tests All code changes should come with test changes. We use `py.test `_ instead of Python's -``unittest``. This seems to only be really important when marking tests for -skipping. +``unittest``. -We try to keep the project at 100% test coverage but know this isn't something -we can achieve forever. So long as your tests cover your contribution 80% or -better, we're happy to try and bump up that last bit, or just accept the code. +We try to keep the project at high test coverage but know this isn't something +we can achieve alone. Tests should be included with your pull requests and +should cover 100% of your changes. -We currently test Braces against late (usually latest) versions of Python 2.7, 3.4, 3.5, and 3.6. We also test against the latest released version of Django from 1.11 to 2.0. +We test ``django-braces`` against currently supported versions of Python and +LTS versions of Django. All pull requests are run through a matrix of these +Python and Django versions. Locally, use a recent Python and Django. diff --git a/requirements.txt b/requirements.txt index 75467fb..220909f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ factory_boy mock pytest-django pytest-cov -coverage \ No newline at end of file +coverage +django \ No newline at end of file diff --git a/setup.py b/setup.py index d1a6487..93f4e3a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,8 @@ import re from setuptools import setup -NAME = 'braces' +NAME = "braces" + def _add_default(m): attr_name, attr_value = m.groups() @@ -12,24 +13,25 @@ def _add_default(m): def parse_dist_meta(): """Extract metadata information from ``$dist/__init__.py``.""" - re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') + re_meta = re.compile(r"__(\w+?)__\s*=\s*(.*)") re_doc = re.compile(r'^"""(.+?)"""') here = os.path.abspath(os.path.dirname(__file__)) - with open(os.path.join(here, NAME, '__init__.py')) as meta_fh: + with open(os.path.join(here, NAME, "__init__.py")) as meta_fh: distmeta = {} for line in meta_fh: - if line.strip() == '# -eof meta-': + if line.strip() == "# -eof meta-": break match = re_meta.match(line.strip()) if match: distmeta.update(_add_default(match)) return distmeta + meta = parse_dist_meta() setup( name="django-braces", - version=meta['version'], + version=meta["version"], description="Reusable, generic mixins for Django", long_description="Mixins to add easy functionality to Django class-based views, forms, and models.", keywords="django, views, forms, mixins", @@ -41,22 +43,23 @@ def parse_dist_meta(): zip_safe=False, include_package_data=True, classifiers=[ - "Programming Language :: Python", - "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: BSD License", - "Environment :: Web Environment", "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.5", + "Development Status :: 6 - Mature", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Framework :: Django", - "Framework :: Django :: 2.2" - "Framework :: Django :: 3.1" - "Framework :: Django :: 3.2" - ], - install_requires=[ - "Django>=2.2" + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries :: Python Modules", ], + install_requires=["Django>=2.2"], ) diff --git a/tests/factories.py b/tests/factories.py index d0ccf11..cf6de5a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -11,14 +11,15 @@ def _get_perm(perm_name): Permission name is a string like 'auth.add_user'. """ - app_label, codename = perm_name.split('.') + app_label, codename = perm_name.split(".") return Permission.objects.get( - content_type__app_label=app_label, codename=codename) + content_type__app_label=app_label, codename=codename + ) class ArticleFactory(factory.django.DjangoModelFactory): - title = factory.Sequence(lambda n: 'Article number {0}'.format(n)) - body = factory.Sequence(lambda n: 'Body of article {0}'.format(n)) + title = factory.Sequence(lambda n: "Article number {0}".format(n)) + body = factory.Sequence(lambda n: "Body of article {0}".format(n)) class Meta: model = Article @@ -26,7 +27,7 @@ class Meta: class GroupFactory(factory.django.DjangoModelFactory): - name = factory.Sequence(lambda n: 'group{0}'.format(n)) + name = factory.Sequence(lambda n: "group{0}".format(n)) class Meta: model = Group @@ -34,11 +35,11 @@ class Meta: class UserFactory(factory.django.DjangoModelFactory): - username = factory.Sequence(lambda n: 'user{0}'.format(n)) - first_name = factory.Sequence(lambda n: 'John {0}'.format(n)) - last_name = factory.Sequence(lambda n: 'Doe {0}'.format(n)) - email = factory.Sequence(lambda n: 'user{0}@example.com'.format(n)) - password = factory.PostGenerationMethodCall('set_password', 'asdf1234') + username = factory.Sequence(lambda n: "user{0}".format(n)) + first_name = factory.Sequence(lambda n: "John {0}".format(n)) + last_name = factory.Sequence(lambda n: "Doe {0}".format(n)) + email = factory.Sequence(lambda n: "user{0}@example.com".format(n)) + password = factory.PostGenerationMethodCall("set_password", "asdf1234") class Meta: model = User diff --git a/tests/forms.py b/tests/forms.py index 95fab22..d57bd0e 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -12,4 +12,4 @@ class FormWithUserKwarg(UserKwargModelFormMixin, forms.Form): class ArticleForm(forms.ModelForm): class Meta: model = Article - fields = ['author', 'title', 'body', 'slug'] + fields = ["author", "title", "body", "slug"] diff --git a/tests/helpers.py b/tests/helpers.py index a94ff61..e667652 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,6 +7,7 @@ class TestViewHelper(object): """ Helper class for unit-testing class based views. """ + view_class = None request_factory_class = test.RequestFactory @@ -14,7 +15,7 @@ def setUp(self): super(TestViewHelper, self).setUp() self.factory = self.request_factory_class() - def build_request(self, method='GET', path='/test/', user=None, **kwargs): + def build_request(self, method="GET", path="/test/", user=None, **kwargs): """ Creates a request using request factory. """ @@ -26,8 +27,9 @@ def build_request(self, method='GET', path='/test/', user=None, **kwargs): req.user = user return req - def build_view(self, request, args=None, kwargs=None, view_class=None, - **viewkwargs): + def build_view( + self, request, args=None, kwargs=None, view_class=None, **viewkwargs + ): """ Creates a `view_class` view instance. """ @@ -39,10 +41,12 @@ def build_view(self, request, args=None, kwargs=None, view_class=None, view_class = self.view_class return view_class( - request=request, args=args, kwargs=kwargs, **viewkwargs) + request=request, args=args, kwargs=kwargs, **viewkwargs + ) - def dispatch_view(self, request, args=None, kwargs=None, view_class=None, - **viewkwargs): + def dispatch_view( + self, request, args=None, kwargs=None, view_class=None, **viewkwargs + ): """ Creates and dispatches `view_class` view. """ @@ -55,6 +59,7 @@ class SetJSONEncoder(DjangoJSONEncoder): A custom JSONEncoder extending `DjangoJSONEncoder` to handle serialization of `set`. """ + def default(self, obj): if isinstance(obj, set): return list(obj) diff --git a/tests/models.py b/tests/models.py index c89fda4..c712ec4 100644 --- a/tests/models.py +++ b/tests/models.py @@ -3,10 +3,7 @@ class Article(models.Model): author = models.ForeignKey( - 'auth.User', - null=True, - blank=True, - on_delete=models.CASCADE + "auth.User", null=True, blank=True, on_delete=models.CASCADE ) title = models.CharField(max_length=30) body = models.TextField() @@ -15,10 +12,7 @@ class Article(models.Model): class CanonicalArticle(models.Model): author = models.ForeignKey( - 'auth.User', - null=True, - blank=True, - on_delete=models.CASCADE + "auth.User", null=True, blank=True, on_delete=models.CASCADE ) title = models.CharField(max_length=30) body = models.TextField() diff --git a/tests/settings.py b/tests/settings.py index 7b414b5..2de0172 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -3,69 +3,63 @@ DEBUG = False TEMPLATE_DEBUG = DEBUG -TIME_ZONE = 'UTC' -LANGUAGE_CODE = 'en-US' +TIME_ZONE = "UTC" +LANGUAGE_CODE = "en-US" SITE_ID = 1 USE_L10N = True USE_TZ = True -SECRET_KEY = 'local' +SECRET_KEY = "local" -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:' - } + "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} } MIDDLEWARE_CLASSES = [ - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] MIDDLEWARE = MIDDLEWARE_CLASSES STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - - 'tests', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.sites", + "tests", ) -PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', -) +PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], }, }, diff --git a/tests/test_access_mixins.py b/tests/test_access_mixins.py index 8f2d50e..fa55352 100644 --- a/tests/test_access_mixins.py +++ b/tests/test_access_mixins.py @@ -13,12 +13,20 @@ from .factories import GroupFactory, UserFactory from .helpers import TestViewHelper -from .views import (PermissionRequiredView, MultiplePermissionsRequiredView, - SuperuserRequiredView, StaffuserRequiredView, - LoginRequiredView, GroupRequiredView, UserPassesTestView, - UserPassesTestNotImplementedView, AnonymousRequiredView, - SSLRequiredView, RecentLoginRequiredView, - UserPassesTestLoginRequiredView) +from .views import ( + PermissionRequiredView, + MultiplePermissionsRequiredView, + SuperuserRequiredView, + StaffuserRequiredView, + LoginRequiredView, + GroupRequiredView, + UserPassesTestView, + UserPassesTestNotImplementedView, + AnonymousRequiredView, + SSLRequiredView, + RecentLoginRequiredView, + UserPassesTestLoginRequiredView, +) @pytest.mark.django_db @@ -26,6 +34,7 @@ class _TestAccessBasicsMixin(TestViewHelper): """ A set of basic tests for access mixins. """ + view_url = None def build_authorized_user(self): @@ -45,10 +54,10 @@ def test_success(self): If user is authorized then view should return normal response. """ user = self.build_authorized_user() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) def test_redirects_to_login(self): """ @@ -56,10 +65,11 @@ def test_redirects_to_login(self): to view this page. """ user = self.build_unauthorized_user() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) - self.assertRedirects(resp, '/accounts/login/?next={0}'.format( - self.view_url)) + self.assertRedirects( + resp, "/accounts/login/?next={0}".format(self.view_url) + ) def test_raise_permission_denied(self): """ @@ -110,7 +120,7 @@ def func(request): resp = self.dispatch_view(req, raise_exception=func) assert resp.status_code == 200 - assert force_str(resp.content) == 'CUSTOM' + assert force_str(resp.content) == "CUSTOM" def test_raise_func_false(self): """ @@ -147,15 +157,16 @@ def test_custom_login_url(self): """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) - resp = self.dispatch_view(req, login_url='/login/') + resp = self.dispatch_view(req, login_url="/login/") self.assertEqual( - '/login/?next={0}'.format(self.view_url), - resp['Location']) + "/login/?next={0}".format(self.view_url), resp["Location"] + ) # Test with reverse_lazy - resp = self.dispatch_view(req, login_url=reverse_lazy('headline')) - self.assertEqual('/headline/?next={0}'.format( - self.view_url), resp['Location']) + resp = self.dispatch_view(req, login_url=reverse_lazy("headline")) + self.assertEqual( + "/headline/?next={0}".format(self.view_url), resp["Location"] + ) def test_custom_redirect_field_name(self): """ @@ -163,9 +174,9 @@ def test_custom_redirect_field_name(self): """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) - resp = self.dispatch_view(req, redirect_field_name='foo') - expected_url = '/accounts/login/?foo={0}'.format(self.view_url) - self.assertEqual(expected_url, resp['Location']) + resp = self.dispatch_view(req, redirect_field_name="foo") + expected_url = "/accounts/login/?foo={0}".format(self.view_url) + self.assertEqual(expected_url, resp["Location"]) @override_settings(LOGIN_URL=None) def test_get_login_url_raises_exception(self): @@ -175,7 +186,8 @@ def test_get_login_url_raises_exception(self): """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view( - self.build_request(path=self.view_url), login_url=None) + self.build_request(path=self.view_url), login_url=None + ) def test_get_redirect_field_name_raises_exception(self): """ @@ -185,7 +197,8 @@ def test_get_redirect_field_name_raises_exception(self): with self.assertRaises(ImproperlyConfigured): self.dispatch_view( self.build_request(path=self.view_url), - redirect_field_name=None) + redirect_field_name=None, + ) @override_settings(LOGIN_URL="/auth/login/") def test_overridden_login_url(self): @@ -194,26 +207,30 @@ def test_overridden_login_url(self): overridden dynamically. """ user = self.build_unauthorized_user() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) - self.assertRedirects(resp, '/auth/login/?next={0}'.format( - self.view_url)) + self.assertRedirects( + resp, "/auth/login/?next={0}".format(self.view_url) + ) def test_redirect_unauthenticated(self): resp = self.dispatch_view( - self.build_request(path=self.view_url), - raise_exception=True, - redirect_unauthenticated_users=True) + self.build_request(path=self.view_url), + raise_exception=True, + redirect_unauthenticated_users=True, + ) assert resp.status_code == 302 - assert resp['Location'] == '/accounts/login/?next={0}'.format( - self.view_url) + assert resp["Location"] == "/accounts/login/?next={0}".format( + self.view_url + ) def test_redirect_unauthenticated_false(self): with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True, - redirect_unauthenticated_users=False) + redirect_unauthenticated_users=False, + ) @pytest.mark.django_db @@ -221,31 +238,35 @@ class TestLoginRequiredMixin(TestViewHelper, test.TestCase): """ Tests for LoginRequiredMixin. """ + view_class = LoginRequiredView - view_url = '/login_required/' + view_url = "/login_required/" def test_anonymous(self): resp = self.client.get(self.view_url) - self.assertRedirects(resp, '/accounts/login/?next=/login_required/') + self.assertRedirects(resp, "/accounts/login/?next=/login_required/") def test_anonymous_raises_exception(self): with self.assertRaises(PermissionDenied): self.dispatch_view( - self.build_request(path=self.view_url), raise_exception=True) + self.build_request(path=self.view_url), raise_exception=True + ) def test_authenticated(self): user = UserFactory() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) assert resp.status_code == 200 - assert force_str(resp.content) == 'OK' + assert force_str(resp.content) == "OK" def test_anonymous_redirects(self): resp = self.dispatch_view( - self.build_request(path=self.view_url), raise_exception=True, - redirect_unauthenticated_users=True) + self.build_request(path=self.view_url), + raise_exception=True, + redirect_unauthenticated_users=True, + ) assert resp.status_code == 302 - assert resp['Location'] == '/accounts/login/?next=/login_required/' + assert resp["Location"] == "/accounts/login/?next=/login_required/" @pytest.mark.django_db @@ -253,22 +274,22 @@ class TestChainedLoginRequiredMixin(TestViewHelper, test.TestCase): """ Tests for LoginRequiredMixin combined with another AccessMixin. """ + view_class = UserPassesTestLoginRequiredView - view_url = '/chained_view/' + view_url = "/chained_view/" def assert_redirect_to_login(self, response): """ Check that the response is a redirect to the login view. """ assert response.status_code == 302 - assert response['Location'] == '/accounts/login/?next=/chained_view/' + assert response["Location"] == "/accounts/login/?next=/chained_view/" def test_anonymous(self): """ Check that anonymous users redirect to login by default. """ - resp = self.dispatch_view( - self.build_request(path=self.view_url)) + resp = self.dispatch_view(self.build_request(path=self.view_url)) self.assert_redirect_to_login(resp) def test_anonymous_raises_exception(self): @@ -278,7 +299,8 @@ def test_anonymous_raises_exception(self): """ with self.assertRaises(PermissionDenied): self.dispatch_view( - self.build_request(path=self.view_url), raise_exception=True) + self.build_request(path=self.view_url), raise_exception=True + ) def test_authenticated_raises_exception(self): """ @@ -289,11 +311,14 @@ def test_authenticated_raises_exception(self): with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url, user=user), - raise_exception=True) + raise_exception=True, + ) with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url, user=user), - raise_exception=True, redirect_unauthenticated_users=True) + raise_exception=True, + redirect_unauthenticated_users=True, + ) def test_anonymous_redirects(self): """ @@ -301,8 +326,10 @@ def test_anonymous_redirects(self): is overridden by redirect_unauthenticated_users. """ resp = self.dispatch_view( - self.build_request(path=self.view_url), raise_exception=True, - redirect_unauthenticated_users=True) + self.build_request(path=self.view_url), + raise_exception=True, + redirect_unauthenticated_users=True, + ) self.assert_redirect_to_login(resp) @@ -311,8 +338,9 @@ class TestAnonymousRequiredMixin(TestViewHelper, test.TestCase): """ Tests for AnonymousRequiredMixin. """ + view_class = AnonymousRequiredView - view_url = '/unauthenticated_view/' + view_url = "/unauthenticated_view/" def test_anonymous(self): """ @@ -321,14 +349,14 @@ def test_anonymous(self): """ resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) # Test with reverse_lazy resp = self.dispatch_view( - self.build_request(), - login_url=reverse_lazy(self.view_url)) + self.build_request(), login_url=reverse_lazy(self.view_url) + ) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) def test_authenticated(self): """ @@ -336,24 +364,24 @@ def test_authenticated(self): to the approparite view. """ user = UserFactory() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(302, resp.status_code) resp = self.client.get(self.view_url, follow=True) - self.assertRedirects(resp, '/authenticated_view/') + self.assertRedirects(resp, "/authenticated_view/") def test_no_url(self): self.view_class.authenticated_redirect_url = None user = UserFactory() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) def test_bad_url(self): - self.view_class.authenticated_redirect_url = '/epicfailurl/' + self.view_class.authenticated_redirect_url = "/epicfailurl/" user = UserFactory() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url, follow=True) self.assertEqual(404, resp.status_code) @@ -363,11 +391,12 @@ class TestPermissionRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """ Tests for PermissionRequiredMixin. """ + view_class = PermissionRequiredView - view_url = '/permission_required/' + view_url = "/permission_required/" def build_authorized_user(self): - return UserFactory(permissions=['auth.add_user']) + return UserFactory(permissions=["auth.add_user"]) def build_unauthorized_user(self): return UserFactory() @@ -383,38 +412,43 @@ def test_invalid_permission(self): @pytest.mark.django_db class TestMultiplePermissionsRequiredMixin( - _TestAccessBasicsMixin, test.TestCase): + _TestAccessBasicsMixin, test.TestCase +): view_class = MultiplePermissionsRequiredView - view_url = '/multiple_permissions_required/' + view_url = "/multiple_permissions_required/" def build_authorized_user(self): - return UserFactory(permissions=[ - 'tests.add_article', 'tests.change_article', 'auth.change_user']) + return UserFactory( + permissions=[ + "tests.add_article", + "tests.change_article", + "auth.change_user", + ] + ) def build_unauthorized_user(self): - return UserFactory(permissions=['tests.add_article']) + return UserFactory(permissions=["tests.add_article"]) def test_redirects_to_login(self): """ User should be redirected to login page if he or she does not have sufficient permissions. """ - url = '/multiple_permissions_required/' + url = "/multiple_permissions_required/" test_cases = ( # missing one permission from 'any' - ['tests.add_article', 'tests.change_article'], + ["tests.add_article", "tests.change_article"], # missing one permission from 'all' - ['tests.add_article', 'auth.add_user'], + ["tests.add_article", "auth.add_user"], # no permissions at all [], ) for permissions in test_cases: user = UserFactory(permissions=permissions) - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(url) - self.assertRedirects(resp, '/accounts/login/?next={0}'.format( - url)) + self.assertRedirects(resp, "/accounts/login/?next={0}".format(url)) def test_invalid_permissions(self): """ @@ -425,10 +459,10 @@ def test_invalid_permissions(self): None, # permissions must be set (), # and they must be a dict {}, # at least one of 'all', 'any' keys must be present - {'all': None}, # both all and any must be list or a tuple - {'all': {'a': 1}}, - {'any': None}, - {'any': {'a': 1}}, + {"all": None}, # both all and any must be list or a tuple + {"all": {"a": 1}}, + {"any": None}, + {"any": {"a": 1}}, ) for attr in permissions: @@ -442,9 +476,9 @@ def test_raise_permission_denied(self): """ test_cases = ( # missing one permission from 'any' - ['tests.add_article', 'tests.change_article'], + ["tests.add_article", "tests.change_article"], # missing one permission from 'all' - ['tests.add_article', 'auth.add_user'], + ["tests.add_article", "auth.add_user"], # no permissions at all [], ) @@ -459,41 +493,45 @@ def test_all_permissions_key(self): """ Tests if everything works if only 'all' permissions has been set. """ - permissions = {'all': ['auth.add_user', 'tests.add_article']} - user = UserFactory(permissions=permissions['all']) + permissions = {"all": ["auth.add_user", "tests.add_article"]} + user = UserFactory(permissions=permissions["all"]) req = self.build_request(user=user) resp = self.dispatch_view(req, permissions=permissions) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) - user = UserFactory(permissions=['auth.add_user']) + user = UserFactory(permissions=["auth.add_user"]) with self.assertRaises(PermissionDenied): self.dispatch_view( - self.build_request(user=user), raise_exception=True, - permissions=permissions) + self.build_request(user=user), + raise_exception=True, + permissions=permissions, + ) def test_any_permissions_key(self): """ Tests if everything works if only 'any' permissions has been set. """ - permissions = {'any': ['auth.add_user', 'tests.add_article']} - user = UserFactory(permissions=['tests.add_article']) + permissions = {"any": ["auth.add_user", "tests.add_article"]} + user = UserFactory(permissions=["tests.add_article"]) req = self.build_request(user=user) resp = self.dispatch_view(req, permissions=permissions) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) user = UserFactory(permissions=[]) with self.assertRaises(PermissionDenied): self.dispatch_view( - self.build_request(user=user), raise_exception=True, - permissions=permissions) + self.build_request(user=user), + raise_exception=True, + permissions=permissions, + ) @pytest.mark.django_db class TestSuperuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): view_class = SuperuserRequiredView - view_url = '/superuser_required/' + view_url = "/superuser_required/" def build_authorized_user(self): return UserFactory(is_superuser=True, is_staff=True) @@ -505,7 +543,7 @@ def build_unauthorized_user(self): @pytest.mark.django_db class TestStaffuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): view_class = StaffuserRequiredView - view_url = '/staffuser_required/' + view_url = "/staffuser_required/" def build_authorized_user(self): return UserFactory(is_staff=True) @@ -517,11 +555,11 @@ def build_unauthorized_user(self): @pytest.mark.django_db class TestGroupRequiredMixin(_TestAccessBasicsMixin, test.TestCase): view_class = GroupRequiredView - view_url = '/group_required/' + view_url = "/group_required/" def build_authorized_user(self): user = UserFactory() - group = GroupFactory(name='test_group') + group = GroupFactory(name="test_group") user.groups.add(group) return user @@ -535,33 +573,33 @@ def build_unauthorized_user(self): return UserFactory() def test_with_string(self): - self.assertEqual('test_group', self.view_class.group_required) + self.assertEqual("test_group", self.view_class.group_required) user = self.build_authorized_user() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) def test_with_group_list(self): - group_list = ['test_group', 'editors'] + group_list = ["test_group", "editors"] # the test client will instantiate a new view on request, so we have to # modify the class variable (and restore it when the test finished) self.view_class.group_required = group_list self.assertEqual(group_list, self.view_class.group_required) user = self.build_authorized_user() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) - self.view_class.group_required = 'test_group' - self.assertEqual('test_group', self.view_class.group_required) + self.assertEqual("OK", force_str(resp.content)) + self.view_class.group_required = "test_group" + self.assertEqual("test_group", self.view_class.group_required) def test_superuser_allowed(self): user = self.build_superuser() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) def test_improperly_configured(self): view = self.view_class() @@ -569,84 +607,87 @@ def test_improperly_configured(self): with self.assertRaises(ImproperlyConfigured): view.get_group_required() - view.group_required = {'foo': 'bar'} + view.group_required = {"foo": "bar"} with self.assertRaises(ImproperlyConfigured): view.get_group_required() def test_with_unicode(self): - self.view_class.group_required = 'niño' - self.assertEqual('niño', self.view_class.group_required) + self.view_class.group_required = "niño" + self.assertEqual("niño", self.view_class.group_required) user = self.build_authorized_user() group = user.groups.all()[0] - group.name = 'niño' + group.name = "niño" group.save() - self.assertEqual('niño', user.groups.all()[0].name) + self.assertEqual("niño", user.groups.all()[0].name) - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) - self.view_class.group_required = 'test_group' - self.assertEqual('test_group', self.view_class.group_required) + self.assertEqual("OK", force_str(resp.content)) + self.view_class.group_required = "test_group" + self.assertEqual("test_group", self.view_class.group_required) @pytest.mark.django_db class TestUserPassesTestMixin(_TestAccessBasicsMixin, test.TestCase): view_class = UserPassesTestView - view_url = '/user_passes_test/' + view_url = "/user_passes_test/" view_not_implemented_class = UserPassesTestNotImplementedView - view_not_implemented_url = '/user_passes_test_not_implemented/' + view_not_implemented_url = "/user_passes_test_not_implemented/" # for testing with passing and not passsing func_test def build_authorized_user(self, is_superuser=False): - return UserFactory(is_superuser=is_superuser, is_staff=True, - email="user@mydomain.com") + return UserFactory( + is_superuser=is_superuser, is_staff=True, email="user@mydomain.com" + ) def build_unauthorized_user(self): return UserFactory() def test_with_user_pass(self): user = self.build_authorized_user() - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual('OK', force_str(resp.content)) + self.assertEqual("OK", force_str(resp.content)) def test_with_user_not_pass(self): user = self.build_authorized_user(is_superuser=True) - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) - self.assertRedirects(resp, '/accounts/login/?next=/user_passes_test/') + self.assertRedirects(resp, "/accounts/login/?next=/user_passes_test/") def test_with_user_raise_exception(self): with self.assertRaises(PermissionDenied): self.dispatch_view( - self.build_request(path=self.view_url), raise_exception=True) + self.build_request(path=self.view_url), raise_exception=True + ) def test_not_implemented(self): view = self.view_not_implemented_class() with self.assertRaises(NotImplementedError): view.dispatch( self.build_request(path=self.view_not_implemented_url), - raise_exception=True) + raise_exception=True, + ) @pytest.mark.django_db class TestSSLRequiredMixin(test.TestCase): view_class = SSLRequiredView - view_url = '/sslrequired/' + view_url = "/sslrequired/" def test_ssl_redirection(self): - self.view_url = 'https://testserver' + self.view_url + self.view_url = "https://testserver" + self.view_url self.view_class.raise_exception = False resp = self.client.get(self.view_url) self.assertRedirects(resp, self.view_url, status_code=301) resp = self.client.get(self.view_url, follow=True) self.assertEqual(200, resp.status_code) - self.assertEqual('https', resp.request.get('wsgi.url_scheme')) + self.assertEqual("https", resp.request.get("wsgi.url_scheme")) def test_raises_exception(self): self.view_class.raise_exception = True @@ -663,7 +704,7 @@ def test_https_does_not_redirect(self): self.view_class.raise_exception = False resp = self.client.get(self.view_url, secure=True) self.assertEqual(200, resp.status_code) - self.assertEqual('https', resp.request.get('wsgi.url_scheme')) + self.assertEqual("https", resp.request.get("wsgi.url_scheme")) @pytest.mark.django_db @@ -671,26 +712,27 @@ class TestRecentLoginRequiredMixin(test.TestCase): """ Tests for RecentLoginRequiredMixin. """ + view_class = RecentLoginRequiredView - recent_view_url = '/recent_login/' - outdated_view_url = '/outdated_login/' + recent_view_url = "/recent_login/" + outdated_view_url = "/outdated_login/" def test_recent_login(self): self.view_class.max_last_login_delta = 1800 last_login = datetime.datetime.now() last_login = make_aware(last_login, get_current_timezone()) user = UserFactory(last_login=last_login) - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.recent_view_url) assert resp.status_code == 200 - assert force_str(resp.content) == 'OK' + assert force_str(resp.content) == "OK" def test_outdated_login(self): self.view_class.max_last_login_delta = 0 last_login = datetime.datetime.now() - datetime.timedelta(hours=2) last_login = make_aware(last_login, get_current_timezone()) user = UserFactory(last_login=last_login) - self.client.login(username=user.username, password='asdf1234') + self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.outdated_view_url) assert resp.status_code == 302 diff --git a/tests/test_ajax_mixins.py b/tests/test_ajax_mixins.py index bf266e4..135d7ea 100644 --- a/tests/test_ajax_mixins.py +++ b/tests/test_ajax_mixins.py @@ -10,15 +10,15 @@ from .factories import ArticleFactory, UserFactory from .helpers import TestViewHelper -from .views import (SimpleJsonView, JsonRequestResponseView, - JsonBadRequestView) +from .views import SimpleJsonView, JsonRequestResponseView, JsonBadRequestView class TestAjaxResponseMixin(TestViewHelper, test.TestCase): """ Tests for AjaxResponseMixin. """ - methods = ['get', 'post', 'put', 'delete'] + + methods = ["get", "post", "put", "delete"] def test_xhr(self): """ @@ -27,8 +27,10 @@ def test_xhr(self): # AjaxResponseView returns 'AJAX_OK' when requested with XmlHttpRequest for m in self.methods: fn = getattr(self.client, m) - resp = fn('/ajax_response/', HTTP_X_REQUESTED_WITH='XMLHttpRequest') - assert force_str(resp.content) == 'AJAX_OK' + resp = fn( + "/ajax_response/", HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) + assert force_str(resp.content) == "AJAX_OK" def test_not_xhr(self): """ @@ -37,18 +39,18 @@ def test_not_xhr(self): """ for m in self.methods: fn = getattr(self.client, m) - resp = fn('/ajax_response/') - assert force_str(resp.content) == 'OK' + resp = fn("/ajax_response/") + assert force_str(resp.content) == "OK" def test_fallback_to_normal_methods(self): """ Ajax methods should fallback to normal methods by default. """ test_cases = [ - ('get', 'get'), - ('post', 'post'), - ('put', 'get'), - ('delete', 'get'), + ("get", "get"), + ("post", "post"), + ("put", "get"), + ("delete", "get"), ] for ajax_method, fallback in test_cases: @@ -68,12 +70,14 @@ class TestJSONResponseMixin(TestViewHelper, test.TestCase): """ Tests for JSONResponseMixin. """ + view_class = SimpleJsonView def assert_json_response(self, resp, status_code=200): self.assertEqual(status_code, resp.status_code) - self.assertEqual('application/json', - resp['content-type'].split(';')[0]) + self.assertEqual( + "application/json", resp["content-type"].split(";")[0] + ) def get_content(self, url): """ @@ -89,9 +93,9 @@ def test_simple_json(self): Tests render_json_response() method. """ user = UserFactory() - self.client.login(username=user.username, password='asdf1234') - data = json.loads(self.get_content('/simple_json/')) - self.assertEqual({'username': user.username}, data) + self.client.login(username=user.username, password="asdf1234") + data = json.loads(self.get_content("/simple_json/")) + self.assertEqual({"username": user.username}, data) def test_serialization(self): """ @@ -99,14 +103,14 @@ def test_serialization(self): using django's serializer framework. """ a1, a2 = [ArticleFactory() for __ in range(2)] - data = json.loads(self.get_content('/article_list_json/')) + data = json.loads(self.get_content("/article_list_json/")) self.assertIsInstance(data, list) self.assertEqual(2, len(data)) titles = [] for row in data: # only title has been serialized - self.assertEqual(1, len(row['fields'])) - titles.append(row['fields']['title']) + self.assertEqual(1, len(row["fields"])) + titles.append(row["fields"]["title"]) self.assertIn(a1.title, titles) self.assertIn(a2.title, titles) @@ -117,7 +121,7 @@ def test_bad_content_type(self): attribute is not set correctly. """ with self.assertRaises(ImproperlyConfigured): - self.dispatch_view(self.build_request(), content_type=['a']) + self.dispatch_view(self.build_request(), content_type=["a"]) def test_pretty_json(self): """ @@ -125,12 +129,12 @@ def test_pretty_json(self): is longer than the normal one. """ user = UserFactory() - self.client.login(username=user.username, password='asfa') - normal_content = self.get_content('/simple_json/') - self.view_class.json_dumps_kwargs = {'indent': 2} - pretty_content = self.get_content('/simple_json/') - normal_json = json.loads('{0}'.format(normal_content)) - pretty_json = json.loads('{0}'.format(pretty_content)) + self.client.login(username=user.username, password="asfa") + normal_content = self.get_content("/simple_json/") + self.view_class.json_dumps_kwargs = {"indent": 2} + pretty_content = self.get_content("/simple_json/") + normal_json = json.loads("{0}".format(normal_content)) + pretty_json = json.loads("{0}".format(pretty_content)) self.assertEqual(normal_json, pretty_json) self.assertTrue(len(pretty_content) > len(normal_content)) @@ -138,25 +142,23 @@ def test_json_encoder_class_atrribute(self): """ Tests setting custom `json_encoder_class` attribute. """ - data = json.loads(self.get_content('/simple_json_custom_encoder/')) - self.assertEqual({'numbers': [1, 2, 3]}, data) + data = json.loads(self.get_content("/simple_json_custom_encoder/")) + self.assertEqual({"numbers": [1, 2, 3]}, data) class TestJsonRequestResponseMixin(TestViewHelper, test.TestCase): view_class = JsonRequestResponseView - request_dict = {'status': 'operational'} + request_dict = {"status": "operational"} def test_get_request_json_properly_formatted(self): """ Properly formatted JSON requests should result in a JSON object """ - data = json.dumps(self.request_dict).encode('utf-8') + data = json.dumps(self.request_dict).encode("utf-8") response = self.client.post( - '/json_request/', - content_type='application/json', - data=data + "/json_request/", content_type="application/json", data=data ) - response_json = json.loads(response.content.decode('utf-8')) + response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 200) self.assertEqual(response_json, self.request_dict) @@ -164,11 +166,8 @@ def test_get_request_json_improperly_formatted(self): """ Improperly formatted JSON requests should make request_json == None """ - response = self.client.post( - '/json_request/', - data=self.request_dict - ) - response_json = json.loads(response.content.decode('utf-8')) + response = self.client.post("/json_request/", data=self.request_dict) + response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 200) self.assertEqual(response_json, None) @@ -178,17 +177,16 @@ def test_bad_request_response_with_custom_error_message(self): or None, the client should get a 400 error """ response = self.client.post( - '/json_custom_bad_request/', - data=self.request_dict + "/json_custom_bad_request/", data=self.request_dict ) - response_json = json.loads(response.content.decode('utf-8')) + response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 400) - self.assertEqual(response_json, {'error': 'you messed up'}) + self.assertEqual(response_json, {"error": "you messed up"}) class TestJsonBadRequestMixin(TestViewHelper, test.TestCase): view_class = JsonBadRequestView - request_dict = {'status': 'operational'} + request_dict = {"status": "operational"} def test_bad_request_response(self): """ @@ -196,10 +194,9 @@ def test_bad_request_response(self): or None, the client should get a 400 error """ response = self.client.post( - '/json_bad_request/', - data=self.request_dict + "/json_bad_request/", data=self.request_dict ) - response_json = json.loads(response.content.decode('utf-8')) + response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 400) self.assertEqual(response_json, self.view_class.error_response_dict) @@ -211,8 +208,5 @@ def test_options_request_with_required_json_should_pass(self): * it's not possible to send HTTP body within an OPTIONS request * it's not up to an OPTIONS request to decide if the payload is valid """ - response = self.client.options( - '/json_bad_request/', - data=None - ) + response = self.client.options("/json_bad_request/", data=None) self.assertEqual(response.status_code, 200) diff --git a/tests/test_forms.py b/tests/test_forms.py index 23c5673..75926f9 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -8,6 +8,7 @@ class TestUserKwargModelFormMixin(test.TestCase): """ Tests for UserKwargModelFormMixin. """ + def test_without_user_kwarg(self): """ It should be possible to create form without 'user' kwarg. @@ -22,6 +23,6 @@ def test_with_user_kwarg(self): Form's 'user' attribute should be set to value passed as 'user' argument. """ - user = User(username='test') + user = User(username="test") form = forms.FormWithUserKwarg(user=user) assert form.user is user diff --git a/tests/test_other_mixins.py b/tests/test_other_mixins.py index 5115ba4..b1ec3df 100644 --- a/tests/test_other_mixins.py +++ b/tests/test_other_mixins.py @@ -11,71 +11,83 @@ from django.views.generic import View from django.utils.encoding import force_str -from braces.views import (SetHeadlineMixin, MessageMixin, FormValidMessageMixin, - FormInvalidMessageMixin) +from braces.views import ( + SetHeadlineMixin, + MessageMixin, + FormValidMessageMixin, + FormInvalidMessageMixin, +) from .factories import UserFactory from .helpers import TestViewHelper from .models import Article, CanonicalArticle -from .views import (ArticleListView, ArticleListViewWithCustomQueryset, - AuthorDetailView, OrderableListView, - FormMessagesView, ContextView) +from .views import ( + ArticleListView, + ArticleListViewWithCustomQueryset, + AuthorDetailView, + OrderableListView, + FormMessagesView, + ContextView, +) class TestSuccessURLRedirectListMixin(test.TestCase): """ Tests for SuccessURLRedirectListMixin. """ + def test_redirect(self): """ Test if browser is redirected to list view. """ - data = {'title': "Test body", 'body': "Test body"} - resp = self.client.post('/article_list/create/', data) - self.assertRedirects(resp, '/article_list/') + data = {"title": "Test body", "body": "Test body"} + resp = self.client.post("/article_list/create/", data) + self.assertRedirects(resp, "/article_list/") def test_no_url_name(self): """ Test that ImproperlyConfigured is raised. """ - data = {'title': "Test body", 'body': "Test body"} + data = {"title": "Test body", "body": "Test body"} with self.assertRaises(ImproperlyConfigured): - self.client.post('/article_list_bad/create/', data) + self.client.post("/article_list_bad/create/", data) class TestUserFormKwargsMixin(test.TestCase): """ Tests for UserFormKwargsMixin. """ + def test_post_method(self): user = UserFactory() - self.client.login(username=user.username, password='asdf1234') - resp = self.client.post('/form_with_user_kwarg/', {'field1': 'foo'}) + self.client.login(username=user.username, password="asdf1234") + resp = self.client.post("/form_with_user_kwarg/", {"field1": "foo"}) assert force_str(resp.content) == "username: %s" % user.username def test_get_method(self): user = UserFactory() - self.client.login(username=user.username, password='asdf1234') - resp = self.client.get('/form_with_user_kwarg/') - assert resp.context['form'].user == user + self.client.login(username=user.username, password="asdf1234") + resp = self.client.get("/form_with_user_kwarg/") + assert resp.context["form"].user == user class TestSetHeadlineMixin(test.TestCase): """ Tests for SetHeadlineMixin. """ + def test_dynamic_headline(self): """ Tests if get_headline() is called properly. """ - resp = self.client.get('/headline/test-headline/') - self.assertEqual('test-headline', resp.context['headline']) + resp = self.client.get("/headline/test-headline/") + self.assertEqual("test-headline", resp.context["headline"]) def test_context_data(self): """ Tests if mixin adds proper headline to template context. """ - resp = self.client.get('/headline/foo-bar/') - self.assertEqual("foo-bar", resp.context['headline']) + resp = self.client.get("/headline/foo-bar/") + self.assertEqual("foo-bar", resp.context["headline"]) def test_get_headline(self): """ @@ -89,27 +101,28 @@ def test_get_headline(self): self.assertEqual("Test headline", mixin.get_headline()) def test_get_headline_lazy(self): - resp = self.client.get('/headline/lazy/') - self.assertEqual('Test Headline', resp.context['headline']) + resp = self.client.get("/headline/lazy/") + self.assertEqual("Test Headline", resp.context["headline"]) class TestStaticContextMixin(test.TestCase): - """ Tests for StaticContextMixin. """ + """Tests for StaticContextMixin.""" + view_class = ContextView - view_url = '/context/' + view_url = "/context/" def test_dict(self): - self.view_class.static_context = {'test': True} + self.view_class.static_context = {"test": True} resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual(True, resp.context['test']) + self.assertEqual(True, resp.context["test"]) def test_two_tuple(self): - self.view_class.static_context = [('a', 1), ('b', 2)] + self.view_class.static_context = [("a", 1), ("b", 2)] resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) - self.assertEqual(1, resp.context['a']) - self.assertEqual(2, resp.context['b']) + self.assertEqual(1, resp.context["a"]) + self.assertEqual(2, resp.context["b"]) def test_not_set(self): self.view_class.static_context = None @@ -117,12 +130,12 @@ def test_not_set(self): self.client.get(self.view_url) def test_string_value_error(self): - self.view_class.static_context = 'Fail' + self.view_class.static_context = "Fail" with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) def test_list_error(self): - self.view_class.static_context = ['fail', 'fail'] + self.view_class.static_context = ["fail", "fail"] with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) @@ -131,6 +144,7 @@ class TestCsrfExemptMixin(test.TestCase): """ Tests for TestCsrfExemptMixin. """ + def setUp(self): super(TestCsrfExemptMixin, self).setUp() self.client = self.client_class(enforce_csrf_checks=True) @@ -139,7 +153,7 @@ def test_csrf_token_is_not_required(self): """ Tests if csrf token is not required. """ - resp = self.client.post('/csrf_exempt/', {'field1': 'test'}) + resp = self.client.post("/csrf_exempt/", {"field1": "test"}) self.assertEqual(200, resp.status_code) self.assertEqual("OK", force_str(resp.content)) @@ -162,24 +176,24 @@ def test_invalid_select_related(self): :return: """ with self.assertRaises(ImproperlyConfigured): - self.dispatch_view(self.build_request(), select_related={'a': 1}) + self.dispatch_view(self.build_request(), select_related={"a": 1}) - @mock.patch('django.db.models.query.QuerySet.select_related') + @mock.patch("django.db.models.query.QuerySet.select_related") def test_select_related_called(self, m): """ Checks if QuerySet's select_related() was called with correct arguments. """ qs = Article.objects.all() - m.return_value = qs.select_related('author') + m.return_value = qs.select_related("author") qs.select_related = m m.reset_mock() resp = self.dispatch_view(self.build_request()) self.assertEqual(200, resp.status_code) - m.assert_called_once_with('author') + m.assert_called_once_with("author") - @mock.patch('django.db.models.query.QuerySet.select_related') + @mock.patch("django.db.models.query.QuerySet.select_related") def test_select_related_keeps_select_related_from_queryset(self, m): """ Checks that an empty select_related attribute does not @@ -192,7 +206,8 @@ def test_select_related_keeps_select_related_from_queryset(self, m): with pytest.warns(UserWarning): resp = self.dispatch_view( self.build_request(), - view_class=ArticleListViewWithCustomQueryset) + view_class=ArticleListViewWithCustomQueryset, + ) self.assertEqual(200, resp.status_code) self.assertEqual(0, m.call_count) @@ -215,24 +230,24 @@ def test_invalid_prefetch_related(self): :return: """ with self.assertRaises(ImproperlyConfigured): - self.dispatch_view(self.build_request(), prefetch_related={'a': 1}) + self.dispatch_view(self.build_request(), prefetch_related={"a": 1}) - @mock.patch('django.db.models.query.QuerySet.prefetch_related') + @mock.patch("django.db.models.query.QuerySet.prefetch_related") def test_prefetch_related_called(self, m): """ Checks if QuerySet's prefetch_related() was called with correct arguments. """ qs = Article.objects.all() - m.return_value = qs.prefetch_related('article_set') + m.return_value = qs.prefetch_related("article_set") qs.prefetch_related = m m.reset_mock() resp = self.dispatch_view(self.build_request()) self.assertEqual(200, resp.status_code) - m.assert_called_once_with('article_set') + m.assert_called_once_with("article_set") - @mock.patch('django.db.models.query.QuerySet.prefetch_related') + @mock.patch("django.db.models.query.QuerySet.prefetch_related") def test_prefetch_related_keeps_select_related_from_queryset(self, m): """ Checks that an empty prefetch_related attribute does not @@ -245,7 +260,8 @@ def test_prefetch_related_keeps_select_related_from_queryset(self, m): with pytest.warns(UserWarning): resp = self.dispatch_view( self.build_request(), - view_class=ArticleListViewWithCustomQueryset) + view_class=ArticleListViewWithCustomQueryset, + ) self.assertEqual(200, resp.status_code) self.assertEqual(0, m.call_count) @@ -254,8 +270,8 @@ class TestOrderableListMixin(TestViewHelper, test.TestCase): view_class = OrderableListView def __make_test_articles(self): - a1 = Article.objects.create(title='Alpha', body='Zet') - a2 = Article.objects.create(title='Zet', body='Alpha') + a1 = Article.objects.create(title="Alpha", body="Zet") + a2 = Article.objects.create(title="Zet", body="Alpha") return a1, a2 def test_correct_order(self): @@ -265,16 +281,24 @@ def test_correct_order(self): a1, a2 = self.__make_test_articles() resp = self.dispatch_view( - self.build_request(path='?order_by=title&ordering=asc'), + self.build_request(path="?order_by=title&ordering=asc"), orderable_columns=None, - get_orderable_columns=lambda: ('id', 'title', )) - self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) + get_orderable_columns=lambda: ( + "id", + "title", + ), + ) + self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) resp = self.dispatch_view( - self.build_request(path='?order_by=id&ordering=desc'), + self.build_request(path="?order_by=id&ordering=desc"), orderable_columns=None, - get_orderable_columns=lambda: ('id', 'title', )) - self.assertEqual(list(resp.context_data['object_list']), [a2, a1]) + get_orderable_columns=lambda: ( + "id", + "title", + ), + ) + self.assertEqual(list(resp.context_data["object_list"]), [a2, a1]) def test_correct_order_with_default_ordering(self): """ @@ -284,25 +308,37 @@ def test_correct_order_with_default_ordering(self): a1, a2 = self.__make_test_articles() resp = self.dispatch_view( - self.build_request(path='?order_by=id'), + self.build_request(path="?order_by=id"), orderable_columns=None, ordering_default=None, - get_orderable_columns=lambda: ('id', 'title', )) - self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) + get_orderable_columns=lambda: ( + "id", + "title", + ), + ) + self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) resp = self.dispatch_view( - self.build_request(path='?order_by=id'), + self.build_request(path="?order_by=id"), orderable_columns=None, ordering_default="asc", - get_orderable_columns=lambda: ('id', 'title', )) - self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) + get_orderable_columns=lambda: ( + "id", + "title", + ), + ) + self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) resp = self.dispatch_view( - self.build_request(path='?order_by=id'), + self.build_request(path="?order_by=id"), orderable_columns=None, ordering_default="desc", - get_orderable_columns=lambda: ('id', 'title', )) - self.assertEqual(list(resp.context_data['object_list']), [a2, a1]) + get_orderable_columns=lambda: ( + "id", + "title", + ), + ) + self.assertEqual(list(resp.context_data["object_list"]), [a2, a1]) def test_correct_order_with_param_not_default_ordering(self): """ @@ -313,11 +349,15 @@ def test_correct_order_with_param_not_default_ordering(self): a1, a2 = self.__make_test_articles() resp = self.dispatch_view( - self.build_request(path='?order_by=id&ordering=asc'), + self.build_request(path="?order_by=id&ordering=asc"), orderable_columns=None, - ordering_default='desc', - get_orderable_columns=lambda: ('id', 'title', )) - self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) + ordering_default="desc", + get_orderable_columns=lambda: ( + "id", + "title", + ), + ) + self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) def test_correct_order_with_incorrect_default_ordering(self): """ @@ -326,8 +366,9 @@ def test_correct_order_with_incorrect_default_ordering(self): """ view = self.view_class() view.ordering_default = "improper_default_value" - self.assertRaises(ImproperlyConfigured, - lambda: view.get_ordering_default()) + self.assertRaises( + ImproperlyConfigured, lambda: view.get_ordering_default() + ) def test_default_column(self): """ @@ -337,7 +378,7 @@ def test_default_column(self): a1, a2 = self.__make_test_articles() resp = self.dispatch_view(self.build_request()) - self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) + self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) def test_get_orderable_columns_returns_correct_values(self): """ @@ -348,8 +389,9 @@ def test_get_orderable_columns_returns_correct_values(self): view = self.view_class() self.assertEqual(view.get_orderable_columns(), view.orderable_columns) view.orderable_columns = None - self.assertRaises(ImproperlyConfigured, - lambda: view.get_orderable_columns()) + self.assertRaises( + ImproperlyConfigured, lambda: view.get_orderable_columns() + ) def test_get_orderable_columns_default_returns_correct_values(self): """ @@ -358,11 +400,14 @@ def test_get_orderable_columns_default_returns_correct_values(self): ImproperlyConfigured exception in the attribute is None """ view = self.view_class() - self.assertEqual(view.get_orderable_columns_default(), - view.orderable_columns_default) + self.assertEqual( + view.get_orderable_columns_default(), + view.orderable_columns_default, + ) view.orderable_columns_default = None - self.assertRaises(ImproperlyConfigured, - lambda: view.get_orderable_columns_default()) + self.assertRaises( + ImproperlyConfigured, lambda: view.get_orderable_columns_default() + ) def test_only_allowed_columns(self): """ @@ -372,50 +417,51 @@ def test_only_allowed_columns(self): a1, a2 = self.__make_test_articles() resp = self.dispatch_view( - self.build_request(path='?order_by=body&ordering=asc'), + self.build_request(path="?order_by=body&ordering=asc"), orderable_columns_default=None, - get_orderable_columns_default=lambda: 'title') - self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) + get_orderable_columns_default=lambda: "title", + ) + self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) class TestCanonicalSlugDetailView(test.TestCase): def setUp(self): - Article.objects.create(title='Alpha', body='Zet', slug='alpha') - Article.objects.create(title='Zet', body='Alpha', slug='zet') + Article.objects.create(title="Alpha", body="Zet", slug="alpha") + Article.objects.create(title="Zet", body="Alpha", slug="zet") def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical. """ - resp = self.client.get('/article-canonical/1-alpha/') + resp = self.client.get("/article-canonical/1-alpha/") self.assertEqual(resp.status_code, 200) - resp = self.client.get('/article-canonical/2-zet/') + resp = self.client.get("/article-canonical/2-zet/") self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ - resp = self.client.get('/article-canonical/1-bad-slug/') + resp = self.client.get("/article-canonical/1-bad-slug/") self.assertEqual(resp.status_code, 301) - resp = self.client.get('/article-canonical/2-bad-slug/') + resp = self.client.get("/article-canonical/2-bad-slug/") self.assertEqual(resp.status_code, 301) class TestNamespaceAwareCanonicalSlugDetailView(test.TestCase): def setUp(self): - Article.objects.create(title='Alpha', body='Zet', slug='alpha') - Article.objects.create(title='Zet', body='Alpha', slug='zet') + Article.objects.create(title="Alpha", body="Zet", slug="alpha") + Article.objects.create(title="Zet", body="Alpha", slug="zet") def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical. """ resp = self.client.get( - '/article-canonical-namespaced/article/1-alpha/') + "/article-canonical-namespaced/article/1-alpha/" + ) self.assertEqual(resp.status_code, 200) - resp = self.client.get( - '/article-canonical-namespaced/article/2-zet/') + resp = self.client.get("/article-canonical-namespaced/article/2-zet/") self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): @@ -424,99 +470,102 @@ def test_non_canonical_slug(self): redirect is namespace aware. """ resp = self.client.get( - '/article-canonical-namespaced/article/1-bad-slug/') + "/article-canonical-namespaced/article/1-bad-slug/" + ) self.assertEqual(resp.status_code, 301) resp = self.client.get( - '/article-canonical-namespaced/article/2-bad-slug/') + "/article-canonical-namespaced/article/2-bad-slug/" + ) self.assertEqual(resp.status_code, 301) class TestOverriddenCanonicalSlugDetailView(test.TestCase): def setUp(self): - Article.objects.create(title='Alpha', body='Zet', slug='alpha') - Article.objects.create(title='Zet', body='Alpha', slug='zet') + Article.objects.create(title="Alpha", body="Zet", slug="alpha") + Article.objects.create(title="Zet", body="Alpha", slug="zet") def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical according to the overridden canonical slug. """ - resp = self.client.get('/article-canonical-override/1-nycun/') + resp = self.client.get("/article-canonical-override/1-nycun/") self.assertEqual(resp.status_code, 200) - resp = self.client.get('/article-canonical-override/2-mrg/') + resp = self.client.get("/article-canonical-override/2-mrg/") self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ - resp = self.client.get('/article-canonical-override/1-bad-slug/') + resp = self.client.get("/article-canonical-override/1-bad-slug/") self.assertEqual(resp.status_code, 301) - resp = self.client.get('/article-canonical-override/2-bad-slug/') + resp = self.client.get("/article-canonical-override/2-bad-slug/") self.assertEqual(resp.status_code, 301) class TestCustomUrlKwargsCanonicalSlugDetailView(test.TestCase): def setUp(self): - Article.objects.create(title='Alpha', body='Zet', slug='alpha') - Article.objects.create(title='Zet', body='Alpha', slug='zet') + Article.objects.create(title="Alpha", body="Zet", slug="alpha") + Article.objects.create(title="Zet", body="Alpha", slug="zet") def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical """ - resp = self.client.get('/article-canonical-custom-kwargs/1-alpha/') + resp = self.client.get("/article-canonical-custom-kwargs/1-alpha/") self.assertEqual(resp.status_code, 200) - resp = self.client.get('/article-canonical-custom-kwargs/2-zet/') + resp = self.client.get("/article-canonical-custom-kwargs/2-zet/") self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ - resp = self.client.get('/article-canonical-custom-kwargs/1-bad-slug/') + resp = self.client.get("/article-canonical-custom-kwargs/1-bad-slug/") self.assertEqual(resp.status_code, 301) - resp = self.client.get('/article-canonical-custom-kwargs/2-bad-slug/') + resp = self.client.get("/article-canonical-custom-kwargs/2-bad-slug/") self.assertEqual(resp.status_code, 301) class TestModelCanonicalSlugDetailView(test.TestCase): def setUp(self): CanonicalArticle.objects.create( - title='Alpha', body='Zet', slug='alpha') - CanonicalArticle.objects.create( - title='Zet', body='Alpha', slug='zet') + title="Alpha", body="Zet", slug="alpha" + ) + CanonicalArticle.objects.create(title="Zet", body="Alpha", slug="zet") def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical according to the model's canonical slug. """ - resp = self.client.get('/article-canonical-model/1-unauthored-alpha/') + resp = self.client.get("/article-canonical-model/1-unauthored-alpha/") self.assertEqual(resp.status_code, 200) - resp = self.client.get('/article-canonical-model/2-unauthored-zet/') + resp = self.client.get("/article-canonical-model/2-unauthored-zet/") self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ - resp = self.client.get('/article-canonical-model/1-bad-slug/') + resp = self.client.get("/article-canonical-model/1-bad-slug/") self.assertEqual(resp.status_code, 301) - resp = self.client.get('/article-canonical-model/2-bad-slug/') + resp = self.client.get("/article-canonical-model/2-bad-slug/") self.assertEqual(resp.status_code, 301) # CookieStorage is used because it doesn't require middleware to be installed @override_settings( - MESSAGE_STORAGE='django.contrib.messages.storage.cookie.CookieStorage') + MESSAGE_STORAGE="django.contrib.messages.storage.cookie.CookieStorage" +) class MessageMixinTests(test.TestCase): def setUp(self): self.rf = test.RequestFactory() self.middleware = MessageMiddleware() def get_request(self, *args, **kwargs): - request = self.rf.get('/') + request = self.rf.get("/") self.middleware.process_request(request) return request @@ -533,22 +582,23 @@ def get_request_response(self, view, *args, **kwargs): def test_add_messages(self): class TestView(MessageMixin, View): def get(self, request): - self.messages.add_message(messages.SUCCESS, 'test') - return HttpResponse('OK') + self.messages.add_message(messages.SUCCESS, "test") + return HttpResponse("OK") request, response = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0].message, 'test') + self.assertEqual(msg[0].message, "test") self.assertEqual(msg[0].level, messages.SUCCESS) def test_get_messages(self): class TestView(MessageMixin, View): def get(self, request): - self.messages.add_message(messages.SUCCESS, 'success') - self.messages.add_message(messages.WARNING, 'warning') - content = ','.join( - m.message for m in self.messages.get_messages()) + self.messages.add_message(messages.SUCCESS, "success") + self.messages.add_message(messages.WARNING, "warning") + content = ",".join( + m.message for m in self.messages.get_messages() + ) return HttpResponse(content) _, response = self.get_request_response(TestView.as_view()) @@ -566,75 +616,75 @@ def test_set_level(self): class TestView(MessageMixin, View): def get(self, request): self.messages.set_level(messages.WARNING) - self.messages.add_message(messages.SUCCESS, 'success') - self.messages.add_message(messages.WARNING, 'warning') - return HttpResponse('OK') + self.messages.add_message(messages.SUCCESS, "success") + self.messages.add_message(messages.WARNING, "warning") + return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) - self.assertEqual(msg, [Message(messages.WARNING, 'warning')]) + self.assertEqual(msg, [Message(messages.WARNING, "warning")]) @override_settings(MESSAGE_LEVEL=messages.DEBUG) def test_debug(self): class TestView(MessageMixin, View): def get(self, request): self.messages.debug("test") - return HttpResponse('OK') + return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0], Message(messages.DEBUG, 'test')) + self.assertEqual(msg[0], Message(messages.DEBUG, "test")) def test_info(self): class TestView(MessageMixin, View): def get(self, request): self.messages.info("test") - return HttpResponse('OK') + return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0], Message(messages.INFO, 'test')) + self.assertEqual(msg[0], Message(messages.INFO, "test")) def test_success(self): class TestView(MessageMixin, View): def get(self, request): self.messages.success("test") - return HttpResponse('OK') + return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0], Message(messages.SUCCESS, 'test')) + self.assertEqual(msg[0], Message(messages.SUCCESS, "test")) def test_warning(self): class TestView(MessageMixin, View): def get(self, request): self.messages.warning("test") - return HttpResponse('OK') + return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0], Message(messages.WARNING, 'test')) + self.assertEqual(msg[0], Message(messages.WARNING, "test")) def test_error(self): class TestView(MessageMixin, View): def get(self, request): self.messages.error("test") - return HttpResponse('OK') + return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0], Message(messages.ERROR, 'test')) + self.assertEqual(msg[0], Message(messages.ERROR, "test")) def test_invalid_attribute(self): class TestView(MessageMixin, View): def get(self, request): self.messages.invalid() - return HttpResponse('OK') + return HttpResponse("OK") with self.assertRaises(AttributeError): self.get_request_response(TestView.as_view()) @@ -644,18 +694,19 @@ def test_wrapper_available_in_dispatch(self): Make sure that self.messages is available in dispatch() even before calling the parent's implementation. """ + class TestView(MessageMixin, View): def dispatch(self, request): - self.messages.add_message(messages.SUCCESS, 'test') + self.messages.add_message(messages.SUCCESS, "test") return super(TestView, self).dispatch(request) def get(self, request): - return HttpResponse('OK') + return HttpResponse("OK") request, response = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) - self.assertEqual(msg[0].message, 'test') + self.assertEqual(msg[0].message, "test") self.assertEqual(msg[0].level, messages.SUCCESS) def test_API(self): @@ -665,21 +716,16 @@ def test_API(self): # This test is designed to break when django.contrib.messages.api # changes (items being added or removed). excluded_API = set() - excluded_API.add('MessageFailure') + excluded_API.add("MessageFailure") class TestFormMessageMixins(test.TestCase): def setUp(self): - self.good_data = { - 'title': 'Good', - 'body': 'Body' - } - self.bad_data = { - 'body': 'Missing title' - } + self.good_data = {"title": "Good", "body": "Body"} + self.bad_data = {"body": "Missing title"} def test_valid_message(self): - url = '/form_messages/' + url = "/form_messages/" response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -688,7 +734,7 @@ def test_valid_message(self): self.assertContains(response, FormMessagesView().form_valid_message) def test_invalid_message(self): - url = '/form_messages/' + url = "/form_messages/" response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -703,14 +749,14 @@ def test_form_valid_message_not_set(self): def test_form_valid_message_not_str(self): mixin = FormValidMessageMixin() - mixin.form_valid_message = ['bad'] + mixin.form_valid_message = ["bad"] with self.assertRaises(ImproperlyConfigured): mixin.get_form_valid_message() def test_form_valid_returns_message(self): mixin = FormValidMessageMixin() - mixin.form_valid_message = 'Good øø' - self.assertEqual(force_str('Good øø'), mixin.get_form_valid_message()) + mixin.form_valid_message = "Good øø" + self.assertEqual(force_str("Good øø"), mixin.get_form_valid_message()) def test_form_invalid_message_not_set(self): mixin = FormInvalidMessageMixin() @@ -719,14 +765,14 @@ def test_form_invalid_message_not_set(self): def test_form_invalid_message_not_str(self): mixin = FormInvalidMessageMixin() - mixin.form_invalid_message = ['bad'] + mixin.form_invalid_message = ["bad"] with self.assertRaises(ImproperlyConfigured): mixin.get_form_invalid_message() def test_form_invalid_returns_message(self): mixin = FormInvalidMessageMixin() - mixin.form_invalid_message = 'Bad øø' - self.assertEqual(force_str('Bad øø'), mixin.get_form_invalid_message()) + mixin.form_invalid_message = "Bad øø" + self.assertEqual(force_str("Bad øø"), mixin.get_form_invalid_message()) class TestAllVerbsMixin(test.TestCase): @@ -760,21 +806,20 @@ def test_delete(self): def test_no_all_handler(self): with self.assertRaises(ImproperlyConfigured): - self.client.get('/all_verbs_no_handler/') + self.client.get("/all_verbs_no_handler/") class TestHeaderMixin(test.TestCase): - def test_attribute(self): - response = self.client.get('/headers/attribute/') - self.assertEqual(response['X-DJANGO-BRACES-1'], '1') - self.assertEqual(response['X-DJANGO-BRACES-2'], '2') + response = self.client.get("/headers/attribute/") + self.assertEqual(response["X-DJANGO-BRACES-1"], "1") + self.assertEqual(response["X-DJANGO-BRACES-2"], "2") def test_method(self): - response = self.client.get('/headers/method/') - self.assertEqual(response['X-DJANGO-BRACES-1'], '1') - self.assertEqual(response['X-DJANGO-BRACES-2'], '2') + response = self.client.get("/headers/method/") + self.assertEqual(response["X-DJANGO-BRACES-1"], "1") + self.assertEqual(response["X-DJANGO-BRACES-2"], "2") def test_existing(self): - response = self.client.get('/headers/existing/') - self.assertEqual(response['X-DJANGO-BRACES-EXISTING'], 'value') + response = self.client.get("/headers/existing/") + self.assertEqual(response["X-DJANGO-BRACES-EXISTING"], "value") diff --git a/tests/urls.py b/tests/urls.py index 276616c..3f84a53 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -5,121 +5,134 @@ urlpatterns = [ # LoginRequiredMixin tests - re_path(r'^login_required/$', views.LoginRequiredView.as_view()), - + re_path(r"^login_required/$", views.LoginRequiredView.as_view()), # AnonymousRequiredView tests - re_path(r'^unauthenticated_view/$', views.AnonymousRequiredView.as_view(), - name='unauthenticated_view'), - re_path(r'^authenticated_view/$', views.AuthenticatedView.as_view(), - name='authenticated_view'), - + re_path( + r"^unauthenticated_view/$", + views.AnonymousRequiredView.as_view(), + name="unauthenticated_view", + ), + re_path( + r"^authenticated_view/$", + views.AuthenticatedView.as_view(), + name="authenticated_view", + ), # AjaxResponseMixin tests - re_path(r'^ajax_response/$', views.AjaxResponseView.as_view()), - + re_path(r"^ajax_response/$", views.AjaxResponseView.as_view()), # CreateAndRedirectToEditView tests - re_path(r'^article/create/$', views.CreateArticleView.as_view()), - re_path(r'^article/(?P\d+)/edit/$', views.EditArticleView.as_view(), - name="edit_article"), - - re_path(r'^article_list/create/$', - views.CreateArticleAndRedirectToListView.as_view()), - re_path(r'^article_list_bad/create/$', - views.CreateArticleAndRedirectToListViewBad.as_view()), - re_path(r'^article_list/$', views.ArticleListView.as_view(), - name='article_list'), - + re_path(r"^article/create/$", views.CreateArticleView.as_view()), + re_path( + r"^article/(?P\d+)/edit/$", + views.EditArticleView.as_view(), + name="edit_article", + ), + re_path( + r"^article_list/create/$", + views.CreateArticleAndRedirectToListView.as_view(), + ), + re_path( + r"^article_list_bad/create/$", + views.CreateArticleAndRedirectToListViewBad.as_view(), + ), + re_path( + r"^article_list/$", + views.ArticleListView.as_view(), + name="article_list", + ), # CanonicalSlugDetailMixin tests - re_path(r'^article-canonical/(?P\d+)-(?P[-\w]+)/$', + re_path( + r"^article-canonical/(?P\d+)-(?P[-\w]+)/$", views.CanonicalSlugDetailView.as_view(), - name="canonical_slug"), - re_path(r'^article-canonical-override/(?P\d+)-(?P[-\w]+)/$', + name="canonical_slug", + ), + re_path( + r"^article-canonical-override/(?P\d+)-(?P[-\w]+)/$", views.OverriddenCanonicalSlugDetailView.as_view(), - name="canonical_override"), - re_path(r'^article-canonical-custom-kwargs/(?P\d+)-(?P[-\w]+)/$', + name="canonical_override", + ), + re_path( + r"^article-canonical-custom-kwargs/(?P\d+)-(?P[-\w]+)/$", views.CanonicalSlugDetailCustomUrlKwargsView.as_view(), - name="canonical_custom_kwargs"), - re_path(r'^article-canonical-model/(?P\d+)-(?P[-\w]+)/$', + name="canonical_custom_kwargs", + ), + re_path( + r"^article-canonical-model/(?P\d+)-(?P[-\w]+)/$", views.ModelCanonicalSlugDetailView.as_view(), - name="canonical_model"), - + name="canonical_model", + ), # UserFormKwargsMixin tests - re_path(r'^form_with_user_kwarg/$', views.FormWithUserKwargView.as_view()), - + re_path(r"^form_with_user_kwarg/$", views.FormWithUserKwargView.as_view()), # SetHeadlineMixin tests - re_path(r'^headline/$', views.HeadlineView.as_view(), name='headline'), - re_path(r'^headline/lazy/$', views.LazyHeadlineView.as_view()), - re_path(r'^headline/(?P[\w-]+)/$', views.DynamicHeadlineView.as_view()), - + re_path(r"^headline/$", views.HeadlineView.as_view(), name="headline"), + re_path(r"^headline/lazy/$", views.LazyHeadlineView.as_view()), + re_path(r"^headline/(?P[\w-]+)/$", views.DynamicHeadlineView.as_view()), # ExtraContextMixin tests - re_path(r'^context/$', views.ContextView.as_view(), name='context'), - + re_path(r"^context/$", views.ContextView.as_view(), name="context"), # PermissionRequiredMixin tests - re_path(r'^permission_required/$', views.PermissionRequiredView.as_view()), - + re_path(r"^permission_required/$", views.PermissionRequiredView.as_view()), # MultiplePermissionsRequiredMixin tests - re_path(r'^multiple_permissions_required/$', - views.MultiplePermissionsRequiredView.as_view()), - + re_path( + r"^multiple_permissions_required/$", + views.MultiplePermissionsRequiredView.as_view(), + ), # SuperuserRequiredMixin tests - re_path(r'^superuser_required/$', views.SuperuserRequiredView.as_view()), - + re_path(r"^superuser_required/$", views.SuperuserRequiredView.as_view()), # StaffuserRequiredMixin tests - re_path(r'^staffuser_required/$', views.StaffuserRequiredView.as_view()), - + re_path(r"^staffuser_required/$", views.StaffuserRequiredView.as_view()), # GroupRequiredMixin tests - re_path(r'^group_required/$', views.GroupRequiredView.as_view()), - + re_path(r"^group_required/$", views.GroupRequiredView.as_view()), # UserPassesTestMixin tests - re_path(r'^user_passes_test/$', views.UserPassesTestView.as_view()), - + re_path(r"^user_passes_test/$", views.UserPassesTestView.as_view()), # UserPassesTestMixin tests - re_path(r'^user_passes_test_not_implemented/$', - views.UserPassesTestNotImplementedView.as_view()), - + re_path( + r"^user_passes_test_not_implemented/$", + views.UserPassesTestNotImplementedView.as_view(), + ), # CsrfExemptMixin tests - re_path(r'^csrf_exempt/$', views.CsrfExemptView.as_view()), - + re_path(r"^csrf_exempt/$", views.CsrfExemptView.as_view()), # JSONResponseMixin tests - re_path(r'^simple_json/$', views.SimpleJsonView.as_view()), - re_path(r'^simple_json_custom_encoder/$', - views.CustomJsonEncoderView.as_view()), - re_path(r'^simple_json_400/$', views.SimpleJsonBadRequestView.as_view()), - re_path(r'^article_list_json/$', views.ArticleListJsonView.as_view()), - + re_path(r"^simple_json/$", views.SimpleJsonView.as_view()), + re_path( + r"^simple_json_custom_encoder/$", views.CustomJsonEncoderView.as_view() + ), + re_path(r"^simple_json_400/$", views.SimpleJsonBadRequestView.as_view()), + re_path(r"^article_list_json/$", views.ArticleListJsonView.as_view()), # JsonRequestResponseMixin tests - re_path(r'^json_request/$', views.JsonRequestResponseView.as_view()), - re_path(r'^json_bad_request/$', views.JsonBadRequestView.as_view()), - re_path(r'^json_custom_bad_request/$', - views.JsonCustomBadRequestView.as_view()), - + re_path(r"^json_request/$", views.JsonRequestResponseView.as_view()), + re_path(r"^json_bad_request/$", views.JsonBadRequestView.as_view()), + re_path( + r"^json_custom_bad_request/$", views.JsonCustomBadRequestView.as_view() + ), # FormMessagesMixin tests - re_path(r'form_messages/$', views.FormMessagesView.as_view()), - + re_path(r"form_messages/$", views.FormMessagesView.as_view()), # AllVerbsMixin tests - re_path(r'all_verbs/$', views.AllVerbsView.as_view()), - re_path(r'all_verbs_no_handler/$', - views.AllVerbsView.as_view(all_handler=None)), - + re_path(r"all_verbs/$", views.AllVerbsView.as_view()), + re_path( + r"all_verbs_no_handler/$", views.AllVerbsView.as_view(all_handler=None) + ), # SSLRequiredMixin tests - re_path(r'^sslrequired/$', views.SSLRequiredView.as_view()), - + re_path(r"^sslrequired/$", views.SSLRequiredView.as_view()), # RecentLoginRequiredMixin tests - re_path(r'^recent_login/$', views.RecentLoginRequiredView.as_view()), - re_path(r'^outdated_login/$', views.RecentLoginRequiredView.as_view()), - + re_path(r"^recent_login/$", views.RecentLoginRequiredView.as_view()), + re_path(r"^outdated_login/$", views.RecentLoginRequiredView.as_view()), # HeaderMixin tests - re_path(r'^headers/attribute/$', views.AttributeHeaderView.as_view()), - re_path(r'^headers/method/$', views.MethodHeaderView.as_view()), - re_path(r'^headers/existing/$', views.ExistingHeaderView.as_view()), + re_path(r"^headers/attribute/$", views.AttributeHeaderView.as_view()), + re_path(r"^headers/method/$", views.MethodHeaderView.as_view()), + re_path(r"^headers/existing/$", views.ExistingHeaderView.as_view()), ] urlpatterns += [ - re_path(r'^accounts/login/$', LoginView.as_view(template_name='blank.html')), - re_path(r'^auth/login/$', LoginView.as_view(template_name='blank.html')), + re_path( + r"^accounts/login/$", LoginView.as_view(template_name="blank.html") + ), + re_path(r"^auth/login/$", LoginView.as_view(template_name="blank.html")), ] urlpatterns += [ - re_path(r'^article-canonical-namespaced/', - include(('tests.urls_namespaced', 'tests'), - namespace='some_namespace')), + re_path( + r"^article-canonical-namespaced/", + include( + ("tests.urls_namespaced", "tests"), namespace="some_namespace" + ), + ), ] diff --git a/tests/urls_namespaced.py b/tests/urls_namespaced.py index cc716be..34b9d7a 100644 --- a/tests/urls_namespaced.py +++ b/tests/urls_namespaced.py @@ -4,8 +4,9 @@ urlpatterns = [ # CanonicalSlugDetailMixin namespace tests - re_path(r'^article/(?P\d+)-(?P[\w-]+)/$', + re_path( + r"^article/(?P\d+)-(?P[\w-]+)/$", views.CanonicalSlugDetailView.as_view(), - name="namespaced_article"), + name="namespaced_article", + ), ] - diff --git a/tests/views.py b/tests/views.py index 59b67a0..64f333c 100644 --- a/tests/views.py +++ b/tests/views.py @@ -3,8 +3,15 @@ from django.contrib.auth.models import User from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ -from django.views.generic import (View, UpdateView, FormView, TemplateView, - ListView, DetailView, CreateView) +from django.views.generic import ( + View, + UpdateView, + FormView, + TemplateView, + ListView, + DetailView, + CreateView, +) from braces import views @@ -17,6 +24,7 @@ class OkView(View): """ A view which simply returns "OK" for every request. """ + def get(self, request): return HttpResponse("OK") @@ -42,7 +50,8 @@ class AnonymousRequiredView(views.AnonymousRequiredMixin, OkView): unauthenticated users and redirect authenticated users to the authenticated_redirect_url set on the view. """ - authenticated_redirect_url = '/authenticated_view/' + + authenticated_redirect_url = "/authenticated_view/" class AuthenticatedView(views.LoginRequiredMixin, OkView): @@ -56,6 +65,7 @@ class AjaxResponseView(views.AjaxResponseMixin, OkView): """ A view for testing AjaxResponseMixin. """ + def get_ajax(self, request): return HttpResponse("AJAX_OK") @@ -73,8 +83,9 @@ class SimpleJsonView(views.JSONResponseMixin, View): """ A view for testing JSONResponseMixin's render_json_response() method. """ + def get(self, request): - object = {'username': request.user.username} + object = {"username": request.user.username} return self.render_json_response(object) @@ -83,10 +94,11 @@ class CustomJsonEncoderView(views.JSONResponseMixin, View): A view for testing JSONResponseMixin's `json_encoder_class` attribute with custom JSONEncoder class. """ + json_encoder_class = SetJSONEncoder def get(self, request): - object = {'numbers': set([1, 2, 3])} + object = {"numbers": set([1, 2, 3])} return self.render_json_response(object) @@ -95,8 +107,9 @@ class SimpleJsonBadRequestView(views.JSONResponseMixin, View): A view for testing JSONResponseMixin's render_json_response() method with 400 HTTP status code. """ + def get(self, request): - object = {'username': request.user.username} + object = {"username": request.user.username} return self.render_json_response(object, status=400) @@ -105,16 +118,17 @@ class ArticleListJsonView(views.JSONResponseMixin, View): A view for testing JSONResponseMixin's render_json_object_response() method. """ + def get(self, request): queryset = Article.objects.all() - return self.render_json_object_response( - queryset, fields=('title',)) + return self.render_json_object_response(queryset, fields=("title",)) class JsonRequestResponseView(views.JsonRequestResponseMixin, View): """ A view for testing JsonRequestResponseMixin's json conversion """ + def post(self, request): return self.render_json_response(self.request_json) @@ -124,6 +138,7 @@ class JsonBadRequestView(views.JsonRequestResponseMixin, View): A view for testing JsonRequestResponseMixin's require_json and render_bad_request_response methods """ + require_json = True def post(self, request, *args, **kwargs): @@ -135,10 +150,10 @@ class JsonCustomBadRequestView(views.JsonRequestResponseMixin, View): A view for testing JsonRequestResponseMixin's render_bad_request_response method with a custom error message """ + def post(self, request, *args, **kwargs): if not self.request_json: - return self.render_bad_request_response( - {'error': 'you messed up'}) + return self.render_bad_request_response({"error": "you messed up"}) return self.render_json_response(self.request_json) @@ -146,32 +161,38 @@ class CreateArticleView(CreateView): """ View for testing CreateAndRedirectEditToView. """ - fields = ['author', 'title', 'body', 'slug'] + + fields = ["author", "title", "body", "slug"] model = Article - template_name = 'form.html' + template_name = "form.html" class EditArticleView(UpdateView): """ View for testing CreateAndRedirectEditToView. """ + model = Article - template_name = 'form.html' + template_name = "form.html" -class CreateArticleAndRedirectToListView(views.SuccessURLRedirectListMixin, - CreateArticleView): +class CreateArticleAndRedirectToListView( + views.SuccessURLRedirectListMixin, CreateArticleView +): """ View for testing SuccessURLRedirectListMixin """ - success_list_url = 'article_list' + + success_list_url = "article_list" -class CreateArticleAndRedirectToListViewBad(views.SuccessURLRedirectListMixin, - CreateArticleView): +class CreateArticleAndRedirectToListViewBad( + views.SuccessURLRedirectListMixin, CreateArticleView +): """ View for testing SuccessURLRedirectListMixin """ + success_list_url = None @@ -181,18 +202,21 @@ class ArticleListView(views.SelectRelatedMixin, ListView): Also used to test SelectRelatedMixin. """ + model = Article - template_name = 'blank.html' - select_related = ('author',) + template_name = "blank.html" + select_related = ("author",) class ArticleListViewWithCustomQueryset(views.SelectRelatedMixin, ListView): """ Another list view for articles, required to test SelectRelatedMixin. """ - queryset = Article.objects.select_related('author').prefetch_related( - 'article_set') - template_name = 'blank.html' + + queryset = Article.objects.select_related("author").prefetch_related( + "article_set" + ) + template_name = "blank.html" select_related = () @@ -200,8 +224,9 @@ class FormWithUserKwargView(views.UserFormKwargsMixin, FormView): """ View for testing UserFormKwargsMixin. """ + form_class = FormWithUserKwarg - template_name = 'form.html' + template_name = "form.html" def form_valid(self, form): return HttpResponse("username: %s" % form.user.username) @@ -211,7 +236,8 @@ class HeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin. """ - template_name = 'blank.html' + + template_name = "blank.html" headline = "Test headline" @@ -219,38 +245,43 @@ class LazyHeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin. """ - template_name = 'blank.html' + + template_name = "blank.html" headline = _("Test Headline") class ContextView(views.StaticContextMixin, TemplateView): - """ View for testing StaticContextMixin. """ - template_name = 'blank.html' - static_context = {'test': True} + """View for testing StaticContextMixin.""" + + template_name = "blank.html" + static_context = {"test": True} class DynamicHeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin's get_headline() method. """ - template_name = 'blank.html' + + template_name = "blank.html" def get_headline(self): - return self.kwargs['s'] + return self.kwargs["s"] class PermissionRequiredView(views.PermissionRequiredMixin, OkView): """ View for testing PermissionRequiredMixin. """ - permission_required = 'auth.add_user' + + permission_required = "auth.add_user" -class MultiplePermissionsRequiredView(views.MultiplePermissionsRequiredMixin, - OkView): +class MultiplePermissionsRequiredView( + views.MultiplePermissionsRequiredMixin, OkView +): permissions = { - 'all': ['tests.add_article', 'tests.change_article'], - 'any': ['auth.add_user', 'auth.change_user'], + "all": ["tests.add_article", "tests.change_article"], + "any": ["auth.add_user", "auth.change_user"], } @@ -268,68 +299,79 @@ class CsrfExemptView(views.CsrfExemptMixin, OkView): class AuthorDetailView(views.PrefetchRelatedMixin, ListView): model = User - prefetch_related = ['article_set'] - template_name = 'blank.html' + prefetch_related = ["article_set"] + template_name = "blank.html" class OrderableListView(views.OrderableListMixin, ListView): model = Article - orderable_columns = ('id', 'title', ) - orderable_columns_default = 'id' + orderable_columns = ( + "id", + "title", + ) + orderable_columns_default = "id" class CanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): model = Article - template_name = 'blank.html' + template_name = "blank.html" -class OverriddenCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, - DetailView): +class OverriddenCanonicalSlugDetailView( + views.CanonicalSlugDetailMixin, DetailView +): model = Article - template_name = 'blank.html' + template_name = "blank.html" def get_canonical_slug(self): - return codecs.encode(self.get_object().slug, 'rot_13') + return codecs.encode(self.get_object().slug, "rot_13") -class CanonicalSlugDetailCustomUrlKwargsView(views.CanonicalSlugDetailMixin, - DetailView): +class CanonicalSlugDetailCustomUrlKwargsView( + views.CanonicalSlugDetailMixin, DetailView +): model = Article - template_name = 'blank.html' - pk_url_kwarg = 'my_pk' - slug_url_kwarg = 'my_slug' + template_name = "blank.html" + pk_url_kwarg = "my_pk" + slug_url_kwarg = "my_slug" -class ModelCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, - DetailView): +class ModelCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): model = CanonicalArticle - template_name = 'blank.html' + template_name = "blank.html" class FormMessagesView(views.FormMessagesMixin, CreateView): form_class = ArticleForm - form_invalid_message = _('Invalid') - form_valid_message = _('Valid') + form_invalid_message = _("Invalid") + form_valid_message = _("Valid") model = Article - success_url = '/form_messages/' - template_name = 'form.html' + success_url = "/form_messages/" + template_name = "form.html" class GroupRequiredView(views.GroupRequiredMixin, OkView): - group_required = 'test_group' + group_required = "test_group" class UserPassesTestView(views.UserPassesTestMixin, OkView): def test_func(self, user): - return user.is_staff and not user.is_superuser \ - and user.email.endswith('@mydomain.com') + return ( + user.is_staff + and not user.is_superuser + and user.email.endswith("@mydomain.com") + ) -class UserPassesTestLoginRequiredView(views.LoginRequiredMixin, - views.UserPassesTestMixin, OkView): +class UserPassesTestLoginRequiredView( + views.LoginRequiredMixin, views.UserPassesTestMixin, OkView +): def test_func(self, user): - return user.is_staff and not user.is_superuser \ - and user.email.endswith('@mydomain.com') + return ( + user.is_staff + and not user.is_superuser + and user.email.endswith("@mydomain.com") + ) class UserPassesTestNotImplementedView(views.UserPassesTestMixin, OkView): @@ -338,7 +380,7 @@ class UserPassesTestNotImplementedView(views.UserPassesTestMixin, OkView): class AllVerbsView(views.AllVerbsMixin, View): def all(self, request, *args, **kwargs): - return HttpResponse('All verbs return this!') + return HttpResponse("All verbs return this!") class SSLRequiredView(views.SSLRequiredMixin, OkView): @@ -353,29 +395,25 @@ class RecentLoginRequiredView(views.RecentLoginRequiredMixin, OkView): class AttributeHeaderView(views.HeaderMixin, OkView): headers = { - 'X-DJANGO-BRACES-1': 1, - 'X-DJANGO-BRACES-2': 2, + "X-DJANGO-BRACES-1": 1, + "X-DJANGO-BRACES-2": 2, } class MethodHeaderView(views.HeaderMixin, OkView): - def get_headers(self, request): return { - 'X-DJANGO-BRACES-1': 1, - 'X-DJANGO-BRACES-2': 2, + "X-DJANGO-BRACES-1": 1, + "X-DJANGO-BRACES-2": 2, } class AuxiliaryHeaderView(View): - def dispatch(self, request, *args, **kwargs): - response = HttpResponse('OK with headers') - response['X-DJANGO-BRACES-EXISTING'] = 'value' + response = HttpResponse("OK with headers") + response["X-DJANGO-BRACES-EXISTING"] = "value" return response class ExistingHeaderView(views.HeaderMixin, AuxiliaryHeaderView): - headers = { - 'X-DJANGO-BRACES-EXISTING': 'other value' - } + headers = {"X-DJANGO-BRACES-EXISTING": "other value"}