# pylint: disable=W0613
"""
Custom Django context processors for the Smarter dashboard application.
These processors inject template context variables into every view that renders
a template inheriting from ``base.html``. Each processor is registered in
``TEMPLATES['OPTIONS']['context_processors']`` in Django settings.
Context processors
------------------
:func:`sidebar`
Resolves and caches the href targets for every sidebar navigation link.
Returns a ``sidebar`` dict keyed by destination name.
:func:`base`
Assembles the primary ``dashboard`` context dict: user identity, role
flags (``is_superuser``, ``is_staff``), feature toggles, resource counts,
and platform version metadata. The inner result is cached per user.
:func:`branding`
Provides a ``branding`` dict containing corporate identity, support
contact details, social-media URLs, CDN paths, and a dynamic copyright
notice. Values are sourced from ``smarter_settings``.
:func:`footer`
Provides a ``footer`` dict with links to legal, plans, support, and
contact pages.
:func:`cache_buster`
Injects a ``cache_buster`` string (``v=<timestamp>``) for appending to
static asset URLs in local development.
Cache utilities
---------------
:func:`cache_invalidations`
Invalidates all per-user caches (account, profile, plugins, llm_clients, and
page-level caches for the dashboard and workbench) after user data changes.
Called by signal handlers in the account app.
.. seealso::
- :class:`smarter.lib.manifest.broker.AbstractBroker`
- ``smarter.apps.account.signals.cache_invalidate``
Usage
-----
Add the processors to your Django settings::
TEMPLATES = [
{
"OPTIONS": {
"context_processors": [
...
"smarter.apps.dashboard.context_processors.sidebar",
"smarter.apps.dashboard.context_processors.base",
"smarter.apps.dashboard.context_processors.branding",
"smarter.apps.dashboard.context_processors.footer",
"smarter.apps.dashboard.context_processors.cache_buster",
],
},
}
]
"""
import sys
import time
from datetime import datetime
from typing import TYPE_CHECKING, Any, Optional
from urllib.parse import urljoin
from smarter.__version__ import __version__
from smarter.apps.account.models import (
Account,
UserProfile,
get_resolved_user,
)
from smarter.apps.connection.urls import ConnectionReverseNames
from smarter.apps.dashboard.views.apply_manifest.urls import ApplyManifestReverseNames
from smarter.apps.dashboard.views.passthrough.urls import PassthroughReverseNames
from smarter.apps.dashboard.views.terminal_emulator.names import (
DashboardLogsReverseNames,
)
from smarter.apps.dashboard.views.views.urls import DashboardReverseNames
from smarter.apps.docs.urls import DocsReverseNames
from smarter.apps.llm_client.models import LLMClient
from smarter.apps.plugin.models import (
PluginMeta,
)
from smarter.apps.plugin.urls import PluginReverseNames
from smarter.apps.prompt.urls import PromptReverseNames
from smarter.apps.provider.urls import ProviderReverseNames
from smarter.apps.secret.urls import SecretReverseNames
from smarter.apps.vectorstore.urls import VectorstoreReverseNames
from smarter.common.conf import smarter_settings
from smarter.common.const import SMARTER_PRODUCT_DESCRIPTION, SMARTER_PRODUCT_NAME
from smarter.common.utils import snake_case
from smarter.lib import logging
from smarter.lib.cache import cache_results
from smarter.lib.django.shortcuts import reverse
from smarter.lib.drf.urls import AuthTokenReverseNames
if TYPE_CHECKING:
from django.http import HttpRequest
[docs]
def is_sphinx_build():
"""Determine if the current execution context is a Sphinx documentation build."""
return "sphinx" in sys.modules
logger = logging.getLogger(__name__)
logger_prefix = logging.formatted_text(__name__)
logger_prefix_cache_invalidations = logging.formatted_text_blue(f"{__name__}.cache_invalidations()")
[docs]
def static_version(request):
return {
"STATIC_VERSION": smarter_settings.version,
}
cache_results()
[docs]
def base(request: "HttpRequest") -> dict[str, Any]:
"""
Provides the base context for all templates inheriting from ``base.html``.
in the Smarter dashboard.
This context processor injects a comprehensive set of user-specific and
application-wide variables into the template context. These variables
include user identity, role flags, product metadata, and resource counts
(such as llm_clients, plugins, API keys, custom domains, connections, and
secrets). The context is used to render the dashboard layout and
personalize the user experience.
The resource counts are cached for performance, and the context is dynamically
constructed based on the authenticated user's account and profile.
:param request: The HTTP request object.
:type request: "HttpRequest"
:return: A dictionary containing the dashboard context variables.
:rtype: dict
"""
user = None
user_profile = None
resolved_user = None
if hasattr(request, "user"):
user = request.user
resolved_user = get_resolved_user(user)
user_profile: Optional[UserProfile] = None
if resolved_user and getattr(resolved_user, "is_authenticated", False):
user_profile = UserProfile.get_cached_object(user=resolved_user) # type: ignore
else:
user = None
@cache_results()
@snake_case()
def get_cached_context(username: Optional[str]) -> dict[str, Any]:
"""
Constructs and returns the cached dashboard context for the specified user.
This helper function assembles a dictionary of dashboard context variables,
including user identity, role flags, product metadata, and resource counts.
It is decorated with a cache to optimize performance and minimize redundant
database queries.
The context is tailored to the authenticated user and is used by the main
``base`` context processor to populate the dashboard template.
:param username: The username for whom the dashboard context is being constructed.
:type username: Optional[str]
:return: A dictionary containing the dashboard context variables for the user.
:rtype: dict
"""
current_year = datetime.now().year
user_email = "anonymous@mail.edu"
username = "anonymous"
is_superuser = False
is_staff = False
if user_profile and user_profile.user.is_authenticated:
try:
user_email = user_profile.user.email
username = user_profile.user.username
is_superuser = user_profile.user.is_superuser
is_staff = user_profile.user.is_staff
except AttributeError:
# technically, this is supposed to be impossible due to the is_authenticated check
pass
cached_context = {
"dashboard": {
"debug_mode": smarter_settings.debug_mode,
"user_email": user_email,
"username": username,
"is_superuser": is_superuser,
"is_staff": is_staff,
"is_vectorstore_enabled": smarter_settings.enable_vectorstore,
"is_file_drop_zone_enabled": smarter_settings.enable_dashboard_apply,
"is_enabled_server_logs": smarter_settings.enable_dashboard_server_logs,
"profile_image_url": (
user_profile.profile_image_url if user_profile and user_profile.profile_image_url else "#"
),
"first_name": (user_profile.user.first_name if user_profile and user_profile.user.first_name else ""),
"last_name": (user_profile.user.last_name if user_profile and user_profile.user.last_name else ""),
"product_name": SMARTER_PRODUCT_NAME,
"company_name": smarter_settings.root_domain,
"smarter_version": "v" + __version__,
"python_version": smarter_settings.python_version,
"django_version": smarter_settings.django_version,
"current_year": current_year,
}
}
logger.debug(
"%s.base() cached dashboard context for user %s: %s",
logger_prefix,
username,
logging.formatted_json(cached_context),
)
return cached_context
context = get_cached_context(username=resolved_user.username if resolved_user else "missing") # type: ignore[assignment]
return context
[docs]
def branding(request: "HttpRequest") -> dict[str, Any]:
"""
Provides organization-specific branding context for dashboard templates.
This context processor injects a comprehensive set of branding and support variables into the template context for all pages inheriting from ``base.html``. These variables ensure that consistent corporate identity, contact, and support information are available throughout the dashboard user interface.
The context includes:
- The root URL of the application, suitable for constructing absolute links.
- Support contact details, such as phone number and email address, for user assistance.
- Corporate name and physical address, for legal and informational display.
- General contact information and published support hours.
- A copyright notice, dynamically including the current year and corporate name.
- Social media profile URLs (Facebook, Twitter, LinkedIn) for brand presence and outreach.
All values are sourced from Django settings, allowing for easy customization and environment-specific overrides.
Example usage in a Django template::
{{ branding.corporate_name }}
{{ branding.support_email }}
{{ branding.copyright }}
This processor is intended to be added to the ``TEMPLATES['OPTIONS']['context_processors']`` list in your Django settings, making the ``branding`` context variable available in all templates rendered by Django that inherit from ``base.html``.
"""
@cache_results()
@snake_case()
def get_cached_context() -> dict[str, Any]:
current_year = datetime.now().year
root_url = request.build_absolute_uri("/").rstrip("/")
context = {
"branding": {
"canonical": request.path,
"root_url": root_url,
"corporate_name": smarter_settings.branding_corporate_name,
"corporate_address": ", ".join(
filter(
None,
[
smarter_settings.branding_address1,
smarter_settings.branding_address2,
smarter_settings.branding_city,
smarter_settings.branding_state,
smarter_settings.branding_postal_code,
smarter_settings.branding_country,
],
)
),
"corporate_currency": smarter_settings.branding_currency,
"corporate_timezone": smarter_settings.branding_timezone,
"support_email": smarter_settings.branding_support_email,
"contact_url": smarter_settings.branding_contact_url,
"support_hours": smarter_settings.branding_support_hours,
"support_phone_number": smarter_settings.branding_support_phone_number,
"copyright": f"© {current_year} {smarter_settings.branding_corporate_name}. All rights reserved.",
"og_url": smarter_settings.marketing_site_url,
"canonical_url": smarter_settings.environment_url,
"og_image": "https://cdn.smarter.sh/cms/img/smarter_og_image.png",
"url_facebook": smarter_settings.branding_url_facebook,
"url_twitter": smarter_settings.branding_url_twitter,
"url_linkedin": smarter_settings.branding_url_linkedin,
"smarter_logo": smarter_settings.logo,
"smarter_product_name": SMARTER_PRODUCT_NAME,
"smarter_product_description": SMARTER_PRODUCT_DESCRIPTION,
"smarter_marketing_site_url": smarter_settings.marketing_site_url,
"smarter_home_url": "/",
"smarter_project_website_url": smarter_settings.smarter_project_website_url,
"smarter_project_cdn_url": smarter_settings.smarter_project_cdn_url,
"smarter_project_docs_url": smarter_settings.smarter_project_docs_url,
"logo_url": "images/logo/smarter-crop.png",
"cdn_logo_url": urljoin(smarter_settings.smarter_project_cdn_url, "images/logo/smarter-crop.png"),
"login_url": urljoin(smarter_settings.environment_url, "/login/"),
"learn_url": smarter_settings.smarter_project_docs_url,
"workbench_exmample_url": urljoin(smarter_settings.environment_url, "/workbench/smarter/prompt/"),
}
}
logger.debug("%s.branding() cached branding context: %s", logger_prefix, logging.formatted_json(context))
return context
return get_cached_context()
[docs]
@snake_case()
def cache_buster(request) -> dict[str, Any]:
"""
Adds a cache-busting query parameter to static asset URLs during development.
This context processor is intended for use in local development environments
to ensure that browsers do not serve outdated versions of static files
(such as JavaScript, CSS, or images) from cache. It injects a ``cache_buster``
variable into the template context, which can be appended as a query parameter
to static asset URLs. The value is a version string based on the current
timestamp, guaranteeing uniqueness on each page load.
Example usage in a Django template::
<script src="{{ STATIC_URL }}main.js?{{ cache_buster }}"></script>
This approach is especially useful when making frequent changes to static
assets during development, as it forces the browser to fetch the latest
version every time the page is reloaded. In production, this processor is
typically disabled or omitted to allow for proper static file caching and
performance optimization.
The ``cache_buster`` variable is a string in the format ``v=<timestamp>``.
"""
return {"cache_buster": "v=" + str(time.time())}
[docs]
def cache_invalidations(user_profile: Optional[UserProfile]) -> None:
"""
Invalidates caches for all resource-counting context processors.
This function is
intended to be called after any operation that modifies the underlying user data.
.. note::
This is called by signal handlers in the account app, tied to the AbstractBroker.
.. seealso::
- :class:`smarter.lib.manifest.broker.AbstractBroker`
- ``smarter.apps.account.signals.cache_invalidate``
"""
if not user_profile:
logger.warning(
"%s.cache_invalidations() called without user_profile. No caches will be invalidated.",
logger_prefix_cache_invalidations,
)
return
logger.debug("%s called for %s", logger_prefix_cache_invalidations, user_profile)
###########################################################################
# resource invalidations
###########################################################################
if user_profile:
Account.get_cached_object(invalidate=True, pk=user_profile.account.id)
UserProfile.get_cached_object(invalidate=True, pk=user_profile.id) # type: ignore
PluginMeta.get_cached_plugins_for_user_profile_id(invalidate=True, user_profile_id=user_profile.id) # type: ignore
LLMClient.get_cached_objects(invalidate=True, user_profile=user_profile) # type: ignore