diff --git a/docs/plugins.md b/docs/plugins.md index 6952e50..650f300 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -37,20 +37,33 @@ class Plugin(DJPressPlugin): return content ``` +## Saving Plugin Data + +Plugins can store a blob of JSON data in the database through the use of a JSONField on the PluginStorage model. + +To retrieve the data, plugins can use: `data = self.get_data()`, and to save the data: `self.save_data(data)`. + ## Available Hooks Currently available hooks: -- `pre_render_content`: Called before markdown content is rendered to HTML -- `post_render_content`: Called after markdown content is rendered to HTML +- `pre_render_content`: Called before markdown content is rendered to HTML. Passes the content to the plugin and + expects to get content back. +- `post_render_content`: Called after markdown content is rendered to HTML. Passes the content to the plugin and + expects to get content back. +- `post_save_post`: Called after saving a published post. Passes the published post to the plugin and ignores any + returned values. **Note** that you can also import the `Hooks` enum class, and reference the hook names specifically, e.g. -`from djpress.plugins import Hooks` and then you can refer to the above two hooks as follows: +`from djpress.plugins import Hooks` and then you can refer to the above hooks as follows: - `Hooks.PRE_RENDER_CONTENT` - `Hooks.POST_RENDER_CONTENT` +- `Hooks.POST_SAVE_POST` -Each hook receives the content as its first argument and must return the modified content. +Each hook receives a value as its first argument and returns a value to be used by DJ Press. For example, the hooks +relating to rendering content expect to receive content back to continue processing. However, the `POST_SAVE_POST` hook +ignores any returned values since it has finished processing that step. ## Installing Plugins diff --git a/pyproject.toml b/pyproject.toml index 75e7066..a097931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "djpress" -version = "0.12.2" +version = "0.13.0" description = "A blog application for Django sites, inspired by classic WordPress." readme = "README.md" requires-python = ">=3.10" @@ -96,7 +96,7 @@ include = ["src/djpress/*"] omit = ["*/tests/*", "*/migrations/*"] [tool.bumpver] -current_version = "0.12.2" +current_version = "0.13.0" version_pattern = "MAJOR.MINOR.PATCH" commit_message = "👍 bump version {old_version} -> {new_version}" commit = true diff --git a/src/djpress/__init__.py b/src/djpress/__init__.py index 61bb9e3..5c8bfae 100644 --- a/src/djpress/__init__.py +++ b/src/djpress/__init__.py @@ -1,3 +1,3 @@ """djpress module.""" -__version__ = "0.12.2" +__version__ = "0.13.0" diff --git a/src/djpress/admin.py b/src/djpress/admin.py index 755d096..8234878 100644 --- a/src/djpress/admin.py +++ b/src/djpress/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin # Register the models here. -from djpress.models import Category, Post +from djpress.models import Category, PluginStorage, Post @admin.register(Category) @@ -23,3 +23,14 @@ class PostAdmin(admin.ModelAdmin): list_display_links: ClassVar["str"] = ["title", "slug"] ordering: ClassVar["str"] = ["post_type", "-date"] # Displays pages first, then sorted by date. list_filter: ClassVar["str"] = ["post_type", "date", "author"] + + +@admin.register(PluginStorage) +class PluginStorageAdmin(admin.ModelAdmin): + """PluginStorage admin configuration.""" + + list_display: ClassVar["str"] = ["plugin_name", "plugin_data"] + list_display_links: ClassVar["str"] = ["plugin_name"] + ordering: ClassVar["str"] = ["plugin_name"] + search_fields: ClassVar["str"] = ["plugin_name"] + list_filter: ClassVar["str"] = ["plugin_name"] diff --git a/src/djpress/migrations/0007_pluginstorage.py b/src/djpress/migrations/0007_pluginstorage.py new file mode 100644 index 0000000..b5b1d10 --- /dev/null +++ b/src/djpress/migrations/0007_pluginstorage.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.3 on 2024-11-20 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("djpress", "0006_alter_post_parent"), + ] + + operations = [ + migrations.CreateModel( + name="PluginStorage", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("plugin_name", models.CharField(max_length=100, unique=True)), + ("plugin_data", models.JSONField(default=dict)), + ], + options={ + "verbose_name": "plugin storage", + "verbose_name_plural": "plugin storage", + }, + ), + ] diff --git a/src/djpress/models/__init__.py b/src/djpress/models/__init__.py index 0d9d478..fa16a41 100644 --- a/src/djpress/models/__init__.py +++ b/src/djpress/models/__init__.py @@ -1,6 +1,7 @@ """Models package for djpress app.""" from djpress.models.category import Category +from djpress.models.plugin_storage import PluginStorage from djpress.models.post import Post -__all__ = ["Category", "Post"] +__all__ = ["Category", "Post", "PluginStorage"] diff --git a/src/djpress/models/plugin_storage.py b/src/djpress/models/plugin_storage.py new file mode 100644 index 0000000..1d8fc14 --- /dev/null +++ b/src/djpress/models/plugin_storage.py @@ -0,0 +1,62 @@ +"""PluginStorage model for storing plugin data in the database.""" + +from django.db import models + + +class PluginStorageManager(models.Manager): + """Manager for the PluginStorage model.""" + + def get_data(self, plugin_name: str) -> dict: + """Get plugin data. + + Retrieve the plugin data from the database. If the plugin data does not exist, return an empty dictionary. + + Args: + plugin_name (str): The name of the plugin. + + Returns: + dict: The plugin data. + """ + try: + data = self.get(plugin_name=plugin_name) + except self.model.DoesNotExist: + return {} + else: + return data.plugin_data or {} + + def save_data(self, plugin_name: str, data: dict) -> None: + """Save plugin data. + + Save or update the plugin data in the database. If no storage exists for this plugin, it will be created. + + Args: + plugin_name (str): The name of the plugin. + data (dict): The plugin data. + + Returns: + None + """ + storage, created = self.update_or_create( + plugin_name=plugin_name, + defaults={"plugin_data": data}, + ) + + +class PluginStorage(models.Model): + """Model for storing plugin data in the database.""" + + plugin_name = models.CharField(max_length=100, unique=True) + plugin_data = models.JSONField(default=dict) + + # Manager + objects = PluginStorageManager() + + class Meta: + """Meta options for the PluginStorage model.""" + + verbose_name = "plugin storage" + verbose_name_plural = "plugin storage" + + def __str__(self) -> str: + """Return the string representation of the plugin item.""" + return self.plugin_name diff --git a/src/djpress/plugins.py b/src/djpress/plugins.py index 94b4644..a2ab7a4 100644 --- a/src/djpress/plugins.py +++ b/src/djpress/plugins.py @@ -185,6 +185,26 @@ def setup(self, registry: PluginRegistry) -> None: registry (PluginRegistry): The plugin registry. """ + def get_data(self) -> dict: + """Get this plugin's stored data. + + Returns: + dict: The plugin's stored data, or empty dict if none exists. + """ + from djpress.models import PluginStorage + + return PluginStorage.objects.get_data(self.name) + + def save_data(self, data: dict) -> None: + """Save this plugin's data. + + Args: + data: The data to store for this plugin. + """ + from djpress.models import PluginStorage + + PluginStorage.objects.save_data(self.name, data) + # Instantiate the global plugin registry registry = PluginRegistry() diff --git a/tests/test_models_pluginstorage.py b/tests/test_models_pluginstorage.py new file mode 100644 index 0000000..8f3b058 --- /dev/null +++ b/tests/test_models_pluginstorage.py @@ -0,0 +1,37 @@ +import pytest + +from djpress.models import PluginStorage + + +@pytest.mark.django_db +def test_plugin_storage_model(): + test_plugin = PluginStorage.objects.create(plugin_name="test_plugin") + + assert str(test_plugin) == "test_plugin" + + +@pytest.mark.django_db +def test_plugin_storage_get_data(): + """Test getting plugin data.""" + # Test empty case (no storage exists) + data = PluginStorage.objects.get_data("test_plugin") + assert data == {} + + # Test with stored data + PluginStorage.objects.save_data("test_plugin", {"key": "value"}) + data = PluginStorage.objects.get_data("test_plugin") + assert data == {"key": "value"} + + +@pytest.mark.django_db +def test_plugin_storage_save_data(): + """Test saving plugin data.""" + # Test creating new storage + PluginStorage.objects.save_data("test_plugin", {"key": "value"}) + storage = PluginStorage.objects.get(plugin_name="test_plugin") + assert storage.plugin_data == {"key": "value"} + + # Test updating existing storage + PluginStorage.objects.save_data("test_plugin", {"new_key": "new_value"}) + storage.refresh_from_db() + assert storage.plugin_data == {"new_key": "new_value"} diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 3209cc3..82c1e3f 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -321,3 +321,30 @@ def test_callback(content: str) -> str: registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback) result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test") assert result == "test modified" + + +@pytest.mark.django_db +def test_plugin_storage_interface(): + """Test plugin storage interface methods.""" + + class TestPlugin(DJPressPlugin): + name = "test_plugin" + + plugin = TestPlugin() + + # Test get_data with no storage + assert plugin.get_data() == {} + + # Test save_data and get_data + plugin.save_data({"key": "value"}) + assert plugin.get_data() == {"key": "value"} + + # Test save_data with update + plugin.save_data({"new_key": "new_value"}) + assert plugin.get_data() == {"new_key": "new_value"} + + # Test save additional data + data = plugin.get_data() + data["extra"] = "data" + plugin.save_data(data) + assert plugin.get_data() == {"new_key": "new_value", "extra": "data"} diff --git a/uv.lock b/uv.lock index 27f10d3..e2a88d1 100644 --- a/uv.lock +++ b/uv.lock @@ -310,7 +310,7 @@ wheels = [ [[package]] name = "djpress" -version = "0.12.2" +version = "0.13.0" source = { editable = "." } dependencies = [ { name = "django" },