• 
      

    Add a new module for dynamic page state injections.

    Review Request #14548 — Created Aug. 4, 2025 and submitted

    Information

    Djblets
    release-5.x

    Reviewers

    TemplateHook is a powerful way of letting extensions add content to
    templates with minimal effort, but it has two problems:

    1. It's limited to extensions, meaning it's not possible for other code
      to leverage the functionality.

    2. It has no say in the caching information, meaning the final ETag for
      a page may not consider dynamic changes to the content from the
      hooks.

    This change adds a new piece of architecture that makes the benefits of
    TemplateHook and hook points more generally-useful in an application.

    djblets.pagestate allows templates to set up page hook points that
    content can be injected into via a {% page_hook_point %} template tag.
    This is much like {% template_hook_point %} for extensions.

    These interface with a PageState instance, which is bound to an
    HttpRequest. These are created/accessed using
    PageState.for_request(request). A PageState tracks any injected
    content for the page, along with ETags for that content.

    Injections can be manual (through PageState.inject() calls) or dynamic
    (through registered injector objects). The dynamic ones work more like
    TemplateHook.

    Injections can provide content (HTML-safe or unsafe) and ETags, but
    either are optional. If an ETag is not provided, the content will be
    used in its place (in either case, this is seed data for a SHA256 hash
    for the resulting ETag). An injection with just an ETag and no content
    is also allowed, as that can be useful for ensuring state generated
    elsewhere for the page is considered in the resulting ETag.

    PageStateMiddleware manages the resulting ETag for the page. If the
    response does not include an ETag, we leave it alone (so we don't cause
    a page with other dynamic elements to be cached incorrectly). If it does
    include an ETag, it will be mixed with the PageState's ETag into a
    final ETag.

    TemplateHook is now built on all this. The {% template_hook_point %}
    tag simply wraps {% page_hook_point %}, and an injector is registered
    that bridges to the registered TemplateHooks. ExtensionsMiddleware
    also interfaces with PageStateMiddleware if not otherwise defined in
    settings.MIDDLEWARE. This means that no changes are needed for
    projects using extensions today. Everything just works.

    Unit tests pass.

    Tested that existing extensions using TemplateHook continue to work
    correctly.

    Built some new code using this support, and verified it worked as
    expected.

    Built the docs and checked the new guide for any obvious errors.

    Summary ID
    Add a new module for dynamic page state injections.
    `TemplateHook` is a powerful way of letting extensions add content to templates with minimal effort, but it has two problems: 1. It's limited to extensions, meaning it's not possible for other code to leverage the functionality. 2. It has no say in the caching information, meaning the final ETag for a page may not consider dynamic changes to the content from the hooks. This change adds a new piece of architecture that makes the benefits of `TemplateHook` and hook points more generally-useful in an application. `djblets.pagestate` allows templates to set up page hook points that content can be injected into via a `{% page_hook_point %}` template tag. This is much like `{% template_hook_point %}` for extensions. These interface with a `PageState` instance, which is bound to an `HttpRequest`. These are created/accessed using `PageState.for_request(request)`. A `PageState` tracks any injected content for the page, along with ETags for that content. Injections can be manual (through `PageState.inject()` calls) or dynamic (through registered injector objects). The dynamic ones work more like `TemplateHook`. Injections can provide content (HTML-safe or unsafe) and ETags, but either are optional. If an ETag is not provided, the content will be used in its place (in either case, this is seed data for a SHA256 hash for the resulting ETag). An injection with just an ETag and no content is also allowed, as that can be useful for ensuring state generated elsewhere for the page is considered in the resulting ETag. `PageStateMiddleware` manages the resulting ETag for the page. If the response does not include an ETag, we leave it alone (so we don't cause a page with other dynamic elements to be cached incorrectly). If it does include an ETag, it will be mixed with the `PageState`'s ETag into a final ETag. `TemplateHook` is now built on all this. The `{% template_hook_point %}` tag simply wraps `{% page_hook_point %}`, and an injector is registered that bridges to the registered `TemplateHook`s. `ExtensionsMiddleware` also interfaces with `PageStateMiddleware` if not otherwise defined in `settings.MIDDLEWARE`. This means that no changes are needed for projects using extensions today. Everything just works.
    2812600f68e0fc7b8e817a34c2af55c237bd52cf
    Description From Last Updated

    'django.utils.translation.gettext as _' imported but unused Column: 1 Error code: F401

    reviewbotreviewbot

    Leftover debug output

    daviddavid

    Do we want to do any validation on the values passed in to this method?

    daviddavid

    Is there really a case where there will be no etag and no content?

    daviddavid

    Arg name should be point_name here.

    daviddavid

    From the code it looks like the key here should be 'content' instead of 'data'?

    daviddavid

    Same here.

    daviddavid

    Let's capitalize ETag here.

    maubinmaubin

    It looks like this should be not in. Right now it's checking if we have the middleware in the list …

    daviddavid

    typo: missing period between templatetags and djblets_pagestate

    daviddavid

    This should be removed.

    maubinmaubin

    I think you can achieve the same thing by combining old_etag and injected_etag into a value that you then pass …

    maubinmaubin

    These docs are missing a Raises section.

    maubinmaubin

    Similar to my comment about using encode_etag(), maybe its worthwhile to have a function in djblets.util.http that returns an etag …

    maubinmaubin

    Should this be typed as HttpRequest | None instead of Any?

    daviddavid

    typo: get_tag -> get_etag

    daviddavid

    'hashlib' imported but unused Column: 1 Error code: F401

    reviewbotreviewbot

    'typing.Any' imported but unused Column: 5 Error code: F401

    reviewbotreviewbot

    This is constructing a new PageStateMiddleware for every request, but that class doesn't have any internal state. Can we have …

    daviddavid
    chipx86
    Review request changed
    Change Summary:
    • Removed all the logic around finalizing hook points and preventing further injections or lookups. Multiple templates may be used in a request/response cycle.
    • Added PageState.clear_injections() to clear some or all points in-between renders.
    Commits:
    Summary ID
    Add a new module for dynamic page state injections.
    `TemplateHook` is a powerful way of letting extensions add content to templates with minimal effort, but it has two problems: 1. It's limited to extensions, meaning it's not possible for other code to leverage the functionality. 2. It has no say in the caching information, meaning the final ETag for a page may not consider dynamic changes to the content from the hooks. This change adds a new piece of architecture that makes the benefits of `TemplateHook` and hook points more generally-useful in an application. `djblets.pagestate` allows templates to set up page hook points that content can be injected into via a `{% page_hook_point %}` template tag. This is much like `{% template_hook_point %}` for extensions. These interface with a `PageState` instance, which is bound to an `HttpRequest`. These are created/accessed using `PageState.for_request(request)`. A `PageState` tracks any injected content for the page, along with ETags for that content. Injections can be manual (through `PageState.inject()` calls) or dynamic (through registered injector objects). The dynamic ones work more like `TemplateHook`. Injections can provide content (HTML-safe or unsafe) and ETags, but either are optional. If an ETag is not provided, the content will be used in its place (in either case, this is seed data for a SHA256 hash for the resulting ETag). An injection with just an ETag and no content is also allowed, as that can be useful for ensuring state generated elsewhere for the page is considered in the resulting ETag. `PageStateMiddleware` manages the resulting ETag for the page. If the response does not include an ETag, we leave it alone (so we don't cause a page with other dynamic elements to be cached incorrectly). If it does include an ETag, it will be mixed with the `PageState`'s ETag into a final ETag. `TemplateHook` is now built on all this. The `{% template_hook_point %}` tag simply wraps `{% page_hook_point %}`, and an injector is registered that bridges to the registered `TemplateHook`s. `ExtensionsMiddleware` also interfaces with `PageStateMiddleware` if not otherwise defined in `settings.MIDDLEWARE`. This means that no changes are needed for projects using extensions today. Everything just works.
    6b3a6cc7bb2731cdd974c78c20e8c94d73e032f2
    Add a new module for dynamic page state injections.
    `TemplateHook` is a powerful way of letting extensions add content to templates with minimal effort, but it has two problems: 1. It's limited to extensions, meaning it's not possible for other code to leverage the functionality. 2. It has no say in the caching information, meaning the final ETag for a page may not consider dynamic changes to the content from the hooks. This change adds a new piece of architecture that makes the benefits of `TemplateHook` and hook points more generally-useful in an application. `djblets.pagestate` allows templates to set up page hook points that content can be injected into via a `{% page_hook_point %}` template tag. This is much like `{% template_hook_point %}` for extensions. These interface with a `PageState` instance, which is bound to an `HttpRequest`. These are created/accessed using `PageState.for_request(request)`. A `PageState` tracks any injected content for the page, along with ETags for that content. Injections can be manual (through `PageState.inject()` calls) or dynamic (through registered injector objects). The dynamic ones work more like `TemplateHook`. Injections can provide content (HTML-safe or unsafe) and ETags, but either are optional. If an ETag is not provided, the content will be used in its place (in either case, this is seed data for a SHA256 hash for the resulting ETag). An injection with just an ETag and no content is also allowed, as that can be useful for ensuring state generated elsewhere for the page is considered in the resulting ETag. `PageStateMiddleware` manages the resulting ETag for the page. If the response does not include an ETag, we leave it alone (so we don't cause a page with other dynamic elements to be cached incorrectly). If it does include an ETag, it will be mixed with the `PageState`'s ETag into a final ETag. `TemplateHook` is now built on all this. The `{% template_hook_point %}` tag simply wraps `{% page_hook_point %}`, and an injector is registered that bridges to the registered `TemplateHook`s. `ExtensionsMiddleware` also interfaces with `PageStateMiddleware` if not otherwise defined in `settings.MIDDLEWARE`. This means that no changes are needed for projects using extensions today. Everything just works.
    51e7dcb9d3b5f120e5b62e61bf5da56662c11614

    Checks run (1 failed, 1 succeeded)

    flake8 failed.
    JSHint passed.

    flake8

    chipx86
    david
    1. 
        
    2. djblets/pagestate/state.py (Diff revision 3)
       
       
      Show all issues

      Leftover debug output

    3. djblets/pagestate/state.py (Diff revision 3)
       
       
      Show all issues

      Do we want to do any validation on the values passed in to this method?

    4. djblets/pagestate/state.py (Diff revision 3)
       
       
      Show all issues

      Is there really a case where there will be no etag and no content?

      1. Yeah, an injector returning content that, due to an {% if %} or something not evaluating, ends up as an empty string, and no etag specified.

    5. Show all issues

      Arg name should be point_name here.

    6. docs/djblets/guides/pagestate/index.rst (Diff revision 3)
       
       
      Show all issues

      From the code it looks like the key here should be 'content' instead of 'data'?

      1. Ah yeah, renamed it and didn't update the docs.

    7. docs/djblets/guides/pagestate/index.rst (Diff revision 3)
       
       
      Show all issues

      Same here.

    8. 
        
    chipx86
    maubin
    1. 
        
    2. djblets/extensions/hooks.py (Diff revision 4)
       
       
      Show all issues

      Let's capitalize ETag here.

    3. djblets/pagestate/middleware.py (Diff revision 4)
       
       
       
      Show all issues

      This should be removed.

    4. djblets/pagestate/middleware.py (Diff revision 4)
       
       
       
       
       
       
       
      Show all issues

      I think you can achieve the same thing by combining old_etag and injected_etag into a value that you then pass to djblets.util.http.encode_etag(). That way if we ever improve the way we build etags (for example when we switched from sha1 to sha256), we don't have to remember to update here too.

    5. djblets/pagestate/state.py (Diff revision 4)
       
       
      Show all issues

      These docs are missing a Raises section.

    6. djblets/pagestate/state.py (Diff revision 4)
       
       
       
       
       
       
       
      Show all issues

      Similar to my comment about using encode_etag(), maybe its worthwhile to have a function in djblets.util.http that returns an etag SHA and a function for updating a SHA. For now these would return hashlib.sha256() and existing_etag.update(new_etag.encode('utf-8')) respectively. Makes it easier in the future if we ever want to change our standards for etags.

      1. I'll want to think about that design a bit. The format of an ETag isn't especially important, as it just needs to be distinct from a previous page load, and SHA256 will be a good guarantee on that.

      2. True, sounds good.

    7. 
        
    david
    1. 
        
    2. djblets/extensions/middleware.py (Diff revision 4)
       
       
      Show all issues

      It looks like this should be not in. Right now it's checking if we have the middleware in the list and adding it again, instead of adding it only if it's missing.

    3. Show all issues

      typo: missing period between templatetags and djblets_pagestate

    4. Show all issues

      Should this be typed as HttpRequest | None instead of Any?

    5. Show all issues

      typo: get_tag -> get_etag

    6. 
        
    chipx86
    Review request changed
    Change Summary:
    • Fixed issues with docs.
    • Fixed an inversed conditional for middleware.
    • Switched to encode_etag() when merging ETags.
    • Fixed a typing issue with HttpRequest | None.
    Commits:
    Summary ID
    Add a new module for dynamic page state injections.
    `TemplateHook` is a powerful way of letting extensions add content to templates with minimal effort, but it has two problems: 1. It's limited to extensions, meaning it's not possible for other code to leverage the functionality. 2. It has no say in the caching information, meaning the final ETag for a page may not consider dynamic changes to the content from the hooks. This change adds a new piece of architecture that makes the benefits of `TemplateHook` and hook points more generally-useful in an application. `djblets.pagestate` allows templates to set up page hook points that content can be injected into via a `{% page_hook_point %}` template tag. This is much like `{% template_hook_point %}` for extensions. These interface with a `PageState` instance, which is bound to an `HttpRequest`. These are created/accessed using `PageState.for_request(request)`. A `PageState` tracks any injected content for the page, along with ETags for that content. Injections can be manual (through `PageState.inject()` calls) or dynamic (through registered injector objects). The dynamic ones work more like `TemplateHook`. Injections can provide content (HTML-safe or unsafe) and ETags, but either are optional. If an ETag is not provided, the content will be used in its place (in either case, this is seed data for a SHA256 hash for the resulting ETag). An injection with just an ETag and no content is also allowed, as that can be useful for ensuring state generated elsewhere for the page is considered in the resulting ETag. `PageStateMiddleware` manages the resulting ETag for the page. If the response does not include an ETag, we leave it alone (so we don't cause a page with other dynamic elements to be cached incorrectly). If it does include an ETag, it will be mixed with the `PageState`'s ETag into a final ETag. `TemplateHook` is now built on all this. The `{% template_hook_point %}` tag simply wraps `{% page_hook_point %}`, and an injector is registered that bridges to the registered `TemplateHook`s. `ExtensionsMiddleware` also interfaces with `PageStateMiddleware` if not otherwise defined in `settings.MIDDLEWARE`. This means that no changes are needed for projects using extensions today. Everything just works.
    db38372e7702f59b927086e1a90a3442dbc326f6
    Add a new module for dynamic page state injections.
    `TemplateHook` is a powerful way of letting extensions add content to templates with minimal effort, but it has two problems: 1. It's limited to extensions, meaning it's not possible for other code to leverage the functionality. 2. It has no say in the caching information, meaning the final ETag for a page may not consider dynamic changes to the content from the hooks. This change adds a new piece of architecture that makes the benefits of `TemplateHook` and hook points more generally-useful in an application. `djblets.pagestate` allows templates to set up page hook points that content can be injected into via a `{% page_hook_point %}` template tag. This is much like `{% template_hook_point %}` for extensions. These interface with a `PageState` instance, which is bound to an `HttpRequest`. These are created/accessed using `PageState.for_request(request)`. A `PageState` tracks any injected content for the page, along with ETags for that content. Injections can be manual (through `PageState.inject()` calls) or dynamic (through registered injector objects). The dynamic ones work more like `TemplateHook`. Injections can provide content (HTML-safe or unsafe) and ETags, but either are optional. If an ETag is not provided, the content will be used in its place (in either case, this is seed data for a SHA256 hash for the resulting ETag). An injection with just an ETag and no content is also allowed, as that can be useful for ensuring state generated elsewhere for the page is considered in the resulting ETag. `PageStateMiddleware` manages the resulting ETag for the page. If the response does not include an ETag, we leave it alone (so we don't cause a page with other dynamic elements to be cached incorrectly). If it does include an ETag, it will be mixed with the `PageState`'s ETag into a final ETag. `TemplateHook` is now built on all this. The `{% template_hook_point %}` tag simply wraps `{% page_hook_point %}`, and an injector is registered that bridges to the registered `TemplateHook`s. `ExtensionsMiddleware` also interfaces with `PageStateMiddleware` if not otherwise defined in `settings.MIDDLEWARE`. This means that no changes are needed for projects using extensions today. Everything just works.
    fd57b0cf60142adf1ee64bbb1db55583ab4eb1f8

    Checks run (1 failed, 1 succeeded)

    flake8 failed.
    JSHint passed.

    flake8

    chipx86
    david
    1. 
        
    2. djblets/extensions/middleware.py (Diff revision 6)
       
       
      Show all issues

      This is constructing a new PageStateMiddleware for every request, but that class doesn't have any internal state. Can we have a cached instance that we reuse?

      1. It's a function, and it takes as its first argument a function that returns the request to process. There's no class there to instantiate.

    3. Does this format correctly when you build the docs?

      1. Yep. Built the docs and checked.

    4. 
        
    maubin
    1. Ship It!
    2. 
        
    david
    1. Ship It!
    2. 
        
    chipx86
    Review request changed
    Status:
    Completed
    Change Summary:
    Pushed to release-5.x (dbd1a5b)