Skip to content

Commit

Permalink
Merge pull request #4576 from korycins/dynamic_settings_for_plugins
Browse files Browse the repository at this point in the history
Add dynamic settings api for plugins
  • Loading branch information
maarcingebala committed Aug 8, 2019
2 parents e96b57d + 9fbb411 commit f7c8814
Show file tree
Hide file tree
Showing 26 changed files with 1,299 additions and 107 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Disabled unneeded reports from uWSGI about broken pipe and write errors from disconnected clients. Preventing from spamming sentry users. - #4596 by @NyanKiyoshi
- Upgraded to django 2.2.4 - #4603 by @NyanKiyoshi
- Invalid IP address in HTTP requests now fallback to the requester's IP address. - #4597 by @NyanKiyoshi
- Add queries and mutation for serving and saving the configuration of all plugins - #4576 by @korycins
- Refactor account mutations - #4510 by @fowczarek
- Users cannot add multiple times the same product into a collection anymore - #4518 by @NyanKiyoshi
- Enterprise-grade attributes management - #4351 by @dominik-zeglen and @NyanKiyoshi
Expand Down
1 change: 1 addition & 0 deletions saleor/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"account.impersonate_users",
"discount.manage_discounts",
"giftcard.manage_gift_card",
"extensions.manage_plugins",
"menu.manage_menus",
"order.manage_orders",
"page.manage_pages",
Expand Down
14 changes: 14 additions & 0 deletions saleor/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import importlib
from typing import List

from django.utils.translation import pgettext_lazy

from .checks import check_extensions # NOQA: F401


Expand All @@ -17,3 +19,15 @@ def discover_plugins_modules(plugins: List[str]):
module = importlib.import_module(module_path)
plugins_modules.append(module.__package__)
return plugins_modules


class ConfigurationTypeField:
STRING = "String"
BOOLEAN = "Boolean"
CHOICES = [
(STRING, pgettext_lazy("Type of the configuration field", "Field is a String")),
(
BOOLEAN,
pgettext_lazy("Type of the configuration field", "Field is a Boolean"),
),
]
70 changes: 70 additions & 0 deletions saleor/extensions/base_plugin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Any, List, Union

from django.db.models import QuerySet
from django_countries.fields import Country
from prices import Money, MoneyRange, TaxedMoney, TaxedMoneyRange

from .models import PluginConfiguration

if TYPE_CHECKING:
from ..core.taxes import TaxType
from ..checkout.models import Checkout, CheckoutLine
Expand All @@ -20,6 +23,24 @@ class BasePlugin:
If the plugin is first, it will use default value calculated by the manager.
"""

PLUGIN_NAME = ""
CONFIG_STRUCTURE = None

def __init__(self, *args, **kwargs):
self._cached_config = None
self.active = None

def __str__(self):
return self.PLUGIN_NAME

def _initialize_plugin_configuration(self):
plugin_config_qs = PluginConfiguration.objects.filter(name=self.PLUGIN_NAME)
plugin_config = self._cached_config or plugin_config_qs.first()

if plugin_config:
self._cached_config = plugin_config
self.active = plugin_config.active

def calculate_checkout_total(
self,
checkout: "Checkout",
Expand Down Expand Up @@ -114,3 +135,52 @@ def get_tax_rate_percentage_value(
self, obj: Union["Product", "ProductType"], country: Country, previous_value
) -> Decimal:
return NotImplemented

@classmethod
def _update_config_items(
cls, configuration_to_update: List[dict], current_config: List[dict]
):
for config_item in current_config:
for config_item_to_update in configuration_to_update:
if config_item["name"] == config_item_to_update.get("name"):
new_value = config_item_to_update.get("value")
config_item.update([("value", new_value)])

@classmethod
def save_plugin_configuration(
cls, plugin_configuration: "PluginConfiguration", cleaned_data
):
current_config = plugin_configuration.configuration
configuration_to_update = cleaned_data.get("configuration")
if configuration_to_update:
cls._update_config_items(configuration_to_update, current_config)
if "active" in cleaned_data:
plugin_configuration.active = cleaned_data["active"]
plugin_configuration.save()
return plugin_configuration

@classmethod
def _get_default_configuration(cls):
defaults = None
return defaults

@classmethod
def _append_config_structure(cls, configuration):
config_structure = getattr(cls, "CONFIG_STRUCTURE", {})
for coniguration_field in configuration:

structure_to_add = config_structure.get(coniguration_field.get("name"))
if structure_to_add:
coniguration_field.update(structure_to_add)
return config_structure

@classmethod
def get_plugin_configuration(cls, queryset: QuerySet) -> "PluginConfiguration":
defaults = cls._get_default_configuration()
configuration = queryset.get_or_create(name=cls.PLUGIN_NAME, defaults=defaults)[
0
]
if configuration.configuration:
# Let's add a translated descriptions and labels
cls._append_config_structure(configuration.configuration)
return configuration
17 changes: 15 additions & 2 deletions saleor/extensions/checks.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import List
from typing import TYPE_CHECKING, List

from django.conf import settings
from django.core.checks import Error, register
from django.utils.module_loading import import_string

if TYPE_CHECKING:
from .base_plugin import BasePlugin


@register()
def check_extensions(app_configs, **kwargs):
Expand Down Expand Up @@ -39,6 +42,16 @@ def check_single_plugin(plugin_path: str, errors: List[Error]):
errors.append(Error("Wrong plugin_path %s" % plugin_path))
return
try:
import_string(plugin_path)
plugin_class = import_string(plugin_path)
except ImportError:
errors.append(Error("Plugin with path: %s doesn't exist" % plugin_path))

if not errors:
check_plugin_name(plugin_class, errors)


def check_plugin_name(plugin_class: "BasePlugin", errors: List[Error]):
if not getattr(plugin_class, "PLUGIN_NAME", None):
errors.append(
Error("Missing field PLUGIN_NAME for plugin - %s" % plugin_class.__name__)
)
32 changes: 30 additions & 2 deletions saleor/extensions/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from prices import Money, MoneyRange, TaxedMoney, TaxedMoneyRange

from ..core.taxes import TaxType, quantize_price
from .models import PluginConfiguration

if TYPE_CHECKING:
from .base_plugin import BasePlugin
Expand Down Expand Up @@ -202,10 +203,37 @@ def get_tax_rate_percentage_value(
"get_tax_rate_percentage_value", default_value, obj, country
).quantize(Decimal("1."))

def save_plugin_configuration(self, plugin_name, cleaned_data: dict):
plugin_configuration = PluginConfiguration.objects.get(name=plugin_name)
for plugin in self.plugins:
if plugin.PLUGIN_NAME == plugin_name:
return plugin.save_plugin_configuration(
plugin_configuration, cleaned_data
)

def get_plugin_configuration(self, plugin_name) -> "PluginConfiguration":
plugin_configurations_qs = PluginConfiguration.objects.all()
for plugin in self.plugins:
if plugin.PLUGIN_NAME == plugin_name:
return plugin.get_plugin_configuration(plugin_configurations_qs)

def get_plugin_configurations(self) -> List["PluginConfiguration"]:
plugin_configurations = []
plugin_configurations_qs = PluginConfiguration.objects.all()
for plugin in self.plugins:
plugin_configuration = plugin.get_plugin_configuration(
plugin_configurations_qs
)
plugin_configurations.append(plugin_configuration)
return plugin_configurations


def get_extensions_manager(
manager_path: str = settings.EXTENSIONS_MANAGER,
plugins: List[str] = settings.PLUGINS,
manager_path: str = None, plugins: List[str] = None
) -> ExtensionsManager:
if not manager_path:
manager_path = settings.EXTENSIONS_MANAGER
if plugins is None:
plugins = settings.PLUGINS
manager = import_string(manager_path)
return manager(plugins)
43 changes: 43 additions & 0 deletions saleor/extensions/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 2.2.3 on 2019-08-07 10:11

import django.contrib.postgres.fields.jsonb
from django.db import migrations, models

import saleor.core.utils.json_serializer


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="PluginConfiguration",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=128, unique=True)),
("description", models.TextField(blank=True)),
("active", models.BooleanField(default=True)),
(
"configuration",
django.contrib.postgres.fields.jsonb.JSONField(
blank=True,
default=dict,
encoder=saleor.core.utils.json_serializer.CustomJsonEncoder,
null=True,
),
),
],
options={"permissions": (("manage_plugins", "Manage plugins"),)},
)
]
Empty file.
22 changes: 22 additions & 0 deletions saleor/extensions/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils.translation import pgettext_lazy

from saleor.core.utils.json_serializer import CustomJsonEncoder


class PluginConfiguration(models.Model):
name = models.CharField(max_length=128, unique=True)
description = models.TextField(blank=True)
active = models.BooleanField(default=True)
configuration = JSONField(
blank=True, null=True, default=dict, encoder=CustomJsonEncoder
)

class Meta:
permissions = (
("manage_plugins", pgettext_lazy("Plugin description", "Manage plugins")),
)

def __str__(self):
return f"Configuration of {self.name}, active: {self.active}"

0 comments on commit f7c8814

Please sign in to comment.