Skip to content

Commit

Permalink
Merge branch 'main' into template-response
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethlove authored Nov 18, 2021
2 parents 82c65d6 + df847c9 commit 2f1a4ec
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 315 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ jobs:
flags: unittests
name: codecov-umbrella
verbose: true
- name: Python Interrogate Check
uses: JackMcKew/python-interrogate-check@main
with:
path: 'braces'
14 changes: 14 additions & 0 deletions braces/views/_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def get_redirect_field_name(self):
return self.redirect_field_name

def handle_no_permission(self, request):
"""What should happen if the user doesn't have permission?"""
if self.raise_exception:
if (
self.redirect_unauthenticated_users
Expand Down Expand Up @@ -102,6 +103,7 @@ class LoginRequiredMixin(AccessMixin):
"""

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

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

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

def get_permission_required(self, request=None):
"""Get which permission is required"""
self._check_permissions_attr()
return self.permissions

def check_permissions(self, request):
"""Get the permissions, both all and any."""
permissions = self.get_permission_required(request)
perms_all = permissions.get("all")
perms_any = permissions.get("any")
Expand Down Expand Up @@ -327,6 +332,7 @@ class GroupRequiredMixin(AccessMixin):
group_required = None

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

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

def test_func(self, user):
"""The function to test the user with"""
raise NotImplementedError(
f"{self._class_name} is missing implementation of the "
"`test_func` method. A function to test the user is required."
)

def get_test_func(self):
"""Get the test function"""
return getattr(self, "test_func")

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

if not user_test_result:
Expand All @@ -397,6 +407,7 @@ class SuperuserRequiredMixin(AccessMixin):
"""

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

Expand All @@ -409,6 +420,7 @@ class StaffuserRequiredMixin(AccessMixin):
"""

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

Expand All @@ -423,6 +435,7 @@ class SSLRequiredMixin:
raise_exception = False

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

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

if resp.status_code == 200:
Expand Down
13 changes: 12 additions & 1 deletion braces/views/_ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class JSONResponseMixin:
json_encoder_class = None

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

def get_json_dumps_kwargs(self):
"""Get kwargs for custom JSON compilation"""
dumps_kwargs = getattr(self, "json_dumps_kwargs", None) or {}
dumps_kwargs.setdefault("ensure_ascii", False)
return dumps_kwargs

def get_json_encoder_class(self):
"""Get the encoder class to use"""
if self.json_encoder_class is None:
self.json_encoder_class = DjangoJSONEncoder
return self.json_encoder_class
Expand Down Expand Up @@ -74,6 +77,7 @@ class AjaxResponseMixin:
"""

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

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

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

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

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


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

def render_bad_request_response(self, error_dict=None):
"""Generate errors for bad content"""
if error_dict is None:
error_dict = self.error_response_dict
json_context = json.dumps(
Expand All @@ -141,12 +150,14 @@ def render_bad_request_response(self, error_dict=None):
)

def get_request_json(self):
"""Get the JSON included in the body"""
try:
return json.loads(self.request.body.decode("utf-8"))
except (json.JSONDecodeError, ValueError):
return None

def dispatch(self, request, *args, **kwargs):
"""Trigger the appropriate method"""
self.request = request
self.args = args
self.kwargs = kwargs
Expand All @@ -164,4 +175,4 @@ def dispatch(self, request, *args, **kwargs):


class JSONRequestResponseMixin(JsonRequestResponseMixin):
pass
"""Convenience alias"""
6 changes: 6 additions & 0 deletions braces/views/_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ class SetHeadlineMixin:
headline = None # Default the headline to none

def get_context_data(self, **kwargs):
"""Add the headline to the context"""
kwargs = super().get_context_data(**kwargs)
kwargs.update({"headline": self.get_headline()})
return kwargs

def get_headline(self):
"""Fetch the headline from the instance"""
if self.headline is None:
class_name = self.__class__.__name__
raise ImproperlyConfigured(
Expand All @@ -37,6 +39,7 @@ class StaticContextMixin:
static_context = None

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

try:
Expand All @@ -50,6 +53,7 @@ def get_context_data(self, **kwargs):
return kwargs

def get_static_context(self):
"""Fetch the static content from the view"""
if self.static_context is None:
class_name = self.__class__.__name__
raise ImproperlyConfigured(
Expand Down Expand Up @@ -120,6 +124,7 @@ class AllVerbsMixin:
all_handler = "all"

def dispatch(self, request, *args, **kwargs):
"""Call the all handler"""
if not self.all_handler:
raise ImproperlyConfigured(
f"{self.__class__.__name__} requires the all_handler attribute to be set."
Expand Down Expand Up @@ -196,6 +201,7 @@ class CacheControlMixin:

@classmethod
def get_cachecontrol_options(cls):
"""Compile a dictionary of selected cache options"""
opts = (
'public', 'private', 'no_cache', 'no_transform',
'must_revalidate', 'proxy_revalidate', 'max_age',
Expand Down
5 changes: 5 additions & 0 deletions braces/views/_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SelectRelatedMixin:
select_related = None # Default related fields to none

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

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

def get_orderable_columns(self):
"""Check that the orderable columns are set and return them"""
if not self.orderable_columns:
raise ImproperlyConfigured(
f"{self.__class__.__name__} needs the ordering columns defined."
)
return self.orderable_columns

def get_orderable_columns_default(self):
"""Which column(s) should be sorted by, by default?"""
if not self.orderable_columns_default:
raise ImproperlyConfigured(
f"{self.__class__.__name__} needs the default ordering column defined."
)
return self.orderable_columns_default

def get_ordering_default(self):
"""Which direction should things be sorted?"""
if not self.ordering_default:
return "asc"
else:
Expand Down
19 changes: 19 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,22 @@ line-length = 79

[tool.pytest.ini_options]
addopts = "--cov --nomigrations"

[tool.interrogate]
ignore-init-method = true
ignore-init-module = false
ignore-magic = false
ignore-semiprivate = false
ignore-private = false
ignore-property-decorators = false
ignore-module = true
ignore-nested-functions = false
ignore-nested-classes = true
fail-under = 75
exclude = ["setup.py", "conftest.py", "docs", "build"]
ignore-regex = ["^get$", "^mock_.*"]
# possible values: 0 (minimal output), 1 (-v), 2 (-vv)
verbose = 1
quiet = false
color = true
omit-covered-files = true
5 changes: 3 additions & 2 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.core.serializers.json import DjangoJSONEncoder


class TestViewHelper(object):
class TestViewHelper:
"""
Helper class for unit-testing class based views.
"""
Expand All @@ -12,7 +12,7 @@ class TestViewHelper(object):
request_factory_class = test.RequestFactory

def setUp(self):
super(TestViewHelper, self).setUp()
super().setUp()
self.factory = self.request_factory_class()

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

def default(self, obj):
"""Control default methods of encoding data"""
if isinstance(obj, set):
return list(obj)
return super(DjangoJSONEncoder, self).default(obj)
2 changes: 1 addition & 1 deletion tests/test_access_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def build_authorized_user(self):
return UserFactory(is_superuser=True, is_staff=True)

def build_unauthorized_user(self):
"""Make a non-superusers"""
"""Make a non-superuser"""
return UserFactory()


Expand Down
Loading

0 comments on commit 2f1a4ec

Please # to comment.