pax_global_header00006660000000000000000000000064150400332320014502gustar00rootroot0000000000000052 comment=38e9154be3c5e187f694d821a466b967485b3bdc django-navtag-3.3.1/000077500000000000000000000000001504003323200142265ustar00rootroot00000000000000django-navtag-3.3.1/.gitignore000066400000000000000000000001071504003323200162140ustar00rootroot00000000000000.tox/ *.pyc *.egg-info/ .eggs/ dist/ .coverage .python-version uv.lock django-navtag-3.3.1/.travis.yml000066400000000000000000000001711504003323200163360ustar00rootroot00000000000000sudo: false language: python python: - "3.6" - "3.7" - "3.8" - "3.9" install: pip install tox-travis script: tox django-navtag-3.3.1/CLAUDE.md000066400000000000000000000044001504003323200155030ustar00rootroot00000000000000# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Repository Overview This is `django-navtag`, a Django template tag library for handling navigation item selection in templates. It provides a simple way to manage active states in navigation menus across template inheritance hierarchies. ## Project Structure - `django_navtag/templatetags/navtag.py` - Core implementation (Nav class and template tags) - `django_navtag/tests/test_navtag.py` - Test suite - `django_navtag/tests/settings.py` - Django test settings ## Common Commands ### Development Setup ```bash # Install with development dependencies (requires pip >= 23.1) pip install -e ".[dev]" # Or using the dependency-groups syntax (pip >= 24.1) pip install -e . --dependency-groups=dev ``` ### Running Tests ```bash # Run tests with pytest pytest # Run with coverage pytest --cov # Run full test matrix across Python/Django versions tox # Run specific environment tox -e py39-django32 ``` ### Code Quality ```bash # Check README formatting tox -e readme # Run coverage checks (100% for tests, 90% for main code) tox -e coverage ``` ### Release Process - Uses zest.releaser for version management - Current version: 3.3.dev0 - Tag signing is enabled ## Key Implementation Details ### Nav Class (`django_navtag/templatetags/navtag.py`) The Nav class is the core of the library, providing: - Hierarchical navigation state management via dictionary structure - Special comparison operators (`__eq__`, `__contains__`) - Pattern matching with `!` syntax for children-only matching - Active path tracking via `get_active_path()` ### Template Tags 1. **`{% nav %}`** - Sets active navigation items - Supports hierarchical paths (e.g., `about_menu.info`) - Custom context variable names with `for` - Text output customization 2. **`{% navlink %}`** - Renders links/spans based on navigation state - Automatically switches between `` and `` elements - Supports special patterns and alternate nav variables ### Testing - Uses pytest with pytest-django - Test templates in `django_navtag/templates/navtag_tests/` - Django settings module: `django_navtag.tests.settings` ## Python/Django Support - Python: 3.8-3.12 - Django: 3.2, 4.2, 5.0, 5.1django-navtag-3.3.1/LICENSE000066400000000000000000000020411504003323200152300ustar00rootroot00000000000000Copyright (c) 2013, Chris Beaven Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-navtag-3.3.1/README.rst000066400000000000000000000166301504003323200157230ustar00rootroot00000000000000``{% nav %}`` tag ================= .. image:: https://badge.fury.io/py/django-navtag.svg :target: https://badge.fury.io/py/django-navtag .. image:: https://codecov.io/gh/SmileyChris/django-navtag/branch/master/graph/badge.svg :target: https://codecov.io/gh/SmileyChris/django-navtag A simple Django template tag to handle navigation item selection. .. contents:: :local: :backlinks: none Installation ------------ Install the package using pip: .. code:: bash pip install django-navtag Usage ----- Add the app to your ``INSTALLED_APPS`` setting: .. code:: python INSTALLED_APPS = ( # ... 'django_navtag', ) Give your base template a navigation block something like this: .. code:: jinja {% load navtag %} {% block nav %} {% nav text ' class="selected"' %} {% endblock %} In your templates, extend the base and set the navigation location: .. code:: jinja {% extends "base.html" %} {% block nav %} {% nav "home" %} {{ block.super }} {% endblock %} .. note:: This works for multiple levels of template inheritance, due to the fact that only the first ``{% nav %}`` call found will change the ``nav`` context variable. Hierarchical navigation ----------------------- To create a sub-menu you can check against, simply dot-separate the item: .. code:: jinja {% nav "about_menu.info" %} This will be pass for both ``{% if nav.about_menu %}`` and ``{% if nav.about_menu.info %}``. Using a different context variable ---------------------------------- By default, this tag creates a ``nav`` context variable. To use an alternate context variable name, call ``{% nav [item] for [var_name] %}``: .. code:: jinja {% block nav %} {% nav "home" for sidenav %} {{ block.super }} {% endblock %} Setting the text output by the nav variable ------------------------------------------- As shown in the initial example, you can set the text return value of the ``nav`` context variable. Use the format ``{% nav text [content] %}``. For example: .. code:: jinja {% nav text "active" %} Alternately, you can use boolean comparison of the context variable rather than text value: .. code:: jinja If using a different context variable name, use the format ``{% nav text [content] for [var_name] %}``. Comparison operations --------------------- The ``nav`` object supports comparison operations for more flexible navigation handling: **Exact matching with** ``==``: .. code:: jinja {% nav "products.phones" %} {% if nav == "products.phones" %} {# True - exact match #} {% endif %} {% if nav == "products" %} {# False - not exact #} {% endif %} **Special patterns with** ``!``: .. code:: jinja {% nav "products.electronics" %} {% if nav == "products!" %} {# True - matches any child of products #} {% endif %} {% if nav == "products!clearance" %} {# True - matches children except 'clearance' #} {% endif %} **Component checking with** ``in``: .. code:: jinja {% nav "products.electronics.phones" %} {% if "products" in nav %} {# True - component exists #} {% endif %} {% if "electronics" in nav %} {# True - component exists #} {% endif %} {% if "tablets" in nav %} {# False - component doesn't exist #} {% endif %} These operations also work with sub-navigation: .. code:: jinja {% nav "products.electronics.phones" %} {% if nav.products == "electronics.phones" %} {# True #} {% endif %} {% if "electronics" in nav.products %} {# True #} {% endif %} Iteration --------- The ``nav`` object supports iteration over its active path components: .. code:: jinja {% nav "products.electronics.phones" %} {% for component in nav %} {{ component }} {# Outputs: products, electronics, phones #} {% endfor %} This also works with sub-navigation: .. code:: jinja {% nav "products.electronics.phones" %} {% for component in nav.products %} {{ component }} {# Outputs: electronics, phones #} {% endfor %} The ``{% navlink %}`` tag ------------------------- The ``{% navlink %}`` tag provides a convenient way to create navigation links that automatically change based on the active navigation state. It works as a block tag that renders different HTML elements depending on whether the navigation item is active. Basic usage: .. code:: jinja {% load navtag %} {% nav text 'active' %} {% nav "products" %} The tag will render: - ``...`` - when the nav item is active - ``...`` - when the nav item is a parent of the active item - ``...`` - when the nav item is not active The second parameter uses Django's built-in ``{% url %}`` tag syntax, so you can pass URL names with arguments: .. code:: jinja {% navlink 'product' 'product_detail' product_id=product.id %} {{ product.name }} {% endnavlink %} Custom attributes ~~~~~~~~~~~~~~~~~ You can customize the attribute added to active links using ``{% nav text %}`` with an attribute format: .. code:: jinja {% nav text ' aria-selected="true"' %} {% nav "home" %} {% navlink 'home' 'home_url' %}Home{% endnavlink %} {# Renders: Home #} Special matching patterns ~~~~~~~~~~~~~~~~~~~~~~~~~ The ``{% navlink %}`` tag supports special patterns for more precise matching: **Children-only pattern** (``item!``): .. code:: jinja {% nav "courses.special" %} {% navlink 'courses' 'course_list' %}All Courses{% endnavlink %} {# Renders as link with class="active" #} {% navlink 'courses!' 'course_detail' %}Course Details{% endnavlink %} {# Renders as link with class="active" - only when nav is a child of courses #} When ``courses`` is active (not a child), the first link is active but the second becomes a ````. **Exclusion pattern** (``item!exclude``): .. code:: jinja {% nav "courses.special" %} {% navlink 'courses!list' 'course_detail' %}Course (not list){% endnavlink %} {# Renders as link - active for any child except 'list' #} {% navlink 'courses!special' 'course_detail' %}Course (not special){% endnavlink %} {# Renders as span - 'special' is excluded #} You can also use these patterns with ``{% if %}`` statements: .. code:: jinja {% if nav == "courses!" %} {# True - matches any child of courses #} {% endif %} Alternate nav context ~~~~~~~~~~~~~~~~~~~~~ To use a different navigation context variable, prefix the nav item with the variable name: .. code:: jinja {% nav "products" for mainnav %} {% nav "settings" for sidenav %} {% navlink 'mainnav:products' 'product_list' %}Products{% endnavlink %} {% navlink 'sidenav:settings' 'user_settings' %}Settings{% endnavlink %} django-navtag-3.3.1/deploy.py000077500000000000000000000262571504003323200161130ustar00rootroot00000000000000#!/usr/bin/env python3 # /// script # requires-python = ">=3.8" # dependencies = [ # "keyring", # ] # /// """Deploy package to PyPI.""" import argparse import configparser import os import shutil import subprocess import sys from pathlib import Path def run_command(cmd, check=True, capture_output=False): """Run a command and return the result.""" result = subprocess.run( cmd, shell=True, check=check, capture_output=capture_output, text=True ) if capture_output: return result.stdout.strip() else: return result def check_uv_installed(): """Check if uv is installed.""" if shutil.which("uv") is None: print("Error: 'uv' is not installed. Please install it first.") print("Visit: https://github.com/astral-sh/uv#installation") sys.exit(1) print("✓ uv is installed") def ensure_uv_env_vars(): """Ensure required UV environment variables are set.""" required_vars = { "UV_KEYRING_PROVIDER": "subprocess", "UV_PUBLISH_USERNAME": "__token__", } vars_to_set = {} for var, value in required_vars.items(): if os.environ.get(var) != value: vars_to_set[var] = value os.environ[var] = value if not vars_to_set: print("✓ UV environment variables already configured") return print("Setting UV environment variables for this session...") for var, value in vars_to_set.items(): print(f" {var}={value}") # Detect shell and update config file shell = os.environ.get("SHELL", "").split("/")[-1] home = Path.home() shell_configs = { "bash": home / ".bashrc", "zsh": home / ".zshrc", "fish": home / ".config/fish/config.fish", } config_file = shell_configs.get(shell) if not config_file or not config_file.exists(): # Try to find which one exists for shell_name, path in shell_configs.items(): if path.exists(): config_file = path shell = shell_name break if config_file and config_file.exists(): print(f"\nUpdating {config_file.name}...") with open(config_file, "r") as f: content = f.read() # Check if variables are already in the file lines_to_add = [] for var, value in vars_to_set.items(): if f"export {var}=" not in content: lines_to_add.append(f"export {var}={value}") if lines_to_add: with open(config_file, "a") as f: f.write("\n# UV configuration for PyPI publishing\n") for line in lines_to_add: f.write(f"{line}\n") print(f"✓ Added UV environment variables to {config_file.name}") print( f" Note: Restart your shell or run 'source {config_file}' to apply changes" ) else: print(f"✓ UV environment variables already in {config_file.name}") else: print("\n⚠️ Could not detect shell configuration file") print("Please manually add these to your shell configuration:") for var, value in vars_to_set.items(): print(f" export {var}={value}") def ensure_keyring(): """Ensure keyring is installed and configured.""" # keyring will be automatically installed by uv due to script dependencies import keyring # Check if token is already in keyring try: existing_token = keyring.get_password( "https://upload.pypi.org/legacy/", "__token__" ) if existing_token: print("✓ PyPI token already configured in keyring") return except Exception: pass # Try to get token from .pypirc pypirc_path = Path.home() / ".pypirc" token = None if pypirc_path.exists(): config = configparser.ConfigParser() config.read(pypirc_path) # Try to find token in various sections for section in ["pypi", "pypirc"]: if section in config: if "password" in config[section]: password = config[section]["password"] if password.startswith("pypi-"): token = password break if token: print("Found PyPI token in .pypirc, adding to keyring...") run_command( f"keyring set 'https://upload.pypi.org/legacy/' __token__ <<< '{token}'", shell=True, ) print("✓ PyPI token configured in keyring") else: print("\nNo PyPI token found in .pypirc") print("Please manually set your token with:") print(" keyring set 'https://upload.pypi.org/legacy/' __token__") print("\nYou can find your token at: https://pypi.org/manage/account/token/") sys.exit(1) def check_git_status(): """Ensure git working directory is clean.""" status = run_command("git status --porcelain", capture_output=True) if status: print("\nError: Git working directory is not clean!") print("Please commit or stash your changes first.") print("\nUncommitted changes:") print(status) sys.exit(1) print("✓ Git working directory is clean") def run_tests(): """Run full test suite with tox in parallel.""" print("\nRunning tests with tox (parallel)...") result = run_command("uv run tox -p auto", check=False) if result.returncode != 0: print("\nError: Tests failed!") sys.exit(1) print("✓ All tests passed") def get_current_version(): """Get current version from pyproject.toml.""" output = run_command("uv version --short", capture_output=True) return output.strip() def determine_bump_type(current_version): """Ask user for version bump type.""" print(f"\nCurrent version: {current_version}") try: if "dev" in current_version: print("\nThis is a development version.") choice = input("Release as stable version? [Y/n]: ").strip().lower() if choice in ["", "y", "yes"]: return "stable" # For both dev versions (if user said no) and stable versions print("\nSelect version bump type:") print("1. Major (X.0.0)") print("2. Minor (x.Y.0)") print("3. Patch (x.y.Z)") while True: choice = input("Enter choice [1-3]: ").strip() if choice == "1": return "major" elif choice == "2": return "minor" elif choice == "3": return "patch" else: print("Invalid choice. Please enter 1, 2, or 3.") except KeyboardInterrupt: return None def bump_version(bump_type): """Bump version using uv.""" print(f"Bumping {bump_type} version...") run_command(f"uv version --bump {bump_type}") new_version = get_current_version() print(f"✓ Version bumped to: {new_version}") return new_version def commit_version(version): """Commit version bump.""" print("\nCommitting version bump...") run_command("git add pyproject.toml") run_command(f'git commit -m "Bump version to {version}"') print("✓ Version bump committed") def build_package(): """Build the package.""" print("\nBuilding package...") # Clean up old builds run_command("rm -rf dist/ build/ *.egg-info", check=False) result = run_command("uv build --no-build-logs", check=False) if result.returncode != 0: print("\nError: Package build failed!") sys.exit(1) print("✓ Package built successfully") def publish_package(): """Publish package to PyPI.""" print("\n" + "=" * 60) print("READY TO PUBLISH TO PYPI") print("=" * 60) # Show what will be uploaded dist_files = sorted(Path("dist").glob("*")) print("\nFiles to be uploaded:") for f in dist_files: if f.is_file() and f.name not in [".gitignore", ".DS_Store"]: print(f" - {f.name}") print("\nThis will upload the package to PyPI (production)!") confirm = input("Are you sure you want to continue? [y/N]: ").strip().lower() if confirm != "y": print("\nPublish cancelled.") return False print("\nPublishing to PyPI...") run_command("uv publish") print("\n✓ Package published successfully!") return True def create_git_tag(version): """Create and push git tag.""" tag_name = f"v{version}" print(f"\nCreating git tag {tag_name}...") run_command(f"git tag -s {tag_name} -m 'Release version {version}'") print(f"✓ Git tag {tag_name} created") # Get current branch name current_branch = run_command("git branch --show-current", capture_output=True) push_tag = ( input( f"\nPush branch '{current_branch}' and tag '{tag_name}' to origin? [Y/n]: " ) .strip() .lower() ) if push_tag in ["", "y", "yes"]: print("Pushing branch and tag to origin...") run_command(f"git push origin {current_branch} {tag_name}") print(f"✓ Branch '{current_branch}' and tag '{tag_name}' pushed to origin") def main(): """Main deployment function.""" parser = argparse.ArgumentParser(description="Deploy package to PyPI") parser.add_argument( "command", nargs="?", choices=["tag", "bump"], help="Command to run (e.g., 'tag', 'bump')", ) parser.add_argument("--version", help="Version to tag (for 'tag' command)") parser.add_argument( "--type", choices=["major", "minor", "patch", "stable"], help="Version bump type (for 'bump' command)", ) args = parser.parse_args() # Change to project directory script_dir = Path(__file__).parent os.chdir(script_dir) # Handle tag command if args.command == "tag": check_uv_installed() check_git_status() version = args.version or get_current_version() create_git_tag(version) print(f"\n✅ Tag v{version} created successfully!") return # Handle bump command if args.command == "bump": check_uv_installed() check_git_status() current_version = get_current_version() if args.type: bump_type = args.type else: bump_type = determine_bump_type(current_version) if bump_type is None: print("\nBump cancelled.") return new_version = bump_version(bump_type) commit_version(new_version) print(f"\n✅ Version bumped to {new_version} and committed!") return # Full deployment flow check_uv_installed() ensure_uv_env_vars() ensure_keyring() check_git_status() run_tests() current_version = get_current_version() bump_type = determine_bump_type(current_version) if bump_type is None: print("\nDeployment cancelled.") return new_version = bump_version(bump_type) commit_version(new_version) build_package() if publish_package(): create_git_tag(new_version) print("\n✅ Deployment completed successfully!") else: print("\n⚠️ Package built but not published.") print("You can manually publish later with: uv publish") if __name__ == "__main__": main() django-navtag-3.3.1/django_navtag/000077500000000000000000000000001504003323200170305ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/__init__.py000066400000000000000000000000001504003323200211270ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/models.py000066400000000000000000000000001504003323200206530ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templates/000077500000000000000000000000001504003323200210265ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templates/navtag_tests/000077500000000000000000000000001504003323200235305ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templates/navtag_tests/base.txt000066400000000000000000000002361504003323200252040ustar00rootroot00000000000000{% block nav %} - Home {% if nav.home %}(active){% endif %} - Contact {% if nav.contact %}(active){% endif %} {% endblock %} {% block main %} {% endblock %} django-navtag-3.3.1/django_navtag/templates/navtag_tests/contact.txt000066400000000000000000000001751504003323200257270ustar00rootroot00000000000000{% extends "navtag_tests/base.txt" %} {% load navtag %} {% block nav %} {% nav "contact" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/context/000077500000000000000000000000001504003323200252145ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templates/navtag_tests/context/home.txt000066400000000000000000000003121504003323200267010ustar00rootroot00000000000000{% extends "navtag_tests/base.txt" %} {% load navtag %} {% block nav %} {% nav "home" %} {{ block.super }} {% endblock %} {% block main %} {% if nav.home %}HOME{% else %}huh?{% endif %} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/home-unset.txt000066400000000000000000000001661504003323200263600ustar00rootroot00000000000000{% extends "navtag_tests/home.txt" %} {% load navtag %} {% block nav %} {% nav "" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/home.txt000066400000000000000000000001721504003323200252210ustar00rootroot00000000000000{% extends "navtag_tests/base.txt" %} {% load navtag %} {% block nav %} {% nav "home" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/submenu/000077500000000000000000000000001504003323200252065ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templates/navtag_tests/submenu/apple.txt000066400000000000000000000002171504003323200270500ustar00rootroot00000000000000{% extends "navtag_tests/submenu/base_fruit.txt" %} {% load navtag %} {% block nav %} {% nav "fruit.apple" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/submenu/banana.txt000066400000000000000000000002201504003323200271610ustar00rootroot00000000000000{% extends "navtag_tests/submenu/base_fruit.txt" %} {% load navtag %} {% block nav %} {% nav "fruit.banana" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/submenu/base.txt000066400000000000000000000003471504003323200266650ustar00rootroot00000000000000{% block nav %} - Home {% if nav.home %}(active){% endif %} - Fruit {% if nav.fruit %}(active){% endif %} - Apple {% if nav.fruit.apple %}(active){% endif %} - Banana {% if nav.fruit.banana %}(active){% endif %} {% endblock %} django-navtag-3.3.1/django_navtag/templates/navtag_tests/submenu/base_fruit.txt000066400000000000000000000002031504003323200300650ustar00rootroot00000000000000{% extends "navtag_tests/submenu/base.txt" %} {% load navtag %} {% block nav %} {% nav "fruit" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/submenu/home.txt000066400000000000000000000002021504003323200266710ustar00rootroot00000000000000{% extends "navtag_tests/submenu/base.txt" %} {% load navtag %} {% block nav %} {% nav "home" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/text/000077500000000000000000000000001504003323200245145ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templates/navtag_tests/text/base.txt000066400000000000000000000002411504003323200261640ustar00rootroot00000000000000{% load navtag %} {% block nav %} {% nav text " [is active]" %} - Home{{ nav.home }} - Contact{{ nav.contact }} {% endblock %} {% block main %} {% endblock %} django-navtag-3.3.1/django_navtag/templates/navtag_tests/text/contact.txt000066400000000000000000000002021504003323200267020ustar00rootroot00000000000000{% extends "navtag_tests/text/base.txt" %} {% load navtag %} {% block nav %} {% nav "contact" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templates/navtag_tests/text/home.txt000066400000000000000000000001771504003323200262120ustar00rootroot00000000000000{% extends "navtag_tests/text/base.txt" %} {% load navtag %} {% block nav %} {% nav "home" %} {{ block.super }} {% endblock %}django-navtag-3.3.1/django_navtag/templatetags/000077500000000000000000000000001504003323200215225ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templatetags/__init__.py000066400000000000000000000000001504003323200236210ustar00rootroot00000000000000django-navtag-3.3.1/django_navtag/templatetags/navtag.py000066400000000000000000000307471504003323200233670ustar00rootroot00000000000000from django import template from django.utils.encoding import smart_str from django.utils.safestring import mark_safe register = template.Library() class Nav(object): def __init__(self, tree=None, root=None): self._root = root or self self._tree = tree or {} def __getitem__(self, key): return Nav(self._tree[key], root=self._root) def __str__(self): return mark_safe(str(self._text)) def __bool__(self): return bool(self._tree) def _get_text(self): if hasattr(self._root, "_text_value"): return self._root._text_value return self._tree def _set_text(self, value): self._root._text_value = value _text = property(_get_text, _set_text) def clear(self): self._tree = {} def update(self, *args, **kwargs): self._tree.update(*args, **kwargs) def get_active_path(self, path=""): """Get the dotted path of the active navigation item""" # Handle case where _tree is not a dict (e.g., True for leaf nodes) if not isinstance(self._tree, dict): return "" for key, value in self._tree.items(): current_path = path + "." + key if path else key if isinstance(value, dict): # Recurse into nested nav sub_nav = Nav(value, root=self._root) result = sub_nav.get_active_path(current_path) if result: return result elif value: return current_path return "" def __eq__(self, other): """Check if the active navigation path matches the given pattern Patterns: - "item" - exact match - "item!" - children only (not exact match) - "item!exclude" - children except 'exclude' """ if isinstance(other, str): active_path = self.get_active_path() if "!" in other: parts = other.split("!", 1) parent = parts[0] exclude = parts[1] if len(parts) > 1 and parts[1] else None if exclude: # Pattern like 'courses!list' - match children except specific ones return ( active_path.startswith(parent + ".") and active_path != parent and not active_path.startswith(parent + "." + exclude) ) else: # Pattern like 'courses!' - match children only, not exact return ( active_path.startswith(parent + ".") and active_path != parent ) else: # Normal pattern - exact match return active_path == other elif isinstance(other, Nav): return self.get_active_path() == other.get_active_path() return False def __contains__(self, item): """Check if a component is part of the active navigation path""" if isinstance(item, str): active_path = self.get_active_path() if not active_path: return False # Check if the component matches any part of the path components = active_path.split(".") return item in components return False def __iter__(self): """Iterate over the active path components""" active_path = self.get_active_path() if active_path: for part in active_path.split("."): yield part class NavNode(template.Node): def __init__(self, item=None, var_for=None, var_text=None): self.item = item self.var_name = var_for or "nav" self.text = var_text def render(self, context): first_context_stack = context.dicts[0] nav = first_context_stack.get(self.var_name) if nav is not context.get(self.var_name): raise template.TemplateSyntaxError( "'{0}' variable has been altered in current context".format( self.var_name ) ) if not isinstance(nav, Nav): nav = Nav() # Copy the stack to avoid leaking into other contexts. new_first_context_stack = first_context_stack.copy() new_first_context_stack[self.var_name] = nav context.dicts[0] = new_first_context_stack if self.text: nav._text = self.text.resolve(context) return "" # If self.item was blank then there's nothing else to do here. if not self.item: return "" if nav: # If the nav variable is already set, don't do anything. return "" item = self.item.resolve(context) item = item and smart_str(item) value = True if not item: item = "" for part in reversed(item.split(".")): new_item = {} new_item[part] = value value = new_item nav.clear() nav.update(new_item) return "" def __repr__(self): return "