Skip to content

Commit

Permalink
Merge pull request #71 from stuartmaxwell:plugin-storage
Browse files Browse the repository at this point in the history
Plugin-storage
  • Loading branch information
stuartmaxwell authored Nov 20, 2024
2 parents 976a39f + e89c859 commit f0b5dfe
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 10 deletions.
21 changes: 17 additions & 4 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/djpress/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""djpress module."""

__version__ = "0.12.2"
__version__ = "0.13.0"
13 changes: 12 additions & 1 deletion src/djpress/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"]
24 changes: 24 additions & 0 deletions src/djpress/migrations/0007_pluginstorage.py
Original file line number Diff line number Diff line change
@@ -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",
},
),
]
3 changes: 2 additions & 1 deletion src/djpress/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
62 changes: 62 additions & 0 deletions src/djpress/models/plugin_storage.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions src/djpress/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
37 changes: 37 additions & 0 deletions tests/test_models_pluginstorage.py
Original file line number Diff line number Diff line change
@@ -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"}
27 changes: 27 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f0b5dfe

Please sign in to comment.