# Plugins DJ Press includes a plugin system that allows you to extend its functionality. Plugins can modify content before and after rendering, send notifications when content is published, and more. The plugin system is designed to be easy to use while still being powerful enough for complex extensions. ## Plugin Lifecycle The DJ Press plugin system follows this lifecycle: 1. **Discovery** - Plugins are loaded from the `PLUGINS` setting during application startup 2. **Registration** - Plugins register callbacks for specific hooks 3. **Execution** - Callbacks are executed when hooks are triggered during normal application operation 4. **Cleanup** - When Django shuts down, plugins can perform cleanup (if needed) ## Creating a Plugin To create a plugin, create a new Python package with the following structure: ```text djpress_my_plugin/ __init__.py plugin.py tests/ __init__.py test_plugin.py ``` In `plugin.py`, create a class called `Plugin` that inherits from `DJPressPlugin`: ```python from djpress.plugins import DJPressPlugin from djpress.plugin.hook_registry import PRE_RENDER_CONTENT, POST_RENDER_CONTENT, POST_SAVE_POST class Plugin(DJPressPlugin): name = "djpress_my_plugin" # Required - use same name as package hooks = [ (PRE_RENDER_CONTENT, "modify_content"), (POST_RENDER_CONTENT, "modify_html"), (POST_SAVE_POST, "notify_on_publish"), ] def modify_content(self, content: str) -> str: """Modify the markdown content before rendering. This callback implements the PreRenderHook signature. Args: content: The raw markdown content Returns: Modified markdown content """ try: # Always wrap plugin code in try/except to prevent site breakage if not content or not isinstance(content, str): return content # Example: Add a header to all content prefix = self.config.get("prefix_text", "") if prefix: content = f"{prefix}\n\n{content}" return content except Exception as e: # Log the error but don't break the site import logging logging.error(f"Error in modify_content: {e}") return content def modify_html(self, html_content: str) -> str: """Modify the HTML after markdown rendering. Args: html_content: The rendered HTML content Returns: Modified HTML content """ try: # Example: Add a footer to all content suffix = self.config.get("suffix_text", "") if suffix and html_content: html_content = f"{html_content}
" return html_content except Exception as e: import logging logging.error(f"Error in modify_html: {e}") return html_content def notify_on_publish(self, post): """Send notification when a post is published. Args: post: The Post object that was published Returns: None (return value is ignored for this hook) """ try: # Example: Track published posts in plugin storage data = self.get_data() or {"published_posts": []} # Add the post ID to our tracking list if not already there if post.id not in data.get("published_posts", []): data.setdefault("published_posts", []).append(post.id) self.save_data(data) # Example: Could send an email, ping a webhook, etc. if self.config.get("send_notifications"): self._send_notification(post) except Exception as e: import logging logging.error(f"Error in notify_on_publish: {e}") def _send_notification(self, post): """Private helper method to send notifications.""" # Implementation would depend on notification method pass ``` ## Saving Plugin Data Plugins can store data in the database through the `PluginStorage` model. This is useful for maintaining state between requests or for caching data. ```python # Get the current data (returns None if no data exists) data = self.get_data() # Initialise if needed if data is None: data = {"counter": 0, "settings": {}} # Update the data data["counter"] += 1 data["settings"]["last_updated"] = str(timezone.now()) # Save the data back to the database self.save_data(data) ``` The data must be JSON-serialisable (dictionaries, lists, strings, numbers, booleans, or None). ## Available Hooks DJ Press provides these hooks for plugins: | Hook Name | Description | Arguments | Expected Return | |-----------------------|-------------------------------------------------------|---------------------------|---------------------------| | `PRE_RENDER_CONTENT` | Called before markdown content is rendered to HTML | `content: str` (markdown) | Modified markdown content | | `POST_RENDER_CONTENT` | Called after markdown content is rendered to HTML | `content: str` (HTML) | Modified HTML content | | `POST_SAVE_POST` | Called after saving a published post | `post: Post` (object) | None (return ignored) | | `DJ_HEADER` | Used to insert HTML into the template's `` tag. | None | HTML content (`str`) | | `DJ_FOOTER` | Called after saving a published post | `post: Post` (object) | None (return ignored) | Hooks are imported from the `hook_registry` module, and then assigned to a list called `hooks` in the `Plugin` class. The hook is added to the list as a tuple with the first element being the hook, and the second element being the string name of the callable. ```python from djpress.plugin.hook_registry import PRE_RENDER_CONTENT # Define a hooks list of tuples: hooks = [(PRE_RENDER_CONTENT, "modify_content")] ``` ## Installing Plugins To install a plugin, follow these steps: 1. Install the plugin package: ```bash pip install djpress-my-plugin ``` 1. Add the plugin to your settings: ```python DJPRESS_SETTINGS = { "PLUGINS": [ "djpress_my_plugin" # Package name ], } ``` 1. Run migrations and restart your server: ```bash python manage.py migrate ``` If your plugin is structured differently, specify the full path: ```python DJPRESS_SETTINGS = { "PLUGINS": [ "djpress_my_plugin.custom.MyPlugin" # Full path to class ], } ``` ## Plugin Configuration Configure plugins using the `PLUGIN_SETTINGS` dictionary: ```python DJPRESS_SETTINGS = { "PLUGINS": ["djpress_example_plugin"], "PLUGIN_SETTINGS": { "djpress_example_plugin": { # Must match plugin's 'name' "prefix_text": "### Featured Content", "suffix_text": "Thanks for reading!", "send_notifications": True, "notification_email": "admin@example.com", }, }, } ``` Access settings in your plugin using `self.config`: ```python # Get with default value if setting doesn't exist prefix = self.config.get("prefix_text", "Default prefix") # Check if a setting exists if "send_notifications" in self.config: # Do something ``` ## Error Handling Proper error handling is essential for plugins. Always wrap your code in try/except blocks: ```python def my_hook_handler(self, content): try: # Your code here return modified_content except Exception as e: import logging logging.error(f"Plugin error in my_hook_handler: {e}") # Return original content to avoid breaking the site return content ``` ## Plugin Development Guidelines 1. **Unique Name**: Define a unique `name` property that matches your package name. 2. **Error Handling**: Always use try/except to prevent crashing the site. 3. **Type Hints**: Use type hints for better code maintainability. 4. **Documentation**: Document your plugin's purpose, hooks, and configuration options. 5. **Testing**: Include comprehensive tests for your plugin's functionality. 6. **Performance**: Be mindful of performance in hooks that run frequently. 7. **Security**: Validate and sanitise any user-provided data. ## System Checks DJ Press includes system checks that will warn about: - Unknown hooks (might indicate deprecated hooks or version mismatches) - Plugins that fail to load or initialise properly Run Django's check framework to see any warnings: ```bash python manage.py check ``` ## Complete Example Plugin Check out the [example plugin](https://github.com/stuartmaxwell/djpress/tree/main/djpress-example-plugin) included with DJ Press for a working reference implementation.