# HG changeset patch # User Thierry Florac # Date 1466418303 -7200 # Node ID bcb01961928e2ebfcc726a1c4921cf610a9bc62c # Parent 48aa4de90de291a8abee9697b423b42af4e42154 Added base blog management classes diff -r 48aa4de90de2 -r bcb01961928e src/pyams_content/shared/blog/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/blog/__init__.py Mon Jun 20 12:25:03 2016 +0200 @@ -0,0 +1,46 @@ +# +# Copyright (c) 2008-2016 Thierry Florac +# 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.gallery.interfaces import IGalleryContainerTarget +from pyams_content.component.links.interfaces import ILinkContainerTarget +from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget +from pyams_content.component.theme.interfaces import IThemesTarget +from pyams_content.shared.blog.interfaces import IWfBlogPost, BLOG_CONTENT_TYPE, BLOG_CONTENT_NAME, IBlogPost + +# import packages +from pyams_content.shared.common import WfSharedContent, register_content_type, SharedContent +from zope.interface import implementer + + +@implementer(IWfBlogPost, IParagraphContainerTarget, IThemesTarget, IExtFileContainerTarget, ILinkContainerTarget, + IGalleryContainerTarget) +class WfBlogPost(WfSharedContent): + """Base blog post""" + + content_type = BLOG_CONTENT_TYPE + content_name = BLOG_CONTENT_NAME + +register_content_type(WfBlogPost) + + +@implementer(IBlogPost) +class BlogPost(SharedContent): + """Worfklow managed blog post class""" + + content_class = WfBlogPost diff -r 48aa4de90de2 -r bcb01961928e src/pyams_content/shared/blog/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/blog/interfaces/__init__.py Mon Jun 20 12:25:03 2016 +0200 @@ -0,0 +1,44 @@ +# +# Copyright (c) 2008-2016 Thierry Florac +# 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 ISharedSite, ISharedTool, IWfSharedContent, ISharedContent +from zope.container.interfaces import IContainer + +# import packages + +from pyams_content import _ + + +BLOG_CONTENT_TYPE = 'blog' +BLOG_CONTENT_NAME = _("Blog post") + + +class IBlogManager(ISharedSite, ISharedTool): + """Blog manager interface""" + + +class IWfBlogPost(IWfSharedContent): + """Blog topic interface""" + + +class IBlogFolder(IContainer): + """Blog folder interface""" + + +class IBlogPost(ISharedContent): + """Workflow managed blog post interface""" diff -r 48aa4de90de2 -r bcb01961928e src/pyams_content/shared/blog/manager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/blog/manager.py Mon Jun 20 12:25:03 2016 +0200 @@ -0,0 +1,62 @@ +# +# Copyright (c) 2008-2016 Thierry Florac +# 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 IThemesManagerTarget +from pyams_content.shared.blog.interfaces import IBlogManager, BLOG_CONTENT_TYPE, IBlogFolder +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.component.interfaces import ISite +from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent + +# import packages +from pyams_content.shared.blog import BlogPost +from pyams_content.shared.common.manager import SharedTool +from pyams_utils.traversing import get_parent +from pyramid.events import subscriber +from zope.container.folder import Folder +from zope.interface import implementer + + +@implementer(IBlogFolder) +class BlogFolder(Folder): + """Blog folder class""" + + +@implementer(IBlogManager, IThemesManagerTarget, IAttributeAnnotatable) +class BlogManager(SharedTool): + """Nlog manager class""" + + shared_content_type = BLOG_CONTENT_TYPE + shared_content_factory = BlogPost + + +@subscriber(IObjectAddedEvent, context_selector=IBlogManager) +def handle_added_blog_manager(event): + """Register blog manager when added""" + site = get_parent(event.newParent, ISite) + registry = site.getSiteManager() + if registry is not None: + registry.registerUtility(event.object, IBlogManager, name=event.object.__name__) + + +@subscriber(IObjectRemovedEvent, context_selector=IBlogManager) +def handle_deleted_blog_manager(event): + """Un-register blog manager when deleted""" + site = get_parent(event.oldParent, ISite) + registry = site.getSiteManager() + if registry is not None: + registry.unregisterUtility(event.object, IBlogManager, name=event.object.__name__) diff -r 48aa4de90de2 -r bcb01961928e src/pyams_content/shared/blog/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/blog/zmi/__init__.py Mon Jun 20 12:25:03 2016 +0200 @@ -0,0 +1,118 @@ +# +# Copyright (c) 2008-2016 Thierry Florac +# 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 +from datetime import datetime + +# import interfaces +from pyams_content.interfaces import CREATE_CONTENT_PERMISSION +from pyams_content.shared.blog.interfaces import IWfBlogPost, IBlogManager +from pyams_i18n.interfaces import II18n, II18nManager +from pyams_skin.interfaces.viewlet import IMenuHeader, IWidgetTitleViewletManager +from pyams_skin.layer import IPyAMSLayer +from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowInfo +from pyams_zmi.interfaces.menu import IContentManagementMenu +from pyams_zmi.layer import IAdminLayer + +# import packages +from pyams_content.shared.blog.manager import BlogFolder +from pyams_content.shared.common.zmi import SharedContentAddForm, SharedContentAJAXAddForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.interfaces import IContentTitle +from pyams_skin.viewlet.toolbar import ToolbarAction +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextRequestAdapter +from pyams_utils.unicode import translate_string +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config +from pyramid.view import view_config +from zope.interface import Interface +from zope.lifecycleevent import ObjectCreatedEvent + +from pyams_content import _ + + +@adapter_config(context=(IWfBlogPost, IContentManagementMenu), provides=IMenuHeader) +class BlogPostContentMenuHeader(ContextRequestAdapter): + """Blog post content menu header adapter""" + + header = _("This blog post") + + +@adapter_config(context=(IWfBlogPost, IPyAMSLayer, Interface), provides=IContentTitle) +class BlogPostTitleAdapter(ContextRequestViewAdapter): + """Blog post title adapter""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Blog post « {title} »")).format( + title=II18n(self.context).query_attribute('title', request=self.request)) + + +@viewlet_config(name='add-shared-content.action', context=IBlogManager, layer=IAdminLayer, view=Interface, + manager=IWidgetTitleViewletManager, permission=CREATE_CONTENT_PERMISSION, weight=1) +class BlogPostAddAction(ToolbarAction): + """Blog post adding action""" + + label = _("Add blog post") + url = 'add-shared-content.html' + modal_target = True + + +@pagelet_config(name='add-shared-content.html', context=IBlogManager, layer=IPyAMSLayer, + permission=CREATE_CONTENT_PERMISSION) +class BlogPostAddForm(SharedContentAddForm): + """Blog post add form""" + + legend = _("Add blog post") + content_url = None + + def add(self, wf_content): + # create shared content + content = self.context.shared_content_factory() + self.request.registry.notify(ObjectCreatedEvent(content)) + # check blog folders + now = datetime.utcnow() + year, month = now.strftime('%Y:%m').split(':') + year_folder = self.context.get(year) + if year_folder is None: + year_folder = self.context[year] = BlogFolder() + month_folder = year_folder.get(month) + if month_folder is None: + month_folder = year_folder[month] = BlogFolder() + # check title language + added = False + i18n_manager = II18nManager(self.context) + for lang in i18n_manager.get_languages(): + title = translate_string(wf_content.title.get(lang)) + if title: + content_name = translate_string(title, force_lower=True, spaces='-') + month_folder[content_name] = content + added = True + break + # handle workflow + if added: + IWorkflowVersions(content).add_version(wf_content, None) + IWorkflowInfo(wf_content).fire_transition('init') + self.content_url = absolute_url(wf_content, self.request, 'admin.html') + + def nextURL(self): + return self.content_url + + +@view_config(name='add-shared-content.json', context=IBlogManager, request_type=IPyAMSLayer, + permission=CREATE_CONTENT_PERMISSION, renderer='json', xhr=True) +class BlogPostAJAXAddForm(SharedContentAJAXAddForm, BlogPostAddForm): + """Blog post add form, JSON renderer""" diff -r 48aa4de90de2 -r bcb01961928e src/pyams_content/shared/blog/zmi/manager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/blog/zmi/manager.py Mon Jun 20 12:25:03 2016 +0200 @@ -0,0 +1,127 @@ +# +# Copyright (c) 2008-2016 Thierry Florac +# 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 +from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION +from pyams_content.root.interfaces import ISiteRoot +from pyams_content.shared.blog.interfaces import IBlogManager +from pyams_content.zmi.interfaces import IUserAddingsMenuLabel +from pyams_i18n.interfaces import II18n, INegotiator +from pyams_skin.interfaces.container import ITableElementEditor +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from pyams_zmi.layer import IAdminLayer + +# import interfaces +from z3c.form.interfaces import IDataExtractedEvent + +# import packages +from pyams_content.shared.blog.manager import BlogManager +from pyams_content.shared.zmi.sites import SiteTreeTable +from pyams_form.form import AJAXAddForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.table import DefaultElementEditorAdapter +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_utils.adapter import adapter_config, ContextRequestAdapter +from pyams_utils.registry import query_utility +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 +from pyramid.events import subscriber +from pyramid.view import view_config +from z3c.form import field +from zope.interface import Invalid + +from pyams_content import _ + + +@adapter_config(context=(IBlogManager, IAdminLayer), provides=IUserAddingsMenuLabel) +class BlogManagerUserAddingsMenuLabelAdapter(ContextRequestAdapter): + """Blog manager user addings menu label adapter""" + + @property + def label(self): + return '{content} ({blog})'.format( + content=self.request.localizer.translate(self.context.shared_content_factory.content_class.content_name), + blog=II18n(self.context).query_attribute('title', request=self.request)) + + +@viewlet_config(name='add-blog-manager.menu', context=ISiteRoot, layer=IAdminLayer, + view=SiteTreeTable, manager=IToolbarAddingMenu, permission=MANAGE_SITE_ROOT_PERMISSION) +class BlogManagerAddMenu(ToolbarMenuItem): + """Blog manager add menu""" + + label = _("Add blog manager") + label_css_class = 'fa fa-fw fa-tags' + url = 'add-blog-manager.html' + modal_target = True + + +@pagelet_config(name='add-blog-manager.html', context=ISiteRoot, layer=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION) +class BlogManagerAddForm(AdminDialogAddForm): + """Blog manager add form""" + + title = _("Blog manager") + legend = _("Add blog manager") + icon_css_class = 'fa fa-fw fa-tags' + + fields = field.Fields(IBlogManager).select('title', 'short_name') + ajax_handler = 'add-blog-manager.json' + edit_permission = None + + def create(self, data): + return BlogManager() + + def add(self, object): + short_name = II18n(object).query_attribute('short_name', request=self.request) + name = translate_string(short_name, force_lower=True, spaces='-') + self.context[name] = object + + def nextURL(self): + return absolute_url(self.context, self.request, 'site-tree.html') + + +@subscriber(IDataExtractedEvent, form_selector=BlogManagerAddForm) +def handle_new_blog_manager_data_extraction(event): + """Handle new blog manager data extraction""" + container = event.form.context + negotiator = query_utility(INegotiator) + short_name = event.data['short_name'].get(negotiator.server_language) + if not short_name: + event.form.widgets.errors += (Invalid(_("You must provide a short name for default server language!")),) + return + name = translate_string(short_name, force_lower=True, spaces='-') + if name in container: + event.form.widgets.errors += (Invalid(_("Specified blog manager name is already used!")),) + return + blog = query_utility(IBlogManager, name=short_name) + if blog is not None: + event.form.widgets.errors += (Invalid(_("A blog manager is already registered with this name!!")),) + + +@view_config(name='add-blog-manager.json', context=ISiteRoot, request_type=IPyAMSLayer, + permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True) +class BlogManagerAJAXAddForm(AJAXAddForm, BlogManagerAddForm): + """Blog manager add form, JSON renderer""" + + +@adapter_config(context=(IBlogManager, IAdminLayer, SiteTreeTable), provides=ITableElementEditor) +class SiteTreeTableElementEditor(DefaultElementEditorAdapter): + """Site tree table element editor""" + + view_name = 'admin.html' + modal_target = False