Skip to content

Commit

Permalink
Merge pull request saleor#8959 from saleor/add-new-app-extensions
Browse files Browse the repository at this point in the history
Add new type of App's extensions' targets
  • Loading branch information
Maciej Korycinski authored Feb 2, 2022
2 parents dbc17c8 + 742eb6c commit 32ea131
Show file tree
Hide file tree
Showing 24 changed files with 487 additions and 419 deletions.
47 changes: 35 additions & 12 deletions saleor/app/app_manifest_sample.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
{
"id": "saleor.app.sample",
"version": "1.0.0",
"name": "My Wonderful App",
"about": "My Wonderful App is a wonderful App for Saleor.",
"permissions": ["MANAGE_USERS", "MANAGE_STAFF"],
"appUrl": "http://localhost:3000/app",
"configurationUrl": "htpp://localhost:3000/configuration",
"tokenTargetUrl": "http://localhost:3000/register",
"dataPrivacy": "Lorem ipsum",
"dataPrivacyUrl": "http://localhost:3000/app-data-privacy",
"homepageUrl": "http://localhost:3000/homepage",
"supportUrl": "http://localhost:3000/support"
"name": "Saleor App",
"version": "1.0.0",
"about": "",
"dataPrivacy": "",
"dataPrivacyUrl": "",
"homepageUrl": "https://example.com/homepage",
"supportUrl": "https://example.com/support",
"id": "sample-app",
"permissions": [
"MANAGE_PRODUCTS",
"MANAGE_ORDERS"
],
"appUrl": "https://example.com/app",
"extensions": [
{
"label": "Create with Sample app",
"mount": "PRODUCT_OVERVIEW_CREATE",
"target": "POPUP",
"permissions": [
"MANAGE_PRODUCTS"
],
"url": "https://example.com/extension/"
},
{
"label": "Create with App and redirect",
"mount": "PRODUCT_OVERVIEW_MORE_ACTIONS",
"target": "APP_PAGE",
"permissions": [
"MANAGE_PRODUCTS"
],
"url": "/extension/redirect"
}
],
"configurationUrl": "https://example.com/configuration/",
"tokenTargetUrl": "https://example.com/configuration/install"
}
7 changes: 3 additions & 4 deletions saleor/app/installation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..core.permissions import get_permission_names
from .manifest_validations import clean_manifest_data
from .models import App, AppExtension, AppInstallation
from .types import AppType
from .types import AppExtensionTarget, AppType

REQUEST_TIMEOUT = 25

Expand Down Expand Up @@ -57,9 +57,8 @@ def install_app(
app=app,
label=extension_data.get("label"),
url=extension_data.get("url"),
view=extension_data.get("view"),
type=extension_data.get("type"),
target=extension_data.get("target"),
mount=extension_data.get("mount"),
target=extension_data.get("target", AppExtensionTarget.POPUP),
)
extension.permissions.set(extension_data.get("permissions", []))

Expand Down
105 changes: 53 additions & 52 deletions saleor/app/manifest_validations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from collections import defaultdict
from typing import Dict, Iterable, List

Expand All @@ -12,33 +13,56 @@
split_permission_codename,
)
from .error_codes import AppErrorCode
from .types import AppExtensionTarget, AppExtensionType, AppExtensionView
from .types import AppExtensionMount, AppExtensionTarget
from .validators import AppURLValidator

T_ERRORS = Dict[str, List[ValidationError]]

logger = logging.getLogger(__name__)

AVAILABLE_APP_EXTENSION_CONFIGS = {
AppExtensionType.DETAILS: {
AppExtensionView.PRODUCT: [
AppExtensionTarget.CREATE,
AppExtensionTarget.MORE_ACTIONS,
]
},
AppExtensionType.OVERVIEW: {
AppExtensionView.PRODUCT: [
AppExtensionTarget.CREATE,
AppExtensionTarget.MORE_ACTIONS,
]
},
}
T_ERRORS = Dict[str, List[ValidationError]]


def _clean_app_url(url):
url_validator = AppURLValidator()
url_validator(url)


def _clean_extension_url_with_only_path(
manifest_data: dict, target: str, extension_url: str
):
if target == AppExtensionTarget.APP_PAGE:
return
elif manifest_data["appUrl"]:
_clean_app_url(manifest_data["appUrl"])
else:
msg = (
"Incorrect relation between extension's target and URL fields. "
"APP_PAGE can be used only with relative URL path."
)
logger.warning(msg, extra={"target": target, "url": extension_url})
raise ValidationError(msg)


def clean_extension_url(extension: dict, manifest_data: dict):
"""Clean assigned extension url.
Make sure that format of url is correct based on the rest of manifest fields.
- url can start with '/' when one of these conditions is true:
a) extension.target == APP_PAGE
b) appUrl is provided
- url cannot start with protocol when target == "APP_PAGE"
"""
extension_url = extension["url"]
target = extension.get("target") or AppExtensionTarget.POPUP
if extension_url.startswith("/"):
_clean_extension_url_with_only_path(manifest_data, target, extension_url)
elif target == AppExtensionTarget.APP_PAGE:
msg = "Url cannot start with protocol when target == APP_PAGE"
logger.warning(msg)
raise ValidationError(msg)
else:
_clean_app_url(extension_url)


def clean_manifest_url(manifest_url):
try:
_clean_app_url(manifest_url)
Expand Down Expand Up @@ -121,57 +145,36 @@ def _clean_extension_permissions(extension, app_permissions, errors):
extension["permissions"] = extension_permissions


def _validate_configuration(extension, errors):

available_config_for_type = AVAILABLE_APP_EXTENSION_CONFIGS.get(
extension["type"], {}
)
available_config_for_type_and_view = available_config_for_type.get(
extension["view"], []
)

if extension["target"] not in available_config_for_type_and_view:
msg = (
"Incorrect configuration of app extension for fields: view, type and "
"target."
)
def clean_extension_enum_field(enum, field_name, extension, errors):
if extension[field_name] in [code.upper() for code, _ in enum.CHOICES]:
extension[field_name] = getattr(enum, extension[field_name])
else:
errors["extensions"].append(
ValidationError(
msg,
f"Incorrect value for field: {field_name}",
code=AppErrorCode.INVALID.value,
)
)


def clean_extensions(manifest_data, app_permissions, errors):
extensions = manifest_data.get("extensions", [])
enum_map = [
(AppExtensionView, "view"),
(AppExtensionType, "type"),
(AppExtensionTarget, "target"),
]
for extension in extensions:
for extension_enum, key in enum_map:
if extension[key] in [code.upper() for code, _ in extension_enum.CHOICES]:
extension[key] = getattr(extension_enum, extension[key])
else:
errors["extensions"].append(
ValidationError(
f"Incorrect value for field: {key}",
code=AppErrorCode.INVALID.value,
)
)
if "target" not in extension:
extension["target"] = AppExtensionTarget.POPUP
else:
clean_extension_enum_field(AppExtensionTarget, "target", extension, errors)
clean_extension_enum_field(AppExtensionMount, "mount", extension, errors)

try:
_clean_app_url(extension["url"])
clean_extension_url(extension, manifest_data)
except (ValidationError, AttributeError):
errors["extensions"].append(
ValidationError(
"Incorrect value for field: url.",
code=AppErrorCode.INVALID_URL_FORMAT.value,
)
)
_validate_configuration(extension, errors)
_clean_extension_permissions(extension, app_permissions, errors)


Expand All @@ -180,9 +183,7 @@ def validate_required_fields(manifest_data, errors):
extension_required_fields = {
"label",
"url",
"view",
"type",
"target",
"mount",
}
manifest_missing_fields = manifest_required_fields.difference(manifest_data)
if manifest_missing_fields:
Expand Down
49 changes: 49 additions & 0 deletions saleor/app/migrations/0006_convert_extension_enums_into_one.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 3.2.6 on 2022-01-27 08:20

from django.db import migrations, models


def migrate_enum_values_to_single_enum(apps, schema_editor):
AppExtension = apps.get_model("app", "AppExtension")
app_extensions = AppExtension.objects.all()
for extension in app_extensions:
if extension.type == "overview":
if extension.target == "more_actions":
extension.mount = "product_overview_more_actions"
else:
extension.mount = "product_overview_create"
else:
extension.mount = "product_details_more_actions"
# simple save as there aren't a lot of app extensions.
extension.save()


class Migration(migrations.Migration):

dependencies = [
("app", "0005_appextension"),
]
operations = [
migrations.AddField(
model_name="appextension",
name="mount",
field=models.CharField(
choices=[
("product_overview_create", "product_overview_create"),
("product_overview_more_actions", "product_overview_more_actions"),
("product_details_more_actions", "product_details_more_actions"),
("navigation_catalog", "navigation_catalog"),
("navigation_orders", "navigation_orders"),
("navigation_customers", "navigation_customers"),
("navigation_discounts", "navigation_discounts"),
("navigation_translations", "navigation_translations"),
("navigation_pages", "navigation_pages"),
],
max_length=256,
null=True,
),
),
migrations.RunPython(
migrate_enum_values_to_single_enum, migrations.RunPython.noop
),
]
43 changes: 43 additions & 0 deletions saleor/app/migrations/0007_auto_20220127_0942.py

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

22 changes: 22 additions & 0 deletions saleor/app/migrations/0008_appextension_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.2.6 on 2022-01-27 09:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("app", "0007_auto_20220127_0942"),
]

operations = [
migrations.AddField(
model_name="appextension",
name="target",
field=models.CharField(
choices=[("popup", "popup"), ("app_page", "app_page")],
default="popup",
max_length=128,
),
),
]
11 changes: 7 additions & 4 deletions saleor/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ..core.models import Job, ModelWithMetadata
from ..core.permissions import AppPermission
from ..webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType
from .types import AppExtensionTarget, AppExtensionType, AppExtensionView, AppType
from .types import AppExtensionMount, AppExtensionTarget, AppType


class AppQueryset(models.QuerySet):
Expand Down Expand Up @@ -109,9 +109,12 @@ class AppExtension(models.Model):
app = models.ForeignKey(App, on_delete=models.CASCADE, related_name="extensions")
label = models.CharField(max_length=256)
url = models.URLField()
view = models.CharField(choices=AppExtensionView.CHOICES, max_length=128)
type = models.CharField(choices=AppExtensionType.CHOICES, max_length=128)
target = models.CharField(choices=AppExtensionTarget.CHOICES, max_length=128)
mount = models.CharField(choices=AppExtensionMount.CHOICES, max_length=256)
target = models.CharField(
choices=AppExtensionTarget.CHOICES,
max_length=128,
default=AppExtensionTarget.POPUP,
)
permissions = models.ManyToManyField(
Permission,
blank=True,
Expand Down
5 changes: 3 additions & 2 deletions saleor/app/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def install_app_task(job_id, activate=False):
app_installation.message = (
"Failed to connect to app. Try later or contact with app support."
)
except Exception:
app_installation.message = "Unknow error. Contact with app support."
except Exception as e:
logger.warning("Failed to install app. error %s", e)
app_installation.message = "Unknown error. Contact with app support."
app_installation.status = JobStatus.FAILED
app_installation.save()
2 changes: 1 addition & 1 deletion saleor/app/tests/test_app_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ def test_install_app_task_undefined_error(monkeypatch, app_installation):
install_app_task(app_installation.pk)
app_installation.refresh_from_db()
assert app_installation.status == JobStatus.FAILED
assert app_installation.message == "Unknow error. Contact with app support."
assert app_installation.message == "Unknown error. Contact with app support."
Loading

0 comments on commit 32ea131

Please sign in to comment.