diff --git a/dev-docs/plugins/hooks/action-hook.md b/dev-docs/plugins/hooks/action-hook.md index 9e7031a6ef..ce4b28087f 100644 --- a/dev-docs/plugins/hooks/action-hook.md +++ b/dev-docs/plugins/hooks/action-hook.md @@ -26,29 +26,29 @@ Our example Django app looks like this: ``` stats_app/ - __init__.py - stats.py + __init__.py + stats.py ``` We will create a new Python package inside the `stats_app` and name it `hooks`: ``` stats_app/ - hooks/ + hooks/ + __init__.py __init__.py - __init__.py - stats.py + stats.py ``` Next, we will create an empty `get_stats.py` file in `hooks`: ``` stats_app/ - hooks/ + hooks/ + __init__.py + get_stats.py __init__.py - get_stats.py - __init__.py - stats.py + stats.py ``` Our hook's definition will be located in the `get_stats.py` file, but its instance will be re-exported from the `hooks/__init__.py` file. diff --git a/dev-docs/plugins/hooks/filter-hook.md b/dev-docs/plugins/hooks/filter-hook.md index e001523b1d..1afa7a0fd4 100644 --- a/dev-docs/plugins/hooks/filter-hook.md +++ b/dev-docs/plugins/hooks/filter-hook.md @@ -26,29 +26,29 @@ Our example function lives in an example Django app, looking like this: ``` parser_app/ - __init__.py - parser.py + __init__.py + parser.py ``` We will create a new Python package inside the `parser_app` and name it `hooks`: ``` parser_app/ - hooks/ + hooks/ + __init__.py __init__.py - __init__.py - parser.py + parser.py ``` Next, we will create an empty `parse_user_message.py` file in `hooks`: ``` parser_app/ - hooks/ + hooks/ + __init__.py + parse_user_message.py __init__.py - parse_user_message.py - __init__.py - parser.py + parser.py ``` Our hook's definition will be located in the `parse_user_message.py` file, but its instance will be re-exported from the `hooks/__init__.py` file. diff --git a/dev-docs/plugins/index.md b/dev-docs/plugins/index.md index ff3e49a4ca..fea81159dd 100644 --- a/dev-docs/plugins/index.md +++ b/dev-docs/plugins/index.md @@ -33,9 +33,9 @@ A plugin must be a directory containing a Python package with a `misago_plugin.p ``` example-plugin/ - example_plugin/ - __init__.py - misago_plugin.py + example_plugin/ + __init__.py + misago_plugin.py ``` A valid plugin has: @@ -61,6 +61,6 @@ If you are using the [local development setup](https://github.com/rafalp/misago) ## Creating a custom plugin -If you are interested in creating a custom plugin, please see the [plugin tutorial](./plugin-development.md). +If you are interested in creating a custom plugin, please see the [plugin tutorial](./tutorial.md). Once you have your basic plugin up and running, the [extending Misago](./extending-misago.md) document contains a list of all available extension points. diff --git a/dev-docs/plugins/plugin-development.md b/dev-docs/plugins/plugin-development.md deleted file mode 100644 index 235445b773..0000000000 --- a/dev-docs/plugins/plugin-development.md +++ /dev/null @@ -1,3 +0,0 @@ -# Plugin dev tutorial - -TODO \ No newline at end of file diff --git a/dev-docs/plugins/plugin-manifest-reference.md b/dev-docs/plugins/plugin-manifest-reference.md new file mode 100644 index 0000000000..3abc7ca003 --- /dev/null +++ b/dev-docs/plugins/plugin-manifest-reference.md @@ -0,0 +1,90 @@ +# Plugin manifest reference + +A frozen dataclass with plugin's metadata. + + +## Optional arguments + +### `name: str` + +A string with the plugin name. Limited to 100 characters. + + +### `description: str` + +A string with the plugin description. Limited to 250 characters. + + +### `license: str` + +A string with the plugin license. Limited to 50 characters. + + +### `icon: str` + +A string with the plugin icon. Must be a valid Font Awesome icon CSS name, e.g., `fa fa-icon` or `fas fa-other-icon`. + + +### `color: str` + +A string with the plugin icon's color. Must be a color hex format prefixed with `#`, e.g., `#efefef`. + + +### `version: str` + +A string with the plugin version. Limited to 50 characters. + + +### `author: str` + +A string with the plugin author's name. Limited to 150 characters. + + +### `homepage: str` + +A string with the URL to the plugin's homepage. + + +### `sponsor: str` + +A string with the URL to a page with sponsorship instructions or a donation form. + + +### `help: str` + +A string with the URL to the plugin's help page or a support forum. + + +### `bugs: str` + +A string with the URL to the plugin's bug reporting tool. + + +### `repo: str` + +A string with the URL to the plugin's code repository. + + +## Example + +The code below shows a `misago_plugin.py` file with a plugin manifest with all fields filled in: + +```python +from misago import MisagoPlugin + + +manifest = MisagoPlugin( + name="Example plugin with complete manifest", + description="This plugin has all fields in its manifest filled in.", + license="GNU GPL v2", + icon="fa fa-wrench", + color="#9b59b6", + version="0.1DEV", + author="Rafał Pitoń", + homepage="https://misago-project.org", + sponsor="https://github.com/sponsors/rafalp", + help="https://misago-project.org/c/support/30/", + bugs="https://misago-project.org/c/bug-reports/29/", + repo="https://github.com/rafalp/misago", +) +``` \ No newline at end of file diff --git a/dev-docs/plugins/tutorial.md b/dev-docs/plugins/tutorial.md new file mode 100644 index 0000000000..b8f1844ba5 --- /dev/null +++ b/dev-docs/plugins/tutorial.md @@ -0,0 +1,80 @@ +# Creating a custom plugin + +Welcome to the plugin tutorial! In this tutorial, you will implement a "Users Online" plugin that displays a block containing a list of users currently online for site administrators on Misago's categories page. Additionally, this plugin will add a new page displaying all users currently online. + +This exercise will cover all the basics of plugin development: + +- Setting up a development environment for creating a new plugin. +- Getting a practical experience with Django templates, views, URLs, and the ORM. +- Using template outlets to include new HTML on existing pages. +- Adding a new page to the site. + + +## Django basics are required + +Because Misago plugins are Django apps, knowledge of Django basics is necessary for plugin development. + +If you don't know what Django apps are, how to use its ORM, create templates, views, or URLs, please see the ["First steps"](https://docs.djangoproject.com/en/5.0/#first-steps) section of the Django documentation. It provides a gentle and quick introduction to these concepts, which will be essential later. + + +## Misago development environment + +To begin plugin development, clone the [Misago GitHub](https://github.com/rafalp/Misago) repository and run the `./dev init` command in your terminal. This will build the necessary Docker containers, install Python dependencies, and initialize the database. Once the command completes, you can start the development server using the `docker compose up` command. + +Once the development server starts, visit http://127.0.0.1:8000/ in your browser to see your Misago site. You can sign in to the admin account using the `admin` and `password` credentials. + +The `./dev` utility provides more commands than `init`. Run it without any arguments to get the list of all available commands. + + +## Initializing a minimal plugin + +Look at the contents of the `plugins` directory. Misago's main repository comes with a few plugins already pre-installed. These plugins exist mainly to test the plugin system's features, but `minimal-plugin`, `full-manifest-plugin`, and the `misago-pypi-plugin` specified in the `pip-install.txt` file can be used as quick references for plugin developers. + +Stop your development environment if it's running (`ctrl + c` or `cmd + c` in the terminal). Now, let's create a new directory for in `plugins` and name it `misago-users-online-plugin`. Inside of it, create another directory and name `misago_users_online_plugin`. Within this last directory, create two empty Python files: `__init__.py` and `misago_plugin.py`. Final directories structure should look like this: + +``` +misago-users-online-plugin/ + misago_users_online_plugin/ + __init__.py + misago_plugin.py +``` + +This is the file structure of a minimal valid plugin that Misago will discover and load: + +- `misago-users-online-plugin`: a directory containing all the plugin's files. +- `misago_users_online_plugin`: a Python package (and a Django application) that Misago will import. +- `__init__.py`: a file that makes the `misago_users_online_plugin` directory a Python package. +- `misago_plugin.py`: a file that makes the `misago_users_online_plugin` directory a Misago plugin. + +It's important that the final plugin name and its Python package name are so close to each other. After the plugin is released to PyPI, Misago will build its imported Python package name from its PyPI name specified in the `pip-install.txt` file, using the `name.lower().replace("-", "_")` function. For example, the [Misago PyPI Plugin](https://pypi.org/project/misago-pypi-plugin/) is installable from PyPI as `misago-pypi-plugin`, but its Python package is named `misago_pypi_plugin` because that's the name Misago will try to include based on the `pip-install.txt` file contents. + +The `misago-users-online-plugin` directory can contain additional files and directories. For example, it can include a `pyproject.toml` file with plugin's Python package data and dependencies. It can also include `requirements.txt` and `requirements-dev.txt` PIP requirements files. Requirements specified in those files will be installed during the Docker image build time. It is also possible (and recommended) to begin plugin development by creating a repository for it on GitHub or another code hosting platform and then cloning it to the `plugins` directory. + + +## Plugin list in admin control panel + +Misago's admin control panel has a "Plugins" page that displays a list of all installed plugins on your Misago site. To access the admin control panel, start the development server with the `docker compose up` command and visit http://127.0.0.1:8000/admincp/ in your browser. You will see a login page for the Misago admin control panel. Log in using the `admin` username and `password` password. + +After logging in, find the "Plugins" link in the menu on the left and click on it. The list of plugins should include our new plugin as "Misago-Users-Online-Plugin". If it's not there, make sure you've restarted the development server and that the plugin structure is correct. + + +## Adding a plugin manifest + +Misago will display a message next to our plugin that it's missing a manifest in its `misago_plugin.py` file. The plugin manifest is an instance of the `MisagoPlugin` data class populated with the plugin's data. + +Let's update the `misago_plugin.py` file to include a basic manifest for our plugin: + +```python +# misago_users_online_plugin/misago_plugin.py +from misago import MisagoPlugin + + +manifest = MisagoPlugin( + name="Users Online", + description="Displays users online list on the categories page.", +) +``` + +Save the updated file and refresh the admin's plugins page. Our plugin will now be displayed as "Users Online", along with a brief description of its functionality. + +The `MisagoPlugin` class allows plugin authors to specify additional information about their plugins. Refer to the [plugin manifest reference](./plugin-manifest-reference.md) document for a comprehensive list of available fields. \ No newline at end of file diff --git a/generate_dev_docs.py b/generate_dev_docs.py index f141e60536..4b1555c162 100644 --- a/generate_dev_docs.py +++ b/generate_dev_docs.py @@ -6,6 +6,7 @@ from textwrap import dedent, indent HOOKS_MODULES = ("misago.oauth2.hooks",) +PLUGIN_MANIFEST = "misago.plugins.manifest.MisagoPlugin" OUTLETS_ENUM = "misago.plugins.enums.PluginOutlet" BASE_PATH = Path(__file__).parent @@ -15,10 +16,21 @@ def main(): + generate_plugin_manifest_reference() generate_hooks_reference() generate_outlets_reference() +def generate_plugin_manifest_reference(): + manifest_path, manifest_attr = PLUGIN_MANIFEST.rsplit(".", 1) + manifest_type = getattr(import_module(manifest_path), manifest_attr) + + with open(PLUGINS_PATH / "plugin-manifest-reference.md", "w") as fp: + fp.write("# Plugin manifest reference") + fp.write("\n\n") + fp.write(indent_docstring_headers(dedent(manifest_type.__doc__)).strip()) + + def generate_hooks_reference(): hooks_data: dict[str, dict[str, ast.Module]] = {} for hooks_module in HOOKS_MODULES: diff --git a/misago/plugins/manifest.py b/misago/plugins/manifest.py index a0aef570f8..b3591589f3 100644 --- a/misago/plugins/manifest.py +++ b/misago/plugins/manifest.py @@ -4,6 +4,87 @@ @dataclass(frozen=True) class MisagoPlugin: + """ + A frozen dataclass with plugin's metadata. + + # Optional arguments + + ## `name: str` + + A string with the plugin name. Limited to 100 characters. + + ## `description: str` + + A string with the plugin description. Limited to 250 characters. + + ## `license: str` + + A string with the plugin license. Limited to 50 characters. + + ## `icon: str` + + A string with the plugin icon. Must be a valid Font Awesome icon CSS name, + e.g., `fa fa-icon` or `fas fa-other-icon`. + + ## `color: str` + + A string with the plugin icon's color. Must be a color hex format prefixed + with `#`, e.g., `#efefef`. + + ## `version: str` + + A string with the plugin version. Limited to 50 characters. + + ## `author: str` + + A string with the plugin author's name. Limited to 150 characters. + + ## `homepage: str` + + A string with the URL to the plugin's homepage. + + ## `sponsor: str` + + A string with the URL to a page with sponsorship instructions or a donation form. + + ## `help: str` + + A string with the URL to the plugin's help page or a support forum. + + ## `bugs: str` + + A string with the URL to the plugin's bug reporting tool. + + ## `repo: str` + + A string with the URL to the plugin's code repository. + + # Example + + The code below shows a `misago_plugin.py` file with a plugin manifest with all + fields filled in: + + ```python + from misago import MisagoPlugin + + + manifest = MisagoPlugin( + name="Example plugin with complete manifest", + description="This plugin has all fields in its manifest filled in.", + license="GNU GPL v2", + icon="fa fa-wrench", + color="#9b59b6", + version="0.1DEV", + author="Rafał Pitoń", + homepage="https://misago-project.org", + sponsor="https://github.com/sponsors/rafalp", + help="https://misago-project.org/c/support/30/", + bugs="https://misago-project.org/c/bug-reports/29/", + repo="https://github.com/rafalp/misago", + ) + ``` + """ + name: Optional[str] = None description: Optional[str] = None license: Optional[str] = None