Source code for cmsplugin_blocks.forms.feature
import json
from django.conf import settings
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from ..models import Feature
from ..choices_helpers import get_feature_plugin_choices
from ..utils.validators import validate_css_classname
[docs]
class FeatureForm(forms.ModelForm):
"""
Admin form to create or change a Feature object.
"""
plugins = forms.MultipleChoiceField(
choices=get_feature_plugin_choices(),
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = Feature
exclude = []
fields = [
"title",
"value",
"scope",
"plugins",
]
[docs]
class FeatureImportForm(forms.Form):
"""
Admin form to choose Feature import options.
"""
scopes = forms.MultipleChoiceField(
label=_("Scopes to ignore"),
required=False,
choices=Feature.SCOPE_CHOICES,
widget=forms.CheckboxSelectMultiple,
help_text=_(
"Selected scopes will be ignored from loaded JSON items and won't be "
"created as Feature objects."
),
)
json_file = forms.FileField(
required=True,
help_text=_("A valid feature JSON file."),
)
class Meta:
fields = [
"scopes",
"json_file",
]
[docs]
def clean_json_file(self):
"""
Open file to validate it as JSON and return its content.
.. Todo::
This could have been done more robustly with Python library "schema".
"""
data = self.cleaned_data["json_file"]
# Check if a valid JSON
try:
payload = json.load(data.open())
except json.JSONDecodeError as e:
raise ValidationError(_("File is not valid JSON: {}").format(str(e)))
else:
# Root type must be a JSON dictionnary
if not isinstance(payload, dict):
raise ValidationError(
_("JSON should be a dictionnary not a: {}").format(
type(payload).__name__
)
)
# Item 'items' is required
if "items" not in payload:
raise ValidationError(
_("JSON is missing 'items' item for the feature data")
)
# Item 'items' must be a list
if not isinstance(payload["items"], list):
raise ValidationError(_("Item 'items' must be a list"))
# Build registry of existing titles
existing_titles = {k: [] for k, v in Feature.SCOPE_CHOICES}
# Check for some errors from items
error_lines = []
for i, feature in enumerate(payload["items"], start=1):
# Get missing field or empty values from loaded items
if (
not feature.get("title", "") or not feature.get("value", "") or
not feature.get("scope", "") or not feature.get("plugins", "")
):
msg = _("#{} is missing one or more required items")
error_lines.append(msg.format(i))
# Check we have a knowed scope
elif feature["scope"] not in [k for k, v in Feature.SCOPE_CHOICES]:
msg = _("#{} define a scope choice that is not enabled")
error_lines.append(msg.format(i))
# Check we have only well known plugin names
elif len([
item
for item in feature["plugins"]
if item not in settings.BLOCKS_KNOWED_FEATURES_PLUGINS
]) > 0:
msg = _("#{} define a plugin name that is not enabled")
error_lines.append(msg.format(i))
# Check for duplicate title per scope
elif feature["title"] in existing_titles[feature["scope"]]:
msg = _("#{} define a title that already exists")
error_lines.append(msg.format(i))
# Almost everything seems ok, finally use value validator before
# storing item
else:
try:
validate_css_classname(feature["value"])
except ValidationError:
msg = _("#{} has invalid CSS classname(s)")
error_lines.append(msg.format(i))
# Everything is ok, store title as an existing one for its scope
else:
existing_titles[feature["scope"]].append(feature["title"])
# Raise error in case of any error messages
if error_lines:
raise ValidationError(
[_("Some dump items are invalid:")] + error_lines
)
return payload
[docs]
def save(self, commit=True):
"""
Save elligible items from loaded JSON as feature objects.
Arguments:
commit (boolean): Only save objects if True.
Returns:
dict: A dictionnary with items for effectively created and ignored items
from loaded JSON.
"""
created = []
duplicates = []
ignored = []
# Fetch existing items
existing = Feature.objects.all().query_full_payload()
existing_titles = {}
for k, v in Feature.SCOPE_CHOICES:
existing_titles[k] = [
item["title"]
for item in existing
if item["scope"] == k
]
# Create features from loaded data if they do not already exists (based on
# their title)
for feature_data in self.cleaned_data["json_file"]["items"]:
if feature_data["title"] not in existing_titles[feature_data["scope"]]:
if feature_data["scope"] in self.cleaned_data["scopes"]:
ignored.append(feature_data)
else:
new_item = Feature(**feature_data)
new_item.full_clean()
if commit:
new_item.save()
created.append(feature_data)
else:
duplicates.append(feature_data)
return {
"disallowed_scopes": self.cleaned_data["scopes"],
"created": created,
"duplicates": duplicates,
"ignored": ignored,
}