Skip to content

Commit

Permalink
Plugins page in admin (#1673)
Browse files Browse the repository at this point in the history
* Sort plugins alphabetically by name

* Load plugin manifests + some tests

* Commit new work on plugins

* Test plugins meta loader

* WIP plugins list in admin

* Add tests for admin plugins list

* Fix build

* Fix plugins not being found on CI

* Test empty plugins list

* Tweak font awesome icon regex

* Add tests

* Update admin plugins list

* Tweak spacing

* Fix tests

* Tweak test

* Tweak plugin docs

* Small doc tweak
  • Loading branch information
rafalp authored Dec 5, 2023
1 parent f4c6373 commit fa14a97
Show file tree
Hide file tree
Showing 30 changed files with 892 additions and 40 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
POSTGRES_USER: misago
POSTGRES_PASSWORD: misago
POSTGRES_HOST: localhost
MISAGO_PLUGINS: plugins
run: |
pytest --cov=misago
- name: Linters
Expand Down
22 changes: 11 additions & 11 deletions dev-docs/plugins/index.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Plugin guide

Misago implements a plugin system that enables developers to customize and extend the core functionality of the software. This plugin system itself extends existing Django applications mechanism.
Misago implements a plugin system that enables customization and extension of the core functionality of the software.

> **Plugins vs forking Misago**
>
> It may seem simpler (and faster) to fork and change Misago directly instead of using a plugins. While this is possible, the time required to later keep the fork in sync with new versions of Misago for every site update may quickly add up, resulting in a net loss of time.
>
> It is recommended to attempt achieving as much as possible through plugins. In situations where this is not feasible, consider [reaching out to the developers](https://misago-project.org/c/development/31/) before resorting to forking. Misago's current extension points list is not complete, and new ones may be added in future releases based on user feedback.

## Plugin installation

To install a plugin, place its directory in the standard `plugins` directory within your Misago setup.

Example plugin directory must contain a valid Python package with a `misago_plugin.py` file.
Example plugin must be a directory containing a valid Python package with a `misago_plugin.py` file.

This graph shows the file structure of a minimal valid plugin:

Expand All @@ -20,10 +26,10 @@ This graph shows the file structure of a minimal valid plugin:

Minimal plugin has:

- `minimal-plugin`: a directory that contains all plugin files.
- `minimal-plugin`: a directory that contains all the plugin's files.
- `minimal_plugin`: a Python package (and a Django application) that Misago will import.
- `__init__.py`: a file that marks `minimal_plugin` directory as a Python package.
- `misago_plugin.py`: a file that marks `minimal_plugin` directory as a Misago plugin.
- `__init__.py`: a file that makes `minimal_plugin` directory a Python package.
- `misago_plugin.py`: a file that makes `minimal_plugin` directory a Misago plugin.

The `minimal-plugin` and `minimal_plugin` directories can contain additional files and directories. The `minimal-plugin` directory may include a `pyproject.toml` or `requirements.txt` file to define the plugin's dependencies. It could also include a hidden `.git` directory if plugin was cloned from a Git repository.

Expand All @@ -32,12 +38,6 @@ Plugins following the above file structure are discovered and installed automati

## Writing custom plugin

> **Plugins vs forking Misago**
>
> It may seem simpler (and faster) to fork and customize Misago directly instead of developing plugins. While this is possible, the time required to later keep the fork in sync with new versions of Misago for every site update may quickly add up, resulting in a net loss of time.
>
> It is recommended to attempt achieving as much as possible through plugins. In situations where this is not feasible, consider [reaching out to the developers](https://misago-project.org/c/development/31/) before resorting to forking. Misago's current extension points are not exhaustive, and new ones may be added in future releases based on user feedback.
- django applications mechanism
- plugin structure

Expand Down
19 changes: 19 additions & 0 deletions misago-admin/src/style/admin-media-icon.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Admin media icon
//
// Used in content tables to represent an item

.media-admin-icon {
@extend .d-flex;
@extend .align-items-center;
@extend .justify-content-center;
@extend .rounded;

width: $admin-media-icon-size;
height: $admin-media-icon-size;

font-size: $font-size-base;
line-height: $font-size-base;

color: $admin-media-icon-color;
background: $admin-media-icon-bg;
}
7 changes: 6 additions & 1 deletion misago-admin/src/style/admin-table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
@extend .rounded-lg;
}

// Make table larger (used for tables with longer text content), eg.: plugins
.table-lg td {
@extend .py-2;
}

// Support images buttons
.card-admin-table .btn-thumbnail {
@extend .btn-light;
Expand Down Expand Up @@ -124,7 +129,7 @@
text-decoration: none;
}

// Center and space verticallt the blankslate message
// Center and space vertically the blankslate message
.card-admin-table .blankslate td {
@extend .text-center;
@extend .py-3;
Expand Down
1 change: 1 addition & 0 deletions misago-admin/src/style/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@
@import "admin-table";
@import "admin-form";
@import "admin-error";
@import "admin-media-icon";
9 changes: 9 additions & 0 deletions misago-admin/src/style/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,12 @@ $card-cap-bg: transparent !default;
// Fix colors after overriding

$input-btn-focus-color: rgba($blue, .15) !default;


// Admin Media icon
//
// Colors and sizing
$admin-media-icon-size: 40px;

$admin-media-icon-color: $gray-600 !default;
$admin-media-icon-bg: $gray-200 !default;
10 changes: 9 additions & 1 deletion misago/admin/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Any, Dict, Optional

from django.http import HttpRequest
from django.urls import reverse, NoReverseMatch
from django.utils.translation import get_language
from django.shortcuts import render as dj_render
Expand All @@ -20,7 +23,12 @@ def get_protected_namespace(request):
pass


def render(request, template, context=None, error_page=False):
def render(
request: HttpRequest,
template: str,
context: Optional[Dict[str, Any]] = None,
error_page: bool = False,
):
context = context or {}

navigation = site.visible_branches(request)
Expand Down
2 changes: 1 addition & 1 deletion misago/conf/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from django.urls import path
from django.utils.translation import pgettext_lazy

from .views import index
from .views import (
AnalyticsSettingsView,
CaptchaSettingsView,
Expand All @@ -10,6 +9,7 @@
OAuth2SettingsView,
ThreadsSettingsView,
UsersSettingsView,
index,
)


Expand Down
19 changes: 19 additions & 0 deletions misago/plugins/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.urls import path
from django.utils.translation import pgettext_lazy

from .views import plugins_list


class MisagoAdminExtension:
def register_urlpatterns(self, urlpatterns):
urlpatterns.namespace("plugins/", "plugins")

urlpatterns.patterns("plugins", path("", plugins_list, name="index"))

def register_navigation_nodes(self, site):
site.add_node(
name=pgettext_lazy("admin node", "Plugins"),
icon="fa fa-cube",
after="settings:index",
namespace="plugins",
)
File renamed without changes.
33 changes: 33 additions & 0 deletions misago/plugins/admin/tests/test_plugins_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.urls import reverse

from ....test import assert_contains


def test_plugins_list_contains_plugins(admin_client):
response = admin_client.get(reverse("misago:admin:plugins:index"))

assert_contains(response, "Example plugin")
assert_contains(response, "empty_manifest_plugin")
assert_contains(response, "invalid_manifest_plugin")
assert_contains(response, "minimal_plugin")


def test_plugins_list_contains_plugins_directories_and_packages(admin_client):
response = admin_client.get(reverse("misago:admin:plugins:index"))

assert_contains(response, "empty-manifest-plugin")
assert_contains(response, "empty_manifest_plugin")
assert_contains(response, "full-manifest-plugin")
assert_contains(response, "full_manifest_plugin")
assert_contains(response, "invalid-manifest-plugin")
assert_contains(response, "invalid_manifest_plugin")
assert_contains(response, "minimal-plugin")
assert_contains(response, "minimal_plugin")


def test_plugins_list_contains_plugins_metadata(admin_client):
response = admin_client.get(reverse("misago:admin:plugins:index"))

assert_contains(response, "Rafał Pitoń")
assert_contains(response, "0.1DEV")
assert_contains(response, "GNU GPL v2")
10 changes: 10 additions & 0 deletions misago/plugins/admin/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from ...admin.views import render
from ..metadata import plugins_metadata


def plugins_list(request):
return render(
request,
"misago/admin/plugins/list.html",
{"plugins": plugins_metadata.get_metadata().values()},
)
27 changes: 20 additions & 7 deletions misago/plugins/discover.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys
from pathlib import Path
from typing import List
from typing import Dict, List


def discover_plugins(plugins_path: str | None) -> List[str]:
Expand All @@ -16,16 +16,29 @@ def discover_plugins(plugins_path: str | None) -> List[str]:

def discover_plugins_in_directory(plugins_path: Path) -> List[str]:
plugins_apps: List[str] = []
plugins_paths: Dict[str, str] = {}

for plugin_path in plugins_path.glob("*/*/misago_plugin.py"):
# First step: glob plugin Python packages
for plugin_path in sorted(plugins_path.glob("*/*/misago_plugin.py")):
plugin_package = plugin_path.parent

# Add plugin to Python path so its importable
plugin_dir = str(plugin_package.parent)
if plugin_dir not in sys.path:
sys.path.append(plugin_dir)
# Skip plugins that are not valid Python packages
plugin_package_init = plugin_package / "__init__.py"
if not plugin_package_init.is_file():
continue

# Add plugin to apps to make Django include it
# Add plugin package name to Django apps for later import
plugins_apps.append(plugin_package.name)

# Store plugin path for later adding to sys.path
plugins_paths[plugin_package.name] = str(plugin_package.parent)

plugins_apps = sorted(plugins_apps)

# Add unique plugins paths to Python path ordered by app name
for plugin_app in plugins_apps:
plugin_path = plugins_paths[plugin_app]
if plugin_path not in sys.path:
sys.path.append(plugin_path)

return plugins_apps
5 changes: 4 additions & 1 deletion misago/plugins/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

@dataclass(frozen=True)
class MisagoPlugin:
name: str
name: Optional[str] = None
description: Optional[str] = None
license: Optional[str] = None
icon: Optional[str] = None
color: Optional[str] = None
version: Optional[str] = None
author: Optional[str] = None
homepage: Optional[str] = None
sponsor: Optional[str] = None
help: Optional[str] = None
bugs: Optional[str] = None
repo: Optional[str] = None
Loading

0 comments on commit fa14a97

Please sign in to comment.