--- a/src/pyams_content/shared/view/__init__.py Tue Jun 27 11:47:34 2017 +0200
+++ b/src/pyams_content/shared/view/__init__.py Tue Jun 27 11:49:01 2017 +0200
@@ -10,22 +10,38 @@
# FOR A PARTICULAR PURPOSE.
#
-
__docformat__ = 'restructuredtext'
# import standard library
+import logging
+logger = logging.getLogger("PyAMS (content)")
# import interfaces
-from pyams_content.component.links.interfaces import ILinkContainerTarget
-from pyams_content.shared.view.interfaces import IWfView, IView, VIEW_CONTENT_TYPE, VIEW_CONTENT_NAME
+from hypatia.interfaces import ICatalog
+from pyams_content.shared.view.interfaces import IView, IWfView, IViewQuery, IViewQueryParamsExtension, \
+ IViewQueryFilterExtension, VIEW_CONTENT_TYPE, VIEW_CONTENT_NAME
+from zope.intid.interfaces import IIntIds
# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Any
+from pyams_cache.beaker import get_cache
+from pyams_catalog.query import CatalogResultSet, or_
from pyams_content.shared.common import WfSharedContent, register_content_type, SharedContent
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.list import unique
+from pyams_utils.registry import get_utility
+from pyams_workflow.interfaces import IWorkflow
+from pyramid.threadlocal import get_current_registry
from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty
+VIEWS_CACHE_NAME = 'PyAMS::view'
+VIEWS_CACHE_KEY = 'view_{view}.context_{context}'
+
+
@implementer(IWfView)
class WfView(WfSharedContent):
"""Base view"""
@@ -35,7 +51,27 @@
selected_content_types = FieldProperty(IWfView['selected_content_types'])
order_by = FieldProperty(IWfView['order_by'])
- reversed_order =FieldProperty(IWfView['reversed_order'])
+ reversed_order = FieldProperty(IWfView['reversed_order'])
+
+ def get_results(self, context, limit=None):
+ intids = get_utility(IIntIds)
+ views_cache = get_cache('default', VIEWS_CACHE_NAME)
+ cache_key = VIEWS_CACHE_KEY.format(view=intids.queryId(self),
+ context=intids.queryId(context))
+ try:
+ results = views_cache.get_value(cache_key)
+ except KeyError:
+ registry = get_current_registry()
+ adapter = registry.queryAdapter(self, IViewQuery, name='es')
+ if adapter is None:
+ adapter = registry.getAdapter(self, IViewQuery)
+ results = adapter.get_results(context)
+ views_cache.set_value(cache_key, [intids.queryId(item) for item in results])
+ logger.debug("Storing view items to cache key {0}".format(cache_key))
+ else:
+ results = CatalogResultSet(results)
+ logger.debug("Retrieving view items from cache key {0}".format(cache_key))
+ return results
register_content_type(WfView)
@@ -45,3 +81,38 @@
"""Workflow managed view class"""
content_class = WfView
+
+
+@adapter_config(context=IWfView, provides=IViewQuery)
+class ViewQuery(ContextAdapter):
+ """View query"""
+
+ def get_es_params(self, context):
+ view = self.context
+ catalog = get_utility(ICatalog)
+ registry = get_current_registry()
+ params = Any(catalog['content_type'], view.selected_content_types)
+ wf_params = None
+ for workflow in registry.getAllUtilitiesRegisteredFor(IWorkflow):
+ wf_params = or_(wf_params, Any(catalog['workflow_state'], workflow.published_states))
+ params &= wf_params
+ for name, adapter in sorted(registry.getAdapters((view,), IViewQueryParamsExtension),
+ key=lambda x: x[1].weight):
+ new_params = adapter.get_params(context)
+ if new_params:
+ params &= new_params
+ return params
+
+ def get_results(self, context, limit=None):
+ view = self.context
+ catalog = get_utility(ICatalog)
+ registry = get_current_registry()
+ params = self.get_es_params(context)
+ items = CatalogResultSet(CatalogQuery(catalog).query(params,
+ sort_index=view.order_by,
+ reverse=view.reversed_order,
+ limit=limit))
+ for name, adapter in sorted(registry.getAdapters((view,), IViewQueryFilterExtension),
+ key=lambda x: x[1].weight):
+ items = adapter.filter(context, items)
+ return unique(items)
--- a/src/pyams_content/shared/view/interfaces/__init__.py Tue Jun 27 11:47:34 2017 +0200
+++ b/src/pyams_content/shared/view/interfaces/__init__.py Tue Jun 27 11:49:01 2017 +0200
@@ -16,10 +16,12 @@
# import standard library
# import interfaces
+from pyams_content.component.links.interfaces import IInternalReferencesList
from pyams_content.shared.common.interfaces import ISharedContent, IWfSharedContent, ISharedTool
# import packages
from pyams_sequence.schema import InternalReferencesList
+from pyams_thesaurus.schema import ThesaurusTermsListField
from zope.interface import Interface, Attribute
from zope.schema import List, Choice, Bool
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
@@ -31,10 +33,10 @@
VIEW_CONTENT_NAME = _('View')
-CREATION_DATE_ORDER = 'creation_datetime'
-UPDATE_DATE_ORDER = 'update_datetime'
-PUBLICATION_DATE_ORDER = 'publication_datetime'
-FIRSTPUBLICATION_DATE_ORDER = 'first_publication_datetime'
+CREATION_DATE_ORDER = 'created_date'
+UPDATE_DATE_ORDER = 'modified_date'
+PUBLICATION_DATE_ORDER = 'publication_date'
+FIRSTPUBLICATION_DATE_ORDER = 'first_publication_date'
VIEW_ORDER = {CREATION_DATE_ORDER: _("Creation date"),
UPDATE_DATE_ORDER: _("Last update date"),
@@ -59,8 +61,8 @@
order_by = Choice(title=_("Order by"),
description=_("Property to use to sort results"),
+ vocabulary=VIEW_ORDER_VOCABULARY,
required=True,
- vocabulary=VIEW_ORDER_VOCABULARY,
default=FIRSTPUBLICATION_DATE_ORDER)
reversed_order = Bool(title=_("Reversed order?"),
@@ -68,21 +70,43 @@
required=True,
default=True)
+ def get_results(self, context):
+ """Get results of catalog query"""
+
class IView(ISharedContent):
"""Workflow managed view interface"""
-class IViewExtension(Interface):
- """View query extension interface
+class IViewQuery(Interface):
+ """View query interface"""
+
+ def get_results(self, context):
+ """Get results of catalog query"""
+
- This interface is used to add features to views from external packages
- """
+class IViewQueryExtension(Interface):
+ """Base view query extension"""
+
+ weight = Attribute("Extension weight")
+
+
+class IViewQueryParamsExtension(IViewQueryExtension):
+ """View query extension interface"""
def get_params(self, context):
"""Add params to catalog query"""
- weight = Attribute("Extension weight")
+
+class IViewQueryEsParamsExtension(IViewQueryExtension):
+ """View query parameters extension for Elasticsearch"""
+
+ def get_es_params(self, context):
+ """Add params to Elasticsearch query"""
+
+
+class IViewQueryFilterExtension(IViewQueryExtension):
+ """View query filter extension"""
def filter(self, context, items):
"""Filter items after catalog query"""
@@ -91,9 +115,42 @@
VIEW_REFERENCES_SETTINGS_KEY = 'pyams_content.view.references'
-class IViewInternalReferencesSettings(Interface):
+ALWAYS_REFERENCE_MODE = 'always'
+IFEMPTY_REFERENCE_MODE = 'if_empty'
+
+REFERENCES_MODES = {ALWAYS_REFERENCE_MODE: _("Always include selected internal references"),
+ IFEMPTY_REFERENCE_MODE: _("Include selected internal references only if empty")}
+
+REFERENCES_MODES_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t)
+ for v, t in REFERENCES_MODES.items()])
+
+
+class IViewInternalReferencesSettings(IInternalReferencesList):
"""View internal references settings"""
- references = InternalReferencesList(title=_("Internal references"),
- description=_("List of internal references"),
- required=False)
+ references_mode = Choice(title=_("Internal references usage"),
+ description=_("Specify how selected references are included into view results"),
+ vocabulary=REFERENCES_MODES_VOCABULARY,
+ required=True,
+ default=ALWAYS_REFERENCE_MODE)
+
+
+VIEW_THEMES_SETTINGS_KEY = 'pyams_content.view.themes'
+
+
+class IViewThemesSettings(Interface):
+ """View themess ettings"""
+
+ select_context_themes = Bool(title=_("Select context themes?"),
+ description=_("If 'yes', themes will be extracted from context"),
+ required=True,
+ default=False)
+
+ themes = ThesaurusTermsListField(title=_("Other terms"),
+ required=False)
+
+ def get_themes(self, context):
+ """Get all themes for given context"""
+
+ def get_themes_index(self, context):
+ """Get all themes index values for given context"""
--- a/src/pyams_content/shared/view/manager.py Tue Jun 27 11:47:34 2017 +0200
+++ b/src/pyams_content/shared/view/manager.py Tue Jun 27 11:49:01 2017 +0200
@@ -16,6 +16,7 @@
# import standard library
# import interfaces
+from pyams_content.component.theme.interfaces import IThemesManagerTarget
from pyams_content.shared.view.interfaces import IViewsManager, VIEW_CONTENT_TYPE
from zope.annotation.interfaces import IAttributeAnnotatable
from zope.component.interfaces import ISite
@@ -29,7 +30,7 @@
from zope.interface import implementer
-@implementer(IViewsManager, IAttributeAnnotatable)
+@implementer(IViewsManager, IThemesManagerTarget, IAttributeAnnotatable)
class ViewsManager(SharedTool):
"""Views manager class"""
--- a/src/pyams_content/shared/view/reference.py Tue Jun 27 11:47:34 2017 +0200
+++ b/src/pyams_content/shared/view/reference.py Tue Jun 27 11:49:01 2017 +0200
@@ -17,12 +17,18 @@
from persistent import Persistent
# import interfaces
+from hypatia.interfaces import ICatalog
from pyams_content.shared.view.interfaces import IViewInternalReferencesSettings, IWfView, VIEW_REFERENCES_SETTINGS_KEY, \
- IViewExtension
+ IViewQueryFilterExtension, ALWAYS_REFERENCE_MODE
from zope.annotation.interfaces import IAnnotations
# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Any
+from pyams_catalog.query import CatalogResultSet
+from pyams_content.workflow import VISIBLE_STATES
from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import get_utility
from pyramid.threadlocal import get_current_registry
from zope.container.contained import Contained
from zope.interface import implementer
@@ -36,6 +42,7 @@
"""View internal references settings"""
references = FieldProperty(IViewInternalReferencesSettings['references'])
+ references_mode = FieldProperty(IViewInternalReferencesSettings['references_mode'])
@adapter_config(context=IWfView, provides=IViewInternalReferencesSettings)
@@ -50,14 +57,18 @@
return settings
-@adapter_config(name='references', context=IWfView, provides=IViewExtension)
-class ViewInternalReferencesExtension(ContextAdapter):
- """View internal references extension"""
-
- def get_param(self, context):
- pass
+@adapter_config(name='references', context=IWfView, provides=IViewQueryFilterExtension)
+class ViewInternalReferencesQueryFilterExtension(ContextAdapter):
+ """View internal references filter extension"""
weight = 999
def filter(self, context, items):
+ settings = IViewInternalReferencesSettings(self.context)
+ if not settings.references:
+ return items
+ if (not items) or (settings.references_mode == ALWAYS_REFERENCE_MODE):
+ catalog = get_utility(ICatalog)
+ params = Any(catalog['oid'], settings.references) & Any(catalog['workflow_state'], VISIBLE_STATES)
+ items.prepend(CatalogResultSet(CatalogQuery(catalog).query(params)))
return items
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/view/theme.py Tue Jun 27 11:49:01 2017 +0200
@@ -0,0 +1,64 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.theme.interfaces import IThemesInfo
+from pyams_content.shared.view.interfaces import IViewThemesSettings, IWfView, VIEW_THEMES_SETTINGS_KEY
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IViewThemesSettings)
+class ViewThemesSettings(Persistent, Contained):
+ """View themes settings"""
+
+ select_context_themes = FieldProperty(IViewThemesSettings['select_context_themes'])
+ themes = FieldProperty(IViewThemesSettings['themes'])
+
+ def get_themes(self, context):
+ themes = set()
+ if self.select_context_themes:
+ themes_info = IThemesInfo(context, None)
+ if themes_info is not None:
+ themes |= set(themes_info.themes or ())
+ if self.themes:
+ themes |= set(self.themes)
+ return themes
+
+ def get_themes_index(self, context):
+ return [theme.label for theme in self.get_themes(context)]
+
+
+@adapter_config(context=IWfView, provides=IViewThemesSettings)
+def ViewThemesSettingsFactory(view):
+ """View themes settings factory"""
+ annotations = IAnnotations(view)
+ settings = annotations.get(VIEW_THEMES_SETTINGS_KEY)
+ if settings is None:
+ settings = annotations[VIEW_THEMES_SETTINGS_KEY] = ViewThemesSettings()
+ get_current_registry().notify(ObjectCreatedEvent(settings))
+ locate(settings, view, '++view:themes++')
+ return settings
--- a/src/pyams_content/shared/view/zmi/properties.py Tue Jun 27 11:47:34 2017 +0200
+++ b/src/pyams_content/shared/view/zmi/properties.py Tue Jun 27 11:49:01 2017 +0200
@@ -39,3 +39,4 @@
fieldset_class = 'bordered no-x-margin margin-y-10'
fields = field.Fields(IWfView).select('selected_content_types', 'order_by', 'reversed_order')
+ weight = 1
--- a/src/pyams_content/shared/view/zmi/reference.py Tue Jun 27 11:47:34 2017 +0200
+++ b/src/pyams_content/shared/view/zmi/reference.py Tue Jun 27 11:49:01 2017 +0200
@@ -18,7 +18,7 @@
# import interfaces
from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
from pyams_content.shared.view.interfaces import IWfView, IViewInternalReferencesSettings
-from pyams_form.interfaces.form import IWidgetForm
+from pyams_form.interfaces.form import IWidgetForm, IUncheckedEditFormButtons
from pyams_skin.interfaces import IInnerPage
from pyams_skin.layer import IPyAMSLayer
from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
@@ -42,31 +42,24 @@
@viewlet_config(name='references.divider', context=IWfView, layer=IAdminLayer,
manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=289)
-class ReferencesMenuDivider(MenuDivider):
- """References menu divider"""
+class ViewReferencesMenuDivider(MenuDivider):
+ """View references menu divider"""
@viewlet_config(name='references.menu', context=IWfView, layer=IAdminLayer,
manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=290)
-class ReferencesMenu(MenuItem):
- """References menu"""
+class ViewReferencesMenu(MenuItem):
+ """View references menu"""
label = _("References...")
icon_class = 'fa-link'
url = '#references.html'
-class IReferencesEditButtons(Interface):
- """References settings form buttons"""
-
- reset = ResetButton(name='reset', title=_("Reset"))
- submit = button.Button(name='submit', title=_("Submit"))
-
-
@pagelet_config(name='references.html', context=IWfView, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
@implementer(IWidgetForm, IInnerPage)
-class ReferencesEditForm(AdminEditForm):
- """References settings edit form"""
+class ViewReferencesEditForm(AdminEditForm):
+ """View references settings edit form"""
legend = _("View internal references settings")
@@ -75,7 +68,7 @@
@property
def buttons(self):
if self.mode == INPUT_MODE:
- return button.Buttons(IReferencesEditButtons)
+ return button.Buttons(IUncheckedEditFormButtons)
else:
return button.Buttons(Interface)
@@ -84,5 +77,5 @@
@view_config(name='references.json', context=IWfView, request_type=IPyAMSLayer,
permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class ReferencesAJAXEditForm(AJAXEditForm, ReferencesEditForm):
+class ViewReferencesAJAXEditForm(AJAXEditForm, ViewReferencesEditForm):
"""References settings edit form, JSON renderer"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/view/zmi/summary.py Tue Jun 27 11:49:01 2017 +0200
@@ -0,0 +1,61 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.common.interfaces.zmi import IInnerSummaryView
+from pyams_content.shared.view.interfaces import IWfView
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
+# import packages
+from pyams_content.shared.common.zmi.summary import SharedContentSummaryForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_zmi.form import InnerAdminDisplayForm
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@adapter_config(name='view-summary',
+ context=(IWfView, IPyAMSLayer, SharedContentSummaryForm),
+ provides=IInnerTabForm)
+class SharedViewSummaryForm(InnerAdminDisplayForm):
+ """Shared view summary"""
+
+ weight = 20
+ tab_label = _("Quick preview")
+ tab_target = 'view-summary.html'
+
+ fields = field.Fields(Interface)
+
+
+@pagelet_config(name='view-summary.html', context=IWfView, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/summary.pt', layer=IPyAMSLayer)
+@implementer(IInnerSummaryView)
+class SharedViewSummaryView(object):
+ """Shared view summary view"""
+
+ def __init__(self, context, request):
+ super(SharedViewSummaryView, self).__init__(context, request)
+
+ @property
+ def items(self):
+ return self.context.get_results(self.context)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/view/zmi/templates/summary.pt Tue Jun 27 11:49:01 2017 +0200
@@ -0,0 +1,3 @@
+<tal:loop repeat="item view.items">
+ <span tal:content="i18n:item.title" /><br />
+</tal:loop>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/view/zmi/theme.py Tue Jun 27 11:49:01 2017 +0200
@@ -0,0 +1,82 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.view.interfaces import IWfView, IViewThemesSettings, IViewsManager
+from pyams_form.interfaces.form import IWidgetForm, IUncheckedEditFormButtons
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_thesaurus.interfaces.thesaurus import IThesaurusContextManager
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.registry import get_utility
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminEditForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='themes.menu', context=IWfView, layer=IAdminLayer,
+ manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=350)
+class ViewThemesMenu(MenuItem):
+ """View themes menu"""
+
+ label = _("Themes...")
+ icon_class = 'fa-tags'
+ url = '#themes.html'
+
+
+@pagelet_config(name='themes.html', context=IWfView, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class ViewThemesEditForm(AdminEditForm):
+ """View themes settings edit form"""
+
+ legend = _("View themes settings")
+
+ fields = field.Fields(IViewThemesSettings)
+
+ @property
+ def buttons(self):
+ if self.mode == INPUT_MODE:
+ return button.Buttons(IUncheckedEditFormButtons)
+ else:
+ return button.Buttons(Interface)
+
+ ajax_handler = 'themes.json'
+
+ def updateWidgets(self, prefix=None):
+ super(ViewThemesEditForm, self).updateWidgets(prefix)
+ if 'themes' in self.widgets:
+ manager = get_utility(IViewsManager)
+ self.widgets['themes'].thesaurus_name = IThesaurusContextManager(manager).thesaurus_name
+
+
+@view_config(name='themes.json', context=IWfView, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ViewThemesAJAXEditForm(AJAXEditForm, ViewThemesEditForm):
+ """View themes settings edit form, JSON renderer"""