Create base infrastructure for typed contents management
authorThierry Florac <thierry.florac@onf.fr>
Fri, 10 Nov 2017 12:07:43 +0100
changeset 276 78422a1c4228
parent 275 0c63253d75bd
child 277 9649f8ce3b1c
Create base infrastructure for typed contents management
src/pyams_content/shared/common/interfaces/types.py
src/pyams_content/shared/common/types.py
src/pyams_content/shared/common/zmi/preview.py
src/pyams_content/shared/common/zmi/types.py
src/pyams_content/skin/resources/js/pyams_content.js
src/pyams_content/skin/resources/js/pyams_content.min.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/interfaces/types.py	Fri Nov 10 12:07:43 2017 +0100
@@ -0,0 +1,102 @@
+#
+# 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 import ISharedTool
+from zope.container.interfaces import IContainer
+from zope.location.interfaces import ILocation
+
+# import packages
+from pyams_i18n.schema import I18nTextLineField, I18nThumbnailImageField
+from zope.container.constraints import contains
+from zope.interface import Attribute
+from zope.schema import TextLine, List, Choice
+
+from pyams_content import _
+
+
+class IBaseDataType(ILocation):
+    """Data interface for data-types and sub-types"""
+
+    name = TextLine(title=_("Name"),
+                    description=_("Name of this data type; must be unique between all data types"),
+                    required=True)
+
+    label = I18nTextLineField(title=_("Label"),
+                              required=True)
+
+    navigation_label = I18nTextLineField(title=_("Navigation label"),
+                                         description=_("Label used for navigation entries"),
+                                         required=False)
+
+    tabfolder_label = I18nTextLineField(title=_("Tab-folder label"),
+                                        description=_("Label used to include into tab folder"),
+                                        required=False)
+
+    seealso_label = I18nTextLineField(title=_("'See also' label"),
+                                      description=_("This label can be used when contents of this type will be "
+                                                    "displayed in a 'See also' entries block"),
+                                      required=False)
+
+    single_label = I18nTextLineField(title=_("'Single value' label"),
+                                     description=_("Label given to this type when a single value is displayed"),
+                                     required=False)
+
+    seeall_label = I18nTextLineField(title=_("'Link to list' label"),
+                                     description=_("Label used to display a link to a list of items of this type"),
+                                     required=False)
+
+    next_label = I18nTextLineField(title=_("Next content label"),
+                                   description=_("Label used to announce next date for this type"),
+                                   required=False)
+
+    pictogram = I18nThumbnailImageField(title=_("Pictogram"),
+                                        description=_("Image associated to this data type"),
+                                        required=False)
+
+
+class ISubType(IBaseDataType):
+    """Data sub-type interface"""
+
+
+class IDataType(IBaseDataType, IContainer):
+    """Data type interface"""
+
+    contains(ISubType)
+
+    field_names = List(title=_("Field names"),
+                       description=_("List of fields associated with this data type"),
+                       value_type=Choice(vocabulary='PyAMS types interface fields'))
+
+
+#
+# Types data manager interfaces
+#
+
+DATA_MANAGER_ANNOTATION_KEY = 'pyams_content.types.manager'
+
+
+class ITypedDataManager(IContainer):
+    """Typed shared data manager interface"""
+
+    contains(IDataType)
+
+
+class ITypedSharedTool(ISharedTool):
+    """Shared tool containing typed data"""
+
+    shared_content_types_fields = Attribute("Content fields interface")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/types.py	Fri Nov 10 12:07:43 2017 +0100
@@ -0,0 +1,134 @@
+#
+# 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.extfile.interfaces import IExtFileContainerTarget
+from pyams_content.component.links.interfaces import ILinkContainerTarget
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget
+from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
+from pyams_content.shared.common.interfaces.types import IDataType, ISubType, IBaseDataType, ITypedSharedTool, \
+    ITypedDataManager, DATA_MANAGER_ANNOTATION_KEY
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from zope.location.interfaces import ISublocations
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_content.shared.common.manager import SharedTool
+from pyams_i18n.property import I18nFileProperty
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import get_global_registry
+from pyams_utils.request import check_request
+from pyams_utils.traversing import get_parent
+from pyams_utils.vocabulary import vocabulary_config
+from zope.annotation.interfaces import IAnnotations
+from zope.container.contained import Contained
+from zope.container.ordered import OrderedContainer
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema import getFieldsInOrder
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+class BaseDataType(Persistent, Contained):
+    """Base data type"""
+
+    label = FieldProperty(IBaseDataType['label'])
+    navigation_label = FieldProperty(IBaseDataType['navigation_label'])
+    tabfolder_label = FieldProperty(IBaseDataType['tabfolder_label'])
+    seealso_label = FieldProperty(IBaseDataType['seealso_label'])
+    single_label = FieldProperty(IBaseDataType['single_label'])
+    seeall_label = FieldProperty(IBaseDataType['seeall_label'])
+    next_label = FieldProperty(IBaseDataType['next_label'])
+    pictogram = I18nFileProperty(IBaseDataType['pictogram'])
+
+
+@implementer(ISubType, IParagraphContainerTarget, IExtFileContainerTarget, ILinkContainerTarget)
+class SubType(BaseDataType):
+    """Data sub-type persistent class"""
+
+
+@implementer(IDataType, IParagraphContainerTarget, IExtFileContainerTarget, ILinkContainerTarget)
+class DataType(BaseDataType, OrderedContainer):
+    """Data type persistent class"""
+
+    field_names = FieldProperty(IDataType['field_names'])
+
+
+@implementer(ITypedDataManager)
+class TypedDataManager(OrderedContainer):
+    """Data types container persistent class"""
+
+
+@adapter_config(context=IBaseDataType, provides=IFormContextPermissionChecker)
+class BaseDatatypePermissionChecker(ContextAdapter):
+    """Base data type permission checker"""
+
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+
+@implementer(ITypedSharedTool)
+class TypedSharedTool(SharedTool):
+    """Typed shared tool"""
+
+    shared_content_types_fields = None
+
+
+@adapter_config(context=ITypedSharedTool, provides=ITypedDataManager)
+def TypedSharedToolDataManagerFactory(context):
+    """Types shared tool data manager factory"""
+    annotations = IAnnotations(context)
+    manager = annotations.get(DATA_MANAGER_ANNOTATION_KEY)
+    if manager is None:
+        manager = annotations[DATA_MANAGER_ANNOTATION_KEY] = TypedDataManager()
+        registry = get_global_registry()
+        registry.notify(ObjectCreatedEvent(manager))
+        locate(manager, context, '++types++')
+    return manager
+
+
+@adapter_config(name='types', context=ITypedSharedTool, provides=ITraversable)
+class TypedSharedToolTypesNamespace(ContextAdapter):
+    """Typed shared tool ++types++ namespace"""
+
+    def traverse(self, name, furtherpath=None):
+        return ITypedDataManager(self.context)
+
+
+@adapter_config(name='types', context=ITypedSharedTool, provides=ISublocations)
+class TypedSharedToolSublocations(ContextAdapter):
+    """Typed shared tool sublocations adapter"""
+
+    def sublocations(self):
+        return ITypedDataManager(self.context).values()
+
+
+@vocabulary_config(name='PyAMS types interface fields')
+class TypedSharedToolDataTypesFields(SimpleVocabulary):
+    """Typed shared tool data types fields vocabulary"""
+
+    def __init__(self, context):
+        terms = []
+        parent = get_parent(context, ITypedSharedTool)
+        if parent is not None:
+            request = check_request()
+            translate = request.localizer.translate
+            terms = [SimpleTerm(name, title=translate(field.title))
+                     for name, field in getFieldsInOrder(parent.shared_content_types_fields)]
+        super(TypedSharedToolDataTypesFields, self).__init__(terms)
--- a/src/pyams_content/shared/common/zmi/preview.py	Fri Nov 10 12:07:09 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-#
-# 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 import IWfSharedContent
-from pyams_content.shared.common.interfaces.zmi import IPreviewForm
-from pyams_skin.interfaces.viewlet import IToolbarViewletManager
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from pyams_zmi.layer import IAdminLayer
-
-# import packages
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.viewlet.toolbar import ToolbarAction
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogDisplayForm
-from z3c.form import field
-from zope.interface import implementer, Interface
-
-from pyams_content import _
-
-
-@viewlet_config(name='preview.action', context=IWfSharedContent, layer=IAdminLayer, view=Interface,
-                manager=IToolbarViewletManager, permisison=VIEW_SYSTEM_PERMISSION, weight=50)
-class WfSharedContentPreviewAction(ToolbarAction):
-    """Shared content preview action"""
-
-    label = _("Preview")
-
-    group_css_class = 'btn-group margin-right-10'
-    label_css_class = 'fa fa-newspaper-o'
-    css_class = 'btn btn-xs btn-default'
-
-    url = 'preview.html'
-    modal_target = True
-
-
-@pagelet_config(name='preview.html', context=IWfSharedContent, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
-@implementer(IPreviewForm)
-class WfSharedContentPreviewForm(AdminDialogDisplayForm):
-    """Shared content preview"""
-
-    legend = _("Content preview")
-    dialog_class = 'modal-max'
-
-    fields = field.Fields(Interface)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/types.py	Fri Nov 10 12:07:43 2017 +0100
@@ -0,0 +1,577 @@
+#
+# 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 json
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
+from pyams_content.shared.common.interfaces.types import ITypedSharedTool, ITypedDataManager, \
+    IBaseDataType, IDataType, ISubType
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.container import ITableElementName
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_viewlet.interfaces import IViewletManager
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import DISPLAY_MODE, IDataExtractedEvent
+from z3c.table.interfaces import IValues, IColumn
+
+# import packages
+from pyams_content.shared.common.types import DataType, SubType
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.table import BaseTable, SorterColumn, TrashColumn, NameColumn, ActionColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextRequestAdapter
+from pyams_utils.traversing import get_parent
+from pyams_utils.unicode import translate_string
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.view import ContainerAdminView
+from pyramid.decorator import reify
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import Invalid
+
+from pyams_content import _
+
+
+@viewlet_config(name='data-types.menu', context=ITypedSharedTool, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=MANAGE_TOOL_PERMISSION, weight=20)
+class TypedSharedToolTypesMenu(MenuItem):
+    """Typed shared tool types menu"""
+
+    label = _("Data types")
+    icon_class = 'fa-folder-o'
+    url = '#data-types.html'
+
+
+#
+# Typed shared data types manager target views
+#
+
+class TypedSharedToolTypesTable(ProtectedFormObjectMixin, BaseTable):
+    """Typed shared tool types table"""
+
+    id = 'types_list'
+    hide_header = True
+    sortOn = None
+
+    @property
+    def cssClasses(self):
+        classes = ['table', 'table-bordered', 'table-striped', 'table-hover', 'table-tight']
+        permission = self.permission
+        if (not permission) or self.request.has_permission(permission, self.context):
+            classes.append('table-dnd')
+        return {'table': ' '.join(classes)}
+
+    @property
+    def data_attributes(self):
+        attributes = super(TypedSharedToolTypesTable, self).data_attributes
+        attributes['table'] = {'id': self.id,
+                               'data-ams-plugins': 'pyams_content',
+                               'data-ams-plugin-pyams_content-src':
+                                   '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
+                               'data-ams-location': absolute_url(ITypedDataManager(self.context), self.request),
+                               'data-ams-tablednd-drag-handle': 'td.sorter',
+                               'data-ams-tablednd-drop-target': 'set-types-order.json'}
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(TypedSharedToolTypesTable, self).values)
+
+    def render(self):
+        if not self.values:
+            translate = self.request.localizer.translate
+            return translate(_("No currently defined data type."))
+        return super(TypedSharedToolTypesTable, self).render()
+
+
+@adapter_config(context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable), provides=IValues)
+class TypedSharedToolTypesValues(ContextRequestViewAdapter):
+    """Typed shared tool types table values adapter"""
+
+    @property
+    def values(self):
+        return ITypedDataManager(self.context).values()
+
+
+@adapter_config(name='sorter', context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable),
+                provides=IColumn)
+class TypedSharedToolTypesSorterColumn(ProtectedFormObjectMixin, SorterColumn):
+    """Typed shared tool types sorter column"""
+
+
+@adapter_config(name='name', context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable),
+                provides=IColumn)
+class TypedSharedToolTypesNameColumn(NameColumn):
+    """Typed shared tool types name column"""
+
+    _header = _("Data type label")
+
+    def renderCell(self, item):
+        return '<span data-ams-stop-propagation="true" ' \
+               '      data-ams-click-handler="PyAMS_content.types.switchSubtypes">' \
+               '    <span class="small hint" title="{hint}" data-ams-hint-gravity="e">' \
+               '        <i class="fa fa-plus-square-o switch"></i>' \
+               '    </span>' \
+               '</span>&nbsp;&nbsp;&nbsp;<span class="title">{title}</span>' \
+               '<div class="inner-table-form subtypes margin-x-10 margin-bottom-0 padding-left-5"></div>'.format(
+            hint=self.request.localizer.translate(_("Click to see subtypes")),
+            title=super(TypedSharedToolTypesNameColumn, self).renderCell(item))
+
+
+@adapter_config(name='paragraphs', context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable),
+                provides=IColumn)
+class TypedSharedToolTypesParagraphsColumn(ActionColumn):
+    """Typed shared tool types paragraphs column"""
+
+    weight = 100
+
+    icon_class = 'fa fa-fw fa-paragraph'
+    icon_hint = _("Default paragraphs")
+
+    url = 'paragraphs-dialog.html'
+    modal_target = True
+
+    permission = MANAGE_TOOL_PERMISSION
+
+
+@adapter_config(name='associations', context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable),
+                provides=IColumn)
+class TypedSharedToolTypesAssociationsColumn(ActionColumn):
+    """Typed shared tool types associations column"""
+
+    weight = 110
+
+    icon_class = 'fa fa-fw fa-link'
+    icon_hint = _("Default associations")
+
+    url = 'associations-dialog.html'
+    modal_target = True
+
+    permission = MANAGE_TOOL_PERMISSION
+
+
+@adapter_config(name='trash', context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable),
+                provides=IColumn)
+class TypedSharedToolTypesTrashColumn(TrashColumn):
+    """Typed shared tool types trash column"""
+
+    permission = MANAGE_TOOL_PERMISSION
+
+
+@pagelet_config(name='data-types.html', context=ITypedSharedTool, layer=IPyAMSLayer,
+                permission=MANAGE_TOOL_PERMISSION)
+class TypedSharedToolTypesView(ContainerAdminView):
+    """Typed shared tool types view"""
+
+    title = _("Content data types")
+    table_class = TypedSharedToolTypesTable
+
+
+#
+# Typed shared data types manager views
+#
+
+@view_config(name='delete-element.json', context=ITypedDataManager, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def delete_data_type(request):
+    """Data type delete view"""
+    translate = request.localizer.translate
+    name = request.params.get('object_name')
+    if not name:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("No provided object_name argument!"))}}
+    if name not in request.context:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("Given data type doesn't exist!"))}}
+    del request.context[name]
+    return {'status': 'success'}
+
+
+@view_config(name='set-types-order.json', context=ITypedDataManager, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def set_data_types_order(request):
+    """Update data types order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
+#
+# Data type views
+#
+
+@adapter_config(context=(IBaseDataType, IPyAMSLayer), provides=ITableElementName)
+class DataTypeElementNameAdapter(ContextRequestAdapter):
+    """Types shared tool types name adapter"""
+
+    @property
+    def name(self):
+        return II18n(self.context).query_attribute('label', request=self.request)
+
+
+@viewlet_config(name='add-data-type.action', context=ITypedSharedTool, layer=IAdminLayer,
+                view=TypedSharedToolTypesView, manager=IWidgetTitleViewletManager,
+                permission=MANAGE_TOOL_PERMISSION, weight=1)
+class DataTypeAddAction(ToolbarAction):
+    """Data type adding action"""
+
+    label = _("Add data type")
+    label_css_class = 'fa fa-fw fa-plus'
+    url = 'add-data-type.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-data-type.html', context=ITypedSharedTool, layer=IPyAMSLayer,
+                permission=MANAGE_TOOL_PERMISSION)
+class DataTypeAddForm(AdminDialogAddForm):
+    """Data type add form"""
+
+    legend = _("Add new data type")
+    icon_css_class = 'fa fa-fw fa-folder-o'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(IDataType).omit('__parent__', '__name__')
+
+    ajax_handler = 'add-data-type.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+    def create(self, data):
+        return DataType()
+
+    def add(self, object):
+        name = translate_string(object.name, spaces='-')
+        ITypedDataManager(self.context)[name] = object
+
+    def nextURL(self):
+        return absolute_url(self.context, self.request, 'admin#data-types.html')
+
+
+@subscriber(IDataExtractedEvent, form_selector=DataTypeAddForm)
+def handle_datatype_add_form_data_extraction(event):
+    """Check new data type for existing name"""
+    context = event.form.context
+    manager = ITypedDataManager(context)
+    name = event.data.get('name')
+    if translate_string(name, spaces='-') in manager:
+        event.form.widgets.errors += (Invalid(_("Specified type name is already used!")),)
+
+
+@view_config(name='add-data-type.json', context=ITypedSharedTool, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class DataTypeAJAXAddForm(AJAXAddForm, DataTypeAddForm):
+    """Data type add form, JSON renderer"""
+
+    def nextURL(self):
+        return '#data-types.html'
+
+
+@pagelet_config(name='properties.html', context=IDataType, layer=IPyAMSLayer, permission=MANAGE_TOOL_PERMISSION)
+class DataTypeEditForm(AdminDialogEditForm):
+    """Data type edit form"""
+
+    legend = _("Data type properties")
+    icon_css_class = 'fa fa-fw fa-folder-o'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(IDataType).omit('__parent__', '__name__')
+
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(DataTypeEditForm, self).updateWidgets(prefix)
+        if 'name' in self.widgets:
+            self.widgets['name'].mode = DISPLAY_MODE
+
+
+@view_config(name='properties.json', context=IDataType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class DataTypeAJAXEditForm(AJAXEditForm, DataTypeEditForm):
+    """Data type edit form, JSON renderer"""
+
+
+#
+# Subtypes views
+#
+
+class DatatypeSubtypesTable(BaseTable):
+    """Data type subtypes table"""
+
+    @property
+    def id(self):
+        return 'subtypes_{0}_list'.format(self.context.__name__)
+
+    hide_header = True
+    sortOn = None
+
+    widget_class = 'ams-widget margin-top-5'
+    cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight table-dnd'}
+
+    @property
+    def data_attributes(self):
+        attributes = super(DatatypeSubtypesTable, self).data_attributes
+        attributes['table'] = {'id': self.id,
+                               'data-ams-plugins': 'pyams_content',
+                               'data-ams-plugin-pyams_content-src':
+                                   '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
+                               'data-ams-location': absolute_url(self.context, self.request),
+                               'data-ams-tablednd-drag-handle': 'td.sorter',
+                               'data-ams-tablednd-drop-target': 'set-subtypes-order.json'}
+        attributes.setdefault('tr', {}).setdefault('data-ams-stop-propagation', 'true')
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(DatatypeSubtypesTable, self).values)
+
+
+@adapter_config(context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable), provides=IValues)
+class DatatypeSubtypesTableValues(ContextRequestViewAdapter):
+    """Data type subtypes table values adapter"""
+
+    @property
+    def values(self):
+        return self.context.values()
+
+
+@adapter_config(name='sorter', context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable), provides=IColumn)
+class DatatypeSubtypesTableSorterColumn(SorterColumn):
+    """Data type subtypes table sorter column"""
+
+
+@adapter_config(name='name', context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable), provides=IColumn)
+class DatatypeSubtypesTableNameColumn(NameColumn):
+    """Data type subtypes table name column"""
+
+    _header = _("Subtype label")
+
+    def renderHeadCell(self):
+        result = super(DatatypeSubtypesTableNameColumn, self).renderHeadCell()
+        registry = self.request.registry
+        viewlet = registry.queryMultiAdapter((self.context, self.request, self.table), IViewletManager,
+                                             name='pyams.widget_title')
+        if viewlet is not None:
+            viewlet.update()
+            result += viewlet.render()
+        return result
+
+
+@adapter_config(name='paragraphs', context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable),
+                provides=IColumn)
+class DatatypeSubtypesTableParagraphsColumn(ActionColumn):
+    """Data type subtypes paragraphs column"""
+
+    weight = 100
+
+    icon_class = 'fa fa-fw fa-paragraph'
+    icon_hint = _("Default paragraphs")
+
+    url = 'paragraphs-dialog.html'
+    modal_target = True
+
+    permission = MANAGE_TOOL_PERMISSION
+
+
+@adapter_config(name='associations', context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable),
+                provides=IColumn)
+class DatatypeSubtypesTableAssociationsColumn(ActionColumn):
+    """Data type subtypes associations column"""
+
+    weight = 110
+
+    icon_class = 'fa fa-fw fa-link'
+    icon_hint = _("Default associations")
+
+    url = 'associations-dialog.html'
+    modal_target = True
+
+    permission = MANAGE_TOOL_PERMISSION
+
+
+@adapter_config(name='trash', context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable), provides=IColumn)
+class DatatypeSubtypesTableTrashColumn(TrashColumn):
+    """Data type subtypes table trash column"""
+
+    permission = MANAGE_TOOL_PERMISSION
+
+
+@view_config(name='get-subtypes-table.json', context=ITypedDataManager, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def get_subtypes_table(request):
+    """Get subtypes table"""
+    datatype = request.context.get(str(request.params.get('object_name')))
+    if datatype is None:
+        raise NotFound()
+    table = DatatypeSubtypesTable(datatype, request)
+    table.update()
+    return table.render()
+
+
+@view_config(name='set-subtypes-order.json', context=IDataType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def set_subtypes_order(request):
+    """Update subtypes order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
+@view_config(name='delete-element.json', context=IDataType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def delete_subtype(request):
+    """Data subtype delete view"""
+    translate = request.localizer.translate
+    name = request.params.get('object_name')
+    if not name:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("No provided object_name argument!"))}}
+    if name not in request.context:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("Given data subtype doesn't exist!"))}}
+    del request.context[name]
+    return {'status': 'success'}
+
+
+#
+# Data sub-types views
+#
+
+
+@viewlet_config(name='add-data-subtype.action', context=IDataType, layer=IPyAMSLayer,
+                view=DatatypeSubtypesTable, manager=IWidgetTitleViewletManager,
+                permission=MANAGE_TOOL_PERMISSION, weight=1)
+class DataSubtypeAddAction(ToolbarAction):
+    """Data subtype adding action"""
+
+    label = _("Add subtype")
+    label_css_class = 'fa fa-fw fa-plus'
+    url = 'add-data-subtype.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-data-subtype.html', context=IDataType, layer=IPyAMSLayer,
+                permission=MANAGE_TOOL_PERMISSION)
+class DataSubtypeAddForm(AdminDialogAddForm):
+    """Data subtype add form"""
+
+    legend = _("Add new subtype")
+    icon_css_class = 'fa fa-fw fa-folder-o'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(ISubType).omit('__parent__', '__name__')
+
+    ajax_handler = 'add-data-subtype.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+    def create(self, data):
+        return SubType()
+
+    def add(self, object):
+        name = translate_string(object.name, spaces='-')
+        IDataType(self.context)[name] = object
+
+    def nextURL(self):
+        return absolute_url(self.context, self.request, 'admin#data-types.html')
+
+
+@subscriber(IDataExtractedEvent, form_selector=DataSubtypeAddForm)
+def handle_subtype_add_form_data_extraction(event):
+    """Check new data subtype for existing name"""
+    context = event.form.context
+    manager = IDataType(context)
+    name = event.data.get('name')
+    if translate_string(name, spaces='-') in manager:
+        event.form.widgets.errors += (Invalid(_("Specified subtype name is already used!")),)
+
+
+@view_config(name='add-data-subtype.json', context=IDataType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class DataSubtypeAJAXAddForm(AJAXAddForm, DataSubtypeAddForm):
+    """Data subtype add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        subtypes_table = DatatypeSubtypesTable(self.context, self.request)
+        subtypes_table.update()
+        return {'status': 'success',
+                'message': self.request.localizer.translate(_("Subtype was correctly added.")),
+                'events': [{
+                    'event': 'PyAMS_content.changed_item',
+                    'options': {
+                        'handler': 'PyAMS_content.types.refreshSubtypes',
+                        'object_name': subtypes_table.id,
+                        'table': subtypes_table.render()
+                    }
+                }]}
+
+
+@pagelet_config(name='properties.html', context=ISubType, layer=IPyAMSLayer, permission=MANAGE_TOOL_PERMISSION)
+class DataSubtypeEditForm(AdminDialogEditForm):
+    """Data subtype edit form"""
+
+    legend = _("Data subtype properties")
+    icon_css_class = 'fa fa-fw fa-folder-o'
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    fields = field.Fields(ISubType).omit('__parent__', '__name__')
+
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(DataSubtypeEditForm, self).updateWidgets(prefix)
+        if 'name' in self.widgets:
+            self.widgets['name'].mode = DISPLAY_MODE
+
+
+@view_config(name='properties.json', context=ISubType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class DataSubtypeAJAXEditForm(AJAXEditForm, DataSubtypeEditForm):
+    """Data subtype edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'label' in changes.get(IBaseDataType, ()):
+            target = get_parent(self.context, IDataType)
+            subtypes_table = DatatypeSubtypesTable(target, self.request)
+            subtypes_table.update()
+            return {'status': 'success',
+                    'message': self.request.localizer.translate(self.successMessage),
+                    'events': [{
+                        'event': 'PyAMS_content.changed_item',
+                        'options': {
+                            'handler': 'PyAMS_content.types.refreshSubtypes',
+                            'object_name': subtypes_table.id,
+                            'table': subtypes_table.render()
+                        }
+                    }]}
+        else:
+            return super(DataSubtypeAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/skin/resources/js/pyams_content.js	Fri Nov 10 12:07:09 2017 +0100
+++ b/src/pyams_content/skin/resources/js/pyams_content.js	Fri Nov 10 12:07:43 2017 +0100
@@ -214,6 +214,45 @@
 
 
 		/**
+		 * Types management
+		 */
+		types: {
+
+			switchSubtypes: function(element) {
+				var source = $(this);
+				var switcher = $('i.switch', source);
+				var td = source.parents('td');
+				var subtypes = $('.subtypes', td);
+				var datatype = source.parents('tr');
+				if (switcher.hasClass('fa-plus-square-o')) {
+					var container = datatype.parents('table');
+					subtypes.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>');
+					MyAMS.ajax.post(container.data('ams-location') + '/get-subtypes-table.json',
+									{object_name: datatype.data('ams-element-name')},
+									function(result) {
+										subtypes.html(result);
+										if (result) {
+											MyAMS.initContent(subtypes);
+											switcher.removeClass('fa-plus-square-o')
+													.addClass('fa-minus-square-o');
+										}
+									});
+				} else {
+					MyAMS.skin.cleanContainer(subtypes);
+					subtypes.empty();
+					switcher.removeClass('fa-minus-square-o')
+							.addClass('fa-plus-square-o');
+				}
+			},
+
+			refreshSubtypes: function(options) {
+				// Reload widget
+				PyAMS_content.refreshTable(options);
+			}
+		},
+
+
+		/**
 		 * Paragraphs management
 		 */
 		paragraphs: {
@@ -247,6 +286,16 @@
 				};
 			},
 
+			refreshParagraphs: function(options) {
+				// Reload widget
+				var widget = PyAMS_content.refreshTable(options);
+				// Check fieldset state
+				var legend = widget.siblings('legend');
+				if (legend.parents('fieldset:first').hasClass('switched')) {
+					legend.click();
+				}
+			},
+
 			refreshParagraph: function(changes) {
 				var container = $('table[id="paragraphs_list"]');
 				var para = $('tr[data-ams-element-name="' + changes.object_name + '"]', container);
--- a/src/pyams_content/skin/resources/js/pyams_content.min.js	Fri Nov 10 12:07:09 2017 +0100
+++ b/src/pyams_content/skin/resources/js/pyams_content.min.js	Fri Nov 10 12:07:43 2017 +0100
@@ -1,1 +1,1 @@
-(function(c,b){var e=b.MyAMS;var d={refreshForm:function(g){var f=c("#"+g.object_name);f.replaceWith(c(g.form));f=c("#"+g.object_name);e.initContent(f);return f},refreshTable:function(f){var g=c("#"+f.object_name).parent(".ams-widget");g.replaceWith(c(f.table));g=c("#"+f.object_name).parent(".ams-widget");e.initContent(g);return g},TinyMCE:{initEditor:function(f){f.image_list=d.TinyMCE.getImagesList;f.link_list=d.TinyMCE.getLinksList;return f},getImagesList:function(i){var f=c(document.activeElement).parents("form");if(f.exists()){var g=f.attr("data-ams-form-handler")||f.attr("action");var h=g.substr(0,g.lastIndexOf("/")+1);return e.ajax.post(h+"get-images-list.json",{},i)}},getLinksList:function(i){var f=c(document.activeElement).parents("form");if(f.exists()){var g=f.attr("data-ams-form-handler")||f.attr("action");var h=g.substr(0,g.lastIndexOf("/")+1);return e.ajax.post(h+"get-links-list.json",{},i)}}},profile:{switchFavorite:function(){var g=c(this);var f=g.data("sequence-oid");e.ajax.post("switch-user-favorite.json",{oid:f},function(h,i){if(h.favorite){g.removeClass("fa-star-o").addClass("fa-star")}else{g.removeClass("fa-star").addClass("fa-star-o")}})}},galleries:{updateImageTitle:function(f){c('img[id="'+f.image_id+'"]').attr("original-title",f.title)},switchImageVisibility:function(f){return function(){var h=c(this);var i=h.parents(".image");var g=i.parents(".gallery");e.ajax.post(g.data("ams-location")+"/set-image-visibility.json",{object_name:i.data("ams-element-name")},function(j,k){if(j.visible){c("i",h).attr("class","fa fa-fw fa-eye");h.parents(".btn-group").siblings("a.fancyimg").removeClass("not-visible")}else{c("i",h).attr("class","fa fa-fw fa-eye-slash text-danger");h.parents(".btn-group").siblings("a.fancyimg").addClass("not-visible")}})}},setOrder:function(h,i){if(i&&i.item.hasClass("already-dropped")){return}var f=i.item.parents(".gallery");var g=c(".image",f).listattr("data-ams-element-name");e.ajax.post(f.data("ams-location")+"/set-images-order.json",{images:JSON.stringify(g)})},removeFile:function(f){return function(){var g=c(this);e.skin.bigBox({title:e.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+e.i18n.DELETE_WARNING,buttons:e.i18n.BTN_OK_CANCEL},function(k){if(k===e.i18n.BTN_OK){var j=g.parents(".gallery");var i=j.data("ams-location");var l=g.parents(".image");var h=l.data("ams-element-name");e.ajax.post(i+"/delete-element.json",{object_name:h},function(m,n){l.remove()})}})}},afterFancyboxLoad:function(h,g){var f=h.element;if(f.hasClass("not-visible")){h.inner.prepend('<div class="hidden-mask"></div>')}}},imgmap:{init:function(){var f=c(this);e.ajax.check(c.fn.canvasAreaDraw,"/--static--/pyams_content/js/jquery-canvasAreaDraw"+e.devext+".js",function(){f.canvasAreaDraw({imageUrl:f.data("ams-image-url")})})},initSummary:function(){var f=c(this);e.ajax.check(c.fn.mapster,"/--static--/pyams_content/js/jquery-imagemapster-1.2.10"+e.devext+".js",function(){f.mapster({fillColor:"ff0000",fillOpacity:0.35,selected:true,highlight:true,staticState:true})})}},associations:{refreshAssociations:function(f){var h=d.refreshTable(f);var g=h.siblings("legend");if(g.parents("fieldset:first").hasClass("switched")){g.click()}},switchVisibility:function(f){return function(){var i=c(this);var h=i.parents("tr");var g=h.parents("table");e.ajax.post(g.data("ams-location")+"/set-association-visibility.json",{object_name:h.data("ams-element-name")},function(j,k){if(j.visible){c("i",i).attr("class","fa fa-fw fa-eye")}else{c("i",i).attr("class","fa fa-fw fa-eye-slash text-danger")}})}}},paragraphs:{preReload:function(){d.paragraphs.switched=c("i.switch.fa-minus-square-o","#paragraphs_list").parents("tr").listattr("id")},postReload:function(){c(d.paragraphs.switched).each(function(){c("i.switch.fa-plus-square-o",'[id="'+this+'"]').parents("div:first").click()});delete d.paragraphs.switched},switchVisibility:function(f){return function(){var i=c(this);var g=i.parents("tr");var h=g.parents("table");e.ajax.post(h.data("ams-location")+"/set-paragraph-visibility.json",{object_name:g.data("ams-element-name")},function(j,k){if(j.visible){c("i",i).attr("class","fa fa-fw fa-eye")}else{c("i",i).attr("class","fa fa-fw fa-eye-slash text-danger")}})}},refreshParagraph:function(h){var g=c('table[id="paragraphs_list"]');var f=c('tr[data-ams-element-name="'+h.object_name+'"]',g);c("span.title",f).html(h.title||" - - - - - - - -")},switchEditor:function(h){var k=c(this);var j=c("i.switch",k);var l=k.parents("td");var i=c(".editor",l);var f=k.parents("tr");if(j.hasClass("fa-plus-square-o")){var g=f.parents("table");i.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>');e.ajax.post(g.data("ams-location")+"/get-paragraph-editor.json",{object_name:f.data("ams-element-name")},function(m){i.html(m);if(m){e.initContent(i);j.removeClass("fa-plus-square-o").addClass("fa-minus-square-o");f.data("ams-disabled-handlers",true)}})}else{e.skin.cleanContainer(i);i.empty();j.removeClass("fa-minus-square-o").addClass("fa-plus-square-o");f.removeData("ams-disabled-handlers")}},switchAllEditors:function(g){var i=c(this);var h=c("i",i);var f=i.parents("table");if(h.hasClass("fa-plus-square-o")){h.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin");e.ajax.post(f.data("ams-location")+"/get-paragraphs-editors.json",{},function(k){for(var l in k){if(!k.hasOwnProperty(l)){continue}var j=c('tr[data-ams-element-name="'+l+'"]',f);var m=c(".editor",j);if(m.is(":empty")){m.html(k[l]);e.initContent(m)}c(".fa-plus-square-o",j).removeClass("fa-plus-square-o").addClass("fa-minus-square-o");j.data("ams-disabled-handlers",true)}if(!c("i.fa-plus-square-o",c("tbody",f)).exists()){h.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o")}})}else{c(".editor",f).each(function(){e.skin.cleanContainer(c(this));c(this).empty()});c(".fa-minus-square-o",f).removeClass("fa-minus-square-o").addClass("fa-plus-square-o");c("tr",f).removeData("ams-disabled-handlers")}},updateToolbar:function(h){var g=c('table[id="paragraphs_list"]');var f=c('tr[data-ams-element-name="'+h.object_name+'"]',g);var i=c(".title-toolbar",f);i.replaceWith(h.toolbar_tag);i=c(".title-toolbar",f);e.initContent(i)},updateMarkers:function(i){var h=c('table[id="paragraphs_list"]');var f=c('tr[data-ams-element-name="'+i.object_name+'"]',h);var j=c(".title-toolbar",f);var g=c("DIV.action."+i.marker_type,j);if(g.exists()){g.replaceWith(i.marker_tag)}else{c(i.marker_tag).appendTo(j)}if(i.marker_tag){g=c("DIV.action."+i.marker_type,j);e.initContent(g)}e.helpers.sort(j,"weight")}},illustration:{addIllustration:function(){var g=c(this);var h=g.parents(".btn-group");var f=h.siblings("legend.switcher");c("i.fa-plus",f).click();g.hide()}},fields:{refreshField:function(h){var g=c('table[id="form_fields_list"]');var f=c('tr[data-ams-element-name="'+h.object_name+'"]',g);c("td:nth-child(4)",f).html(h.title)},switchVisibility:function(f){return function(){var i=c(this);var h=i.parents("tr");var g=h.parents("table");e.ajax.post(g.data("ams-location")+"/set-form-field-visibility.json",{object_name:h.data("ams-element-name")},function(j,k){if(j.visible){c("i",i).attr("class","fa fa-fw fa-eye")}else{c("i",i).attr("class","fa fa-fw fa-eye-slash text-danger")}})}}},themes:{initExtracts:function(h){var g=c('select[name="form.widgets.thesaurus_name:list"]',h);var f=g.val();var j=c('select[name="form.widgets.extract_name:list"]',h);var i=j.val();if(f){e.jsonrpc.post("getExtracts",{thesaurus_name:f},{url:"/api/thesaurus/json"},function(k){j.empty();c(k.result).each(function(){c("<option></option>").attr("value",this.id).attr("selected",this.id===i).text(this.text).appendTo(j)})})}j.attr("data-ams-events-handlers",'{"select2-open": "PyAMS_content.themes.getExtracts"}')},getExtracts:function(i){var f=c(i.currentTarget);var h=f.parents("form");var g=c('select[name="form.widgets.thesaurus_name:list"]',h).val();if(g){e.jsonrpc.post("getExtracts",{thesaurus_name:g},{url:"/api/thesaurus/json"},function(l){var k=c('select[name="form.widgets.extract_name:list"]',h);var j=k.data("select2");j.results.empty();j.opts.populateResults.call(j,j.results,l.result,{term:""})})}}},review:{timer:null,timer_duration:{general:30000,chat:5000},initComments:function(g){var f=c(".chat-body",g);f.animate({scrollTop:f[0].scrollHeight},1000);clearInterval(d.review.timer);d.review.timer=setInterval(d.review.updateComments,d.review.timer_duration.chat);e.skin.registerCleanCallback(d.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(d.review.timer);d.review.timer=setInterval(d.review.updateComments,d.review.timer_duration.general)},updateComments:function(){var f=c(".badge",'nav a[href="#review-comments.html"]'),h;var g=c(".chat-body",".widget-body");if(g.exists()){h=c(".message",g).length}else{h=parseInt(f.text())}e.ajax.post("get-last-review-comments.json",{count:h},function(i){if(g.exists()){f.removeClass("bg-color-danger").addClass("bg-color-info")}if(h!==i.count){f.text(i.count).removeClass("hidden");if(g.exists()){c(".messages",g).append(i.content);g.animate({scrollTop:g[0].scrollHeight},1000)}if(!g.exists()){f.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){c(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")})}}})},initCommentData:function(f){var g=c(".chat-body",".widget-body");return{count:c(".message",g).length}},addCommentAction:function(){return function(){c('textarea[name="comment"]').focus()}},addCommentCallback:function(g){var h=c(this);var i=h.parents(".widget-body");c(".messages",i).append(g.content);c('textarea[name="comment"]',h).val("");var f=c(".chat-body",i);f.animate({scrollTop:f[0].scrollHeight},1000);c(".badge",'nav a[href="#review-comments.html"]').text(g.count).removeClass("hidden")}}};b.PyAMS_content=d;c(b.document).on("PyAMS_content.changed_item",function(g,f){e.executeFunctionByName(f.handler,document,f)});var a=c(".badge",'nav a[href="#review-comments.html"]');if(a.exists()){d.review.timer=setInterval(d.review.updateComments,d.review.timer_duration.general)}})(jQuery,this);
\ No newline at end of file
+!function(a,e){"use strict";var t=e.MyAMS,s={refreshForm:function(e){var s=a("#"+e.object_name);return s.replaceWith(a(e.form)),s=a("#"+e.object_name),t.initContent(s),s},refreshTable:function(e){var s=a("#"+e.object_name).parent(".ams-widget");return s.replaceWith(a(e.table)),s=a("#"+e.object_name).parent(".ams-widget"),t.initContent(s),s},TinyMCE:{initEditor:function(a){return a.image_list=s.TinyMCE.getImagesList,a.link_list=s.TinyMCE.getLinksList,a},getImagesList:function(e){var s=a(document.activeElement).parents("form");if(s.exists()){var n=s.attr("data-ams-form-handler")||s.attr("action"),i=n.substr(0,n.lastIndexOf("/")+1);return t.ajax.post(i+"get-images-list.json",{},e)}},getLinksList:function(e){var s=a(document.activeElement).parents("form");if(s.exists()){var n=s.attr("data-ams-form-handler")||s.attr("action"),i=n.substr(0,n.lastIndexOf("/")+1);return t.ajax.post(i+"get-links-list.json",{},e)}}},profile:{switchFavorite:function(){var e=a(this),s=e.data("sequence-oid");t.ajax.post("switch-user-favorite.json",{oid:s},function(a,t){a.favorite?e.removeClass("fa-star-o").addClass("fa-star"):e.removeClass("fa-star").addClass("fa-star-o")})}},galleries:{updateImageTitle:function(e){a('img[id="'+e.image_id+'"]').attr("original-title",e.title)},switchImageVisibility:function(e){return function(){var e=a(this),s=e.parents(".image"),n=s.parents(".gallery");t.ajax.post(n.data("ams-location")+"/set-image-visibility.json",{object_name:s.data("ams-element-name")},function(t,s){t.visible?(a("i",e).attr("class","fa fa-fw fa-eye"),e.parents(".btn-group").siblings("a.fancyimg").removeClass("not-visible")):(a("i",e).attr("class","fa fa-fw fa-eye-slash text-danger"),e.parents(".btn-group").siblings("a.fancyimg").addClass("not-visible"))})}},setOrder:function(e,s){if(!s||!s.item.hasClass("already-dropped")){var n=s.item.parents(".gallery"),i=a(".image",n).listattr("data-ams-element-name");t.ajax.post(n.data("ams-location")+"/set-images-order.json",{images:JSON.stringify(i)})}},removeFile:function(e){return function(){var e=a(this);t.skin.bigBox({title:t.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+t.i18n.DELETE_WARNING,buttons:t.i18n.BTN_OK_CANCEL},function(a){if(a===t.i18n.BTN_OK){var s=e.parents(".gallery").data("ams-location"),n=e.parents(".image"),i=n.data("ams-element-name");t.ajax.post(s+"/delete-element.json",{object_name:i},function(a,e){n.remove()})}})}},afterFancyboxLoad:function(a,e){a.element.hasClass("not-visible")&&a.inner.prepend('<div class="hidden-mask"></div>')}},imgmap:{init:function(){var e=a(this);t.ajax.check(a.fn.canvasAreaDraw,"/--static--/pyams_content/js/jquery-canvasAreaDraw"+t.devext+".js",function(){e.canvasAreaDraw({imageUrl:e.data("ams-image-url")})})},initSummary:function(){var e=a(this);t.ajax.check(a.fn.mapster,"/--static--/pyams_content/js/jquery-imagemapster-1.2.10"+t.devext+".js",function(){e.mapster({fillColor:"ff0000",fillOpacity:.35,selected:!0,highlight:!0,staticState:!0})})}},associations:{refreshAssociations:function(a){var e=s.refreshTable(a).siblings("legend");e.parents("fieldset:first").hasClass("switched")&&e.click()},switchVisibility:function(e){return function(){var e=a(this),s=e.parents("tr"),n=s.parents("table");t.ajax.post(n.data("ams-location")+"/set-association-visibility.json",{object_name:s.data("ams-element-name")},function(t,s){t.visible?a("i",e).attr("class","fa fa-fw fa-eye"):a("i",e).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},types:{switchSubtypes:function(e){var s=a(this),n=a("i.switch",s),i=s.parents("td"),r=a(".subtypes",i),o=s.parents("tr");if(n.hasClass("fa-plus-square-o")){var l=o.parents("table");r.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>'),t.ajax.post(l.data("ams-location")+"/get-subtypes-table.json",{object_name:o.data("ams-element-name")},function(a){r.html(a),a&&(t.initContent(r),n.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"))})}else t.skin.cleanContainer(r),r.empty(),n.removeClass("fa-minus-square-o").addClass("fa-plus-square-o")},refreshSubtypes:function(a){s.refreshTable(a)}},paragraphs:{preReload:function(){s.paragraphs.switched=a("i.switch.fa-minus-square-o","#paragraphs_list").parents("tr").listattr("id")},postReload:function(){a(s.paragraphs.switched).each(function(){a("i.switch.fa-plus-square-o",'[id="'+this+'"]').parents("div:first").click()}),delete s.paragraphs.switched},switchVisibility:function(e){return function(){var e=a(this),s=e.parents("tr"),n=s.parents("table");t.ajax.post(n.data("ams-location")+"/set-paragraph-visibility.json",{object_name:s.data("ams-element-name")},function(t,s){t.visible?a("i",e).attr("class","fa fa-fw fa-eye"):a("i",e).attr("class","fa fa-fw fa-eye-slash text-danger")})}},refreshParagraphs:function(a){var e=s.refreshTable(a).siblings("legend");e.parents("fieldset:first").hasClass("switched")&&e.click()},refreshParagraph:function(e){var t=a('table[id="paragraphs_list"]'),s=a('tr[data-ams-element-name="'+e.object_name+'"]',t);a("span.title",s).html(e.title||" - - - - - - - -")},switchEditor:function(e){var s=a(this),n=a("i.switch",s),i=s.parents("td"),r=a(".editor",i),o=s.parents("tr");if(n.hasClass("fa-plus-square-o")){var l=o.parents("table");r.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>'),t.ajax.post(l.data("ams-location")+"/get-paragraph-editor.json",{object_name:o.data("ams-element-name")},function(a){r.html(a),a&&(t.initContent(r),n.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),o.data("ams-disabled-handlers",!0))})}else t.skin.cleanContainer(r),r.empty(),n.removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),o.removeData("ams-disabled-handlers")},switchAllEditors:function(e){var s=a(this),n=a("i",s),i=s.parents("table");n.hasClass("fa-plus-square-o")?(n.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin"),t.ajax.post(i.data("ams-location")+"/get-paragraphs-editors.json",{},function(e){for(var s in e)if(e.hasOwnProperty(s)){var r=a('tr[data-ams-element-name="'+s+'"]',i),o=a(".editor",r);o.is(":empty")&&(o.html(e[s]),t.initContent(o)),a(".fa-plus-square-o",r).removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),r.data("ams-disabled-handlers",!0)}a("i.fa-plus-square-o",a("tbody",i)).exists()||n.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o")})):(a(".editor",i).each(function(){t.skin.cleanContainer(a(this)),a(this).empty()}),a(".fa-minus-square-o",i).removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),a("tr",i).removeData("ams-disabled-handlers"))},updateToolbar:function(e){var s=a('table[id="paragraphs_list"]'),n=a('tr[data-ams-element-name="'+e.object_name+'"]',s),i=a(".title-toolbar",n);i.replaceWith(e.toolbar_tag),i=a(".title-toolbar",n),t.initContent(i)},updateMarkers:function(e){var s=a('table[id="paragraphs_list"]'),n=a('tr[data-ams-element-name="'+e.object_name+'"]',s),i=a(".title-toolbar",n),r=a("DIV.action."+e.marker_type,i);r.exists()?r.replaceWith(e.marker_tag):a(e.marker_tag).appendTo(i),e.marker_tag&&(r=a("DIV.action."+e.marker_type,i),t.initContent(r)),t.helpers.sort(i,"weight")}},illustration:{addIllustration:function(){var e=a(this),t=e.parents(".btn-group").siblings("legend.switcher");a("i.fa-plus",t).click(),e.hide()}},fields:{refreshField:function(e){var t=a('table[id="form_fields_list"]'),s=a('tr[data-ams-element-name="'+e.object_name+'"]',t);a("td:nth-child(4)",s).html(e.title)},switchVisibility:function(e){return function(){var e=a(this),s=e.parents("tr"),n=s.parents("table");t.ajax.post(n.data("ams-location")+"/set-form-field-visibility.json",{object_name:s.data("ams-element-name")},function(t,s){t.visible?a("i",e).attr("class","fa fa-fw fa-eye"):a("i",e).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},themes:{initExtracts:function(e){var s=a('select[name="form.widgets.thesaurus_name:list"]',e).val(),n=a('select[name="form.widgets.extract_name:list"]',e),i=n.val();s&&t.jsonrpc.post("getExtracts",{thesaurus_name:s},{url:"/api/thesaurus/json"},function(e){n.empty(),a(e.result).each(function(){a("<option></option>").attr("value",this.id).attr("selected",this.id===i).text(this.text).appendTo(n)})}),n.attr("data-ams-events-handlers",'{"select2-open": "PyAMS_content.themes.getExtracts"}')},getExtracts:function(e){var s=a(e.currentTarget).parents("form"),n=a('select[name="form.widgets.thesaurus_name:list"]',s).val();n&&t.jsonrpc.post("getExtracts",{thesaurus_name:n},{url:"/api/thesaurus/json"},function(e){var t=a('select[name="form.widgets.extract_name:list"]',s).data("select2");t.results.empty(),t.opts.populateResults.call(t,t.results,e.result,{term:""})})}},review:{timer:null,timer_duration:{general:3e4,chat:5e3},initComments:function(e){var n=a(".chat-body",e);n.animate({scrollTop:n[0].scrollHeight},1e3),clearInterval(s.review.timer),s.review.timer=setInterval(s.review.updateComments,s.review.timer_duration.chat),t.skin.registerCleanCallback(s.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(s.review.timer),s.review.timer=setInterval(s.review.updateComments,s.review.timer_duration.general)},updateComments:function(){var e,s=a(".badge",'nav a[href="#review-comments.html"]'),n=a(".chat-body",".widget-body");e=n.exists()?a(".message",n).length:parseInt(s.text()),t.ajax.post("get-last-review-comments.json",{count:e},function(t){n.exists()&&s.removeClass("bg-color-danger").addClass("bg-color-info"),e!==t.count&&(s.text(t.count).removeClass("hidden"),n.exists()&&(a(".messages",n).append(t.content),n.animate({scrollTop:n[0].scrollHeight},1e3)),n.exists()||s.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){a(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")}))})},initCommentData:function(e){var t=a(".chat-body",".widget-body");return{count:a(".message",t).length}},addCommentAction:function(){return function(){a('textarea[name="comment"]').focus()}},addCommentCallback:function(e){var t=a(this),s=t.parents(".widget-body");a(".messages",s).append(e.content),a('textarea[name="comment"]',t).val("");var n=a(".chat-body",s);n.animate({scrollTop:n[0].scrollHeight},1e3),a(".badge",'nav a[href="#review-comments.html"]').text(e.count).removeClass("hidden")}}};e.PyAMS_content=s,a(e.document).on("PyAMS_content.changed_item",function(a,e){t.executeFunctionByName(e.handler,document,e)}),a(".badge",'nav a[href="#review-comments.html"]').exists()&&(s.review.timer=setInterval(s.review.updateComments,s.review.timer_duration.general))}(jQuery,this);