Django Views — The Right Way

repository·master·Indexed 41 days ago

https://github.com/spookylukey/django-views-the-right-way

A reference implementation and opinionated guide on implementing Django views. It advocates for Function-Based Views (FBVs) as a starting point, prefers functional composition over Class-Based View (CBV) mixins, and provides patterns for delegation, common context data handling, and parameterized generic views to avoid premature abstraction.

Tokens
17.1K
Snippets
38
Records
64
Agent score
52%

What's inside Django Views — The Right Way

  1. Scale repetitive views using composition

    master

    For large-scale projects with highly repetitive views (like CRUD operations), move beyond individual view functions and provide abstractions for sets of views.

    To succeed with high-level abstractions (similar to how Django Admin or Django Rest Framework operate), follow these principles:

    • Constrain the scope: Focus on a narrow set of behaviors (e.g., CRUD) to keep abstractions manageable.
    • Prefer Composition over Inheritance: Instead of using complex mixin hierarchies, compose behavior by referencing separate objects. For example, a view should reference separate classes for serializers, permissions, or filters rather than inheriting their logic.
  2. Philosophy of implementing logic in views

    master

    When implementing custom logic in a view, prioritize direct implementation over forcing code into a specific pattern or framework abstraction. The project advocates for a 'just do it' approach:

    • Take ownership: You are in charge of your view function. Make it do what you need without trying to fit into a pre-existing pattern.
    • Write code instead of searching for patterns: If a built-in way to do something doesn't exist, write the necessary code yourself.
    • Iterate on perfection: Don't worry about making code perfectly concise or DRY (Don't Repeat Yourself) immediately. Focus on functionality first, and refactor into reusable patterns later if you find yourself repeating logic.
  3. Philosophy of Thin Views

    master

    To maintain a clean architecture, views should be kept 'skinny' by delegating business logic to the model layer.

    What a view should do:

    • Deal with incoming HTTP requests.
    • Create outgoing HTTP requests.
    • Glue together logic from other layers.

    What a view should NOT do:

    • Contain business logic that is common to other application types (e.g., CLI, desktop GUIs, or scheduled tasks).
    • Directly manipulate database schema details (e.g., setting specific boolean flags or complex filter conditions).

    Note on UI concerns: Logic related to the web interface, such as messages.info(), should remain in the view layer, as it is specific to the HTTP context.

  4. Understand why Mixins may not compose

    master

    While Django Mixins (like LoginRequiredMixin) can be used in Class-Based Views (CBVs) by adding them to the inheritance list, they do not always compose reliably.

    The Risk: If multiple mixins rely on the same method (e.g., overriding dispatch()) and do not use super() correctly, or if they use specific implementations like Django's UserPassesTestMixin (which cannot be stacked), they may silently fail. In such cases, only one of the checks might actually run, potentially creating security vulnerabilities.

    Best Practice:

    • If you need multiple preconditions in a CBV, consider building a single mixin that combines the logic, or use decorators on the view methods if possible.
    • Decorators are generally safer for composition because they wrap existing functionality via an interface, whereas mixins inject logic into the middle of a class hierarchy and depend heavily on implementation details.
  5. Choose the right mechanism for common context data

    master

    Depending on the scope and cost of the data, choose one of the following Django patterns:

    1. Site-wide data: Use Django context processors if the data is needed on almost every page.
    2. Expensive site-wide data: Use lazy evaluation within your context processor to avoid unnecessary computation.
    3. Template-level components: Use custom inclusion template tags for data needed by specific UI components (like headers or footers) that appear in multiple templates. This allows the component to load its own data without modifying view functions.
    4. Group-specific data: Use helper functions (as described in the composition guide) for data shared by a specific subset of views (e.g., all checkout pages).
  6. Enforce security policies at import time with checking decorators

    master

    To avoid the 'bare view' problem of django-decorator-include, you can use a two-step pattern:

    1. The Policy Decorator: A decorator that performs the actual logic (e.g., checking if a user is premium) and sets a specific attribute on the view function to signal that the policy has been applied.
    2. The Checking Decorator: A decorator used within django-decorator-include that does nothing at runtime but checks at import time if the required attribute is present on the view. If the attribute is missing, it raises an AssertionError.

    This ensures that if you forget to decorate a view, the application fails to start, preventing security holes from reaching production.

    Implementation Example:

    _SECURITY_POLICY_APPLIED = "_SECURITY_POLICY_APPLIED"
    
    # 1. The Checker
    def check_security_policy_applied(view_func):
        if not getattr(view_func, _SECURITY_POLICY_APPLIED, False):
            raise AssertionError(f"{view_func.__module__}.{view_func.__name__} needs to have a security policy applied")
        return view_func
    
    # 2. The Policy
    def premium_required(view_func):
        @functools.wraps(view_func)
        def wrapper(request, *args, **kwargs):
            if not (request.user.is_authenticated and request.user.is_premium):
                # ... handle unauthorized access ...
                return HttpResponseRedirect('/')
            return view_func(request, *args, **kwargs)
    
        setattr(wrapper, _SECURITY_POLICY_APPLIED, True)
        return wrapper
    
    # 3. Usage in urls.py
    urlpatterns = [
        include('myapp.urls', decorators=[check_security_policy_applied]),
    ]
  7. Avoiding MRO complexity with Function-Based Views

    master

    Complex Class-Based View (CBV) hierarchies involving multiple mixins can lead to Method Resolution Order (MRO) issues, sometimes requiring custom metaclasses (like AjaxMroFixer) to reorder how methods like post are inherited.

    To avoid the need for 'metaclass fixes' and to make code easier to maintain, consider refactoring complex CBVs into Function-Based Views (FBVs). An FBV can explicitly handle form flow control, request methods, and template responses without delegating to a deep chain of base classes.

    @booking_account_required
    @ajax_form_validate(AccountDetailsForm)
    def account_details(request):
        form_class = AccountDetailsForm
    
        if request.method == "POST":
            form = form_class(request.POST, instance=request.booking_account)
            if form.is_valid():
                form.save()
                messages.info(request, 'Account details updated, thank you.')
                return next_step(request.booking_account)
        else:
            form = form_class(instance=request.booking_account)
    
        return TemplateResponse(request, 'cciw/bookings/account_details.html', {
            'title': 'Booking - account details',
            'stage': BookingStage.ACCOUNT,
            'form': form,
        })
  8. Compare Function-Based Views (FBV) and Class-Based Views (CBV)

    master

    The project advocates for Function-Based Views (FBVs) over Class-Based Views (CBVs) like TemplateView or ListView to maintain visibility of the core Django request-response cycle.

    FeatureFunction-Based View (FBV)Class-Based View (CBV)
    Core NatureA clear Python function/callable.A class that requires .as_view() to become a callable.
    VisibilityRequest and Response are explicit in the signature.Request and Response are obscured by inheritance.
    ComplexitySimple and direct.High inheritance trees and complex method flows.
    Context DataPassed directly via a dictionary.Often requires more boilerplate to implement.

    FBV Example:

    def my_view(request):
        return TemplateResponse(request, 'template.html', {'data': 'value'})

    CBV Example (Obscured):

    from django.views.generic import TemplateView
    
    class ExampleView(TemplateView):
        template_name = "example.html"
  9. Use `queryset` instead of `model` for better flexibility

    master

    When defining views, prefer using the queryset attribute over the model attribute.

    Using queryset = Product.objects.all() provides a more flexible starting point than model = Product. If requirements change (e.g., you need to filter for only 'visible' products), you can easily update the queryset to Product.objects.visible() without changing the fundamental structure of the view. This applies to both CBVs and Function-Based Views (FBVs) using get_object_or_404.

    # In CBVs
    # Avoid:
    class ProductDetailView(DetailView):
        model = Product
    
    # Prefer:
    class ProductDetailView(DetailView):
        queryset = Product.objects.all()
    
    # In FBVs
    # Avoid:
    get_object_or_404(Product, pk=pk)
    
    # Prefer:
    get_object_or_404(Product.objects.all(), pk=pk)
  10. Avoid premature abstraction and the 'wrong abstraction' trap

    master

    When deciding whether to reuse existing code or implement your own, prioritize clarity and the correct mental model over avoiding code duplication.

    Key principles:

    • Knowledge vs. Lines of Code: The goal of DRY (Don't Repeat Yourself) is to avoid duplicating knowledge, not necessarily lines of code. Duplicating code is often better than creating a complex, incorrect abstraction.
    • The Wrong Abstraction: Attempting to force different behaviors into a single shared abstraction can lead to extreme complexity. If two pieces of code look similar but diverge in fundamental ways, it is often better to copy-paste and diverge rather than maintain a fragile common layer.
    • Wait for Patterns: Do not abstract commonality until you have at least two (preferably three) concrete examples. Abstracting too early is premature and often leads to the wrong design.
  11. Create a View Factory for mass-produced views

    master

    To avoid keyword argument clashes in urls.py when using extra options, use a "view factory". A view factory is a function that accepts configuration parameters and returns a specific view function. This pattern is useful for creating many similar views that only differ by configuration.

    Benefits:

    • Prevents clashes between urls.py kwargs and the view's own arguments.
    • Allows the inner view function to have a specific, type-checked signature.
    from django.urls import reverse
    from django.http import HttpResponseRedirect
    
    def make_redirector(pattern_name=None, permanent=False, query_string=False):
        def redirector(request, *args, **kwargs):
            url = reverse(pattern_name, args=args, kwargs=kwargs)
            # Implementation of permanent/query_string logic goes here
            return HttpResponseRedirect(url)
        return redirector
    
    # Usage in urls.py
    urlpatterns = [
        path('old-path/<int:pk>/', make_redirector(
            pattern_name='my_view',
            permanent=True,
            query_string=True,
        )),
    ]