I'm attempting to make an add-on that, among other things, adds another option to the personal preferences page. I've tried to piece together how to do this properly from various guides on loosely related topics, but I've had no luck. I'm just getting into Plone development, and a lot of this is somewhat foreign to me, so please be kind :)
I'm doing all this in Plone 4.3
When I have the add-on enabled on the site, it doesn't give me any errors, but the extra field isn't included on the preferences page.
So far, I've got something like this, ignore the odd naming scheme. Again, this was pieced together from other guides, and I wanted to get it working before I refactored.
userdataschema.py
from plone.app.users.browser.personalpreferences import IPersonalPreferences
from zope.interface import Interface
from zope import schema
class IEnhancedUserDataSchema(IPersonalPreferences):
""" Use all the fields from the default user data schema, and add various
extra fields.
"""
buttonsEnabled = schema.Bool(title=u'Transition button widget.',
default=True,
description=u'Uncheck to remove the transition button box from ALL pages.',
required=False
)
adapter.py:
from plone.app.users.browser.personalpreferences import PersonalPreferencesPanelAdapter
from app.statebuttons.userdataschema import IEnhancedUserDataSchema
from zope.interface import implements
class EnhancedUserDataPanelAdapter(PersonalPreferencesPanelAdapter):
"""
"""
implements(IEnhancedUserDataSchema)
def get_buttonEnabled(self):
return self.context.getProperty('buttonsEnabled', '')
def set_buttonsEnabled(self, value):
return self.context.setMemberProperties({'buttonsEnabled': value})
buttonsEnabled = property(get_buttonEnabled, set_buttonsEnabled)
overrides.zcml:
<configure
xmlns="http://namespaces.zope.org/zope"
i18n_domain="collective.examples.userdata">
<adapter
provides=".userdataschema.IEnhancedUserDataSchema"
for="Products.CMFCore.interfaces.ISiteRoot"
factory=".adapter.EnhancedUserDataPanelAdapter"
/>
</configure>
If anyone could give me some input on what I'm doing wrong, that would be awesome. If I'm way off the mark, I'd appreciate some input on where to go next.
You can make your new field show up on ##personal-preferences by adding this to your existing code:
browser/configure.zcml
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">
<browser:page
for="plone.app.layout.navigation.interfaces.INavigationRoot"
name="personal-preferences"
class=".personalpreferences.CustomPersonalPreferencesPanel"
permission="cmf.SetOwnProperties"
layer="..interfaces.IAppThemeLayer"
/>
</configure>
browser/personalpreferences.py
from plone.app.users.browser.personalpreferences import PersonalPreferencesPanel
from plone.app.users.browser.personalpreferences import LanguageWidget
from plone.app.users.browser.personalpreferences import WysiwygEditorWidget
from zope.formlib import form
from app.statebuttons.userdataschema import IEnhancedUserDataSchema
class CustomPersonalPreferencesPanel(PersonalPreferencesPanel):
form_fields = form.FormFields(IEnhancedUserDataSchema)
# Apply same widget overrides as in the base class
form_fields['language'].custom_widget = LanguageWidget
form_fields['wysiwyg_editor'].custom_widget = WysiwygEditorWidget
Note that you'll need a browser layer IAppThemeLayer registered in profiles/default/browserlayer.xml, and to define storage for your field in profiles/default/memberdata_properties.xml.
You can see why this is necessary in plone.app.users.browser.personalpreferences. IPersonalPreferences is looked up as follows:
class PersonalPreferencesPanel(AccountPanelForm):
...
form_fields = form.FormFields(IPersonalPreferences)
...
The schema is bound to the IPersonalPreferences interface. You have to change the control panel in order to change the looked-up schema.
Instead you could use the IUserDataSchema approach described in https://pypi.python.org/pypi/collective.examples.userdata but as you've seen in order to override ##personal-information you need to use overrides.zcml which is an if-all-else-fails kind of override and not a very welcome citizen in a third party add-on.
Others may disagree but my personal preference (should I say my IPersonalPreference?) for this kind of problem is to create a dedicated form for the setting(s) and link to it from e.g. the personal tools menu.
overwriting personal-preferences should not be necessary.
just follow the documentation on https://pypi.python.org/pypi/plone.app.users
seems you're missing:
a userdatascheaprovider
from plone.app.users.userdataschema import IUserDataSchemaProvider
class UserDataSchemaProvider(object):
implements(IUserDataSchemaProvider)
def getSchema(self):
"""
"""
return IEnhancedUserDataSchema
the GS configuration for it in componentregistry.xml:
a registration of your new field in GS properties.xml to make the field show up on the registration page (optionally)
<object name="site_properties" meta_type="Plone Property Sheet">
<property name="user_registration_fields" type="lines" purge="False">
<element value="yourfield" />
</property>
</object>
to make your field show up in personal preferences follow the documentation on https://pypi.python.org/pypi/plone.app.users/1.1.5
section "How to update the personal information form"
Related
I am trying to build a form package for a Plone website. I am currently working with Plone 4.3. Before I was using Dexterity with five.grok and grok libraries. But after reading the Plone 4.3 migration and five.grok dependency section of this article: http://developer.plone.org/components/grok.html it appears that Plone developers are moving away from using grok all together.
So should I move away from using Grok and how would I go about doing so when all the current documentation is currently using Grok? Additionally I am developing from a Windows based machine.
First creating form without grok is not that hard and do not depends on your Operating System.
Creating a form is always the same. Here is how I proceed:
Some imports
from Products.Five.browser import BrowserView
from plone.autoform.form import AutoExtensibleForm
from plone.app.z3cform import layout
from zope import interface
from zope import schema
from zope import component
from z3c.form import form
from collective.my.i18n import _
Create a schema
class AddFormSchema(interface.Interface):
what = schema.Choice(
title=_(u"What"),
vocabulary="plone.app.vocabularies.UserFriendlyTypes"
)
where = schema.Choice(
title=u"Where",
vocabulary="collective.my.vocabulary.groups"
)
create a generic adapter to fill the form from anywhere
class AddFormAdapter(object):
interface.implements(AddFormSchema)
component.adapts(interface.Interface)
def __init__(self, context):
self.what = None
self.where = None
Then write the form
class AddForm(AutoExtensibleForm, form.Form):
schema = AddFormSchema
form_name = 'add_content'
Add a view
class AddButton(layout.FormWrapper):
"""Add button"""
form = AddForm
Now ZCML time this is the step you don't need when using grok:
<adapter factory=".my.AddFormAdapter"/>
<browser:page
for="*"
name="my.addbutton"
class=".my.AddButton"
template="addbutton.pt"
permission="zope2.View"
/>
Should you move from grok:
This is depending of what are you doing. For an addon I say Yes but for a project, it's up to you.
Grok is not parts of the already big Zope. So adding dependency is something that always should be done only if needed. Grok is an option so I have never used it.
I have a dexterity content type in Plone 4.2.4, that uses the contenttree widget to manage referenced objects, but only in the editForm.
I realized, that the referenced items had to be external_visible to get displayed by the widget, which means that anonymous users could View and AccessContentsInformation. Thats not what I wanted. So I dug in the contenttree widget source and added the following to my products browser/configure.zcml
<include package="Products.CMFCore" file="permissions.zcml"
zcml:condition="installed plone.app.upgrade" />
<browser:page
for="*"
name="contenttree-fetch"
class="my.product.content.bikemetamodel.EditForm"
permission="cmf.ModifyPortalContent"
/>
<adapter factory="my.product.browser.widgets.MetamodellContenttreeAdapter" />
and an adapter
class MetamodellContenttreeAdapter(object):
implements(IBikeMetaModel)
adapts(Interface)
def __init__(self, context):
self.context = context
def _get_allowed_modeltypes(self):
return None
def _set_allowed_modeltypes(self, value):
print "setting", value
allowed_modeltypes = property(_get_allowed_modeltypes, _set_allowed_modeltypes)
[...]
But this seems to be not enough. The underlying catalog search returns no result, If the permissions are set to deny View and AccessContentsInformation to anonymous users. So I guess, I must use the view permission to construct some kind of proxy user.
Would it be ok, to use a SecurityManager in the newly created view to fetch the results as a different user? Or am I just missing something?
Ok, here is how I solved the mystery.
After digging around a while, I realized, that I missed the point with my previous idea to override the ##contenttree-fetch view. The solution I came up with is quite simple and seems elegant (enough) for me. I do a sudo style sidestep now, to gather the required items.
Class EditForm(dexterity.EditForm):
grok.context(IBikeMetaModel)
# If it would be another than the edit view, we could manage
# permisssions here. Not neccessary in edit view, because the
# edit permission defined in this content types *.xml counts
# grok.require("cmf.ModifyPortalContent")
#property
def acl_users(self):
return getToolByName(getSite(), 'acl_users')
def updateWidgets(self):
# This is the magic. A sudo style sidestep to a user
# with the id "system" that has permission to gather
# the required lists in the updateWidgets function of
# the base class
proxy_user = self.acl_users.getUserById("system")
oUser = getSecurityManager()
newSecurityManager(self.request, proxy_user)
super(EditForm, self).updateWidgets()
# custom widget updates
self.widgets['title'].mode = DISPLAY_MODE
self.widgets['year'].mode = HIDDEN_MODE
self.widgets['brand'].mode = HIDDEN_MODE
self.widgets['model'].mode = HIDDEN_MODE
# Very Important! Switch back to the original user.
setSecurityManager(oUser)
I have been working on a dexterity based plone application.
I have created a couple of new types. This is what I did to activate comments on a specific dexterity content type named "activity_report":
In Plone Control Panel
In the Discussion section I enabled the following:
globally enable comments
enable anonymous comments
In the Types Section
I chose the "Activity Report" type from the drop down and enabled the "Allow comments" option.
On the file system
In the FTI file activityreport.xml:
<property name="allow_discussion">True</property>
I have restarted the instance and even reinstalled the product, but I can not activate the comments section in the dexterity type.
It is worth mentioning that a standard type (ex. Page) can have the discussion module activated.
Is there something I am missing?
plone.app.discussion currently disables commenting for all containers (see https://dev.plone.org/ticket/11245 for discussion).
I used a monkey patch like the following in one project to short-circuit the normal check and make sure that commenting was enabled for my folderish content type:
from Acquisition import aq_inner
from Products.highcountrynews.content.interfaces import IHCNNewsArticle
from plone.app.discussion.conversation import Conversation
old_enabled = Conversation.enabled
def enabled(self):
parent = aq_inner(self.__parent__)
if parent.portal_type == 'my_portal_type':
return True
return old_enabled(self)
Conversation.enabled = enabled
where 'my_portal_type' is, of course, the portal_type you want commenting enabled for.
The David response is not accurate. The class to be monkeypatched is plone.app.discussion.browser.conversation.ConversationView :
from Acquisition import aq_inner
from plone.app.discussion.browser.conversation import ConversationView
old_enabled = ConversationView.enabled
def enabled(self):
parent = aq_inner(self.__parent__)
if parent.portal_type == 'My_type':
return True
return old_enabled(self)
It works for Plone 4.2 at least. However, thanks David for the hint.
As David and Victor already pointed out, you can just override the enable method of the conversation class. I would recommend using the following approach which is a bit cleaner than monkey patching the conversation class:
https://github.com/plone/plone.app.discussion/blob/master/docs/source/howtos/howto_override_enable_conversation.txt
I also added support for dexterity types to plone.app.discussion recently, so as soon as there is a new release you won't need to customize the conversation class any longer:
https://github.com/plone/plone.app.discussion/commit/0e587a7d8536125acdd3bd385e880b60d6aec28e
Note that this method supports commenting ON folderish objects. There is no support to enable/disable commenting for objects INSIDE a folderish object yet.
In case you want to be able to switch on/off commenting with a behavior field/widget:
https://github.com/plone/plone.app.dexterity/commit/0573df4f265a39da9efae44e605e3815729457d7
This will hopefully make it into the next plone.app.dexterity release as well.
I solved in configure.zcml:
<interface interface="Products.CMFPlone.interfaces.INonStructuralFolder" />
<class class="Products.PloneHelpCenter.types.Definition.HelpCenterDefinition">
<implements interface="Products.CMFPlone.interfaces.INonStructuralFolder" />
</class>
UPDATE: this is not a good idea. I had problems with missing Add menu for each content type having this fix.
I am trying to implement a basic Zope2 content type directly without using dexterity or Archetypes because I need this to be extremely lean.
from OFS.SimpleItem import SimpleItem
from Products.ZCatalog.CatalogPathAwareness import CatalogAware
from persistent.list import PersistentList
class Doculite(SimpleItem, CatalogAware):
""" implement our class """
meta_type = 'Doculite'
def __init__(self, id, title="No title", desc=''):
self.id = id
self.title = title
self.desc = desc
self.tags = PersistentList()
self.default_catalog = 'portal_catalog'
def add_tags(self, tags):
self.tags.extend(tags)
def Subject(self):
return self.tags
def indexObject(self):
self.reindex_object()
From an external method I am doing this:
def doit(self):
pc = self.portal_catalog
res1 = pc.searchResults()
o1 = self['doc1']
o1.add_tags(['test1', 'test2'])
o1.reindex_object()
res2 = pc.searchResults()
return 'Done'
I clear the catalog and run my external method. My object does not get into the catalog. But from the indexes tab, when I browse the Subject index, I can see my content item listed with the values. Both res1 and res2 and empty.
Why is my content item not showing up inside the searchResuts() of the catalog?
Plone is a full-fat content management system, if you're after something lean it's probably not the right choice (perhaps try Pyramid.)
For your content type to be a full part of a Plone site it has to fulfil a number of requirements across the Zope2, CMF and Plone layers. plone.app.content.item.Item is about the simplest base class you can get for a content item for a Plone site, though a simpler base class in itself will not really make instances of your content type any more 'lean' - an instance of a class in Python is basically just a dict and a pointer to it's class.
Most of the work on a page view will be rendering the various user interface features of a site. Rendering the schema based add/edit forms of frameworks like Archetypes and Dexterity is also relatively expensive.
I'd spend a little time profiling your application using one of the supported content type systems before putting time into building your own.
In order to see your objects in the "Catalog" tab of the portal_catalog your objects need to have a "getPhysicalPath()" method that returns a tuple representing their path (ex. ('','Plone','myobject')).
Also try to use this:
from Products.CMFCore.CMFCatalogAware import CMFCatalogAware
as base class.
You need to register your type with the catalog multiplexer. Look at the configuration in the zmi -> archetypes_tool.
I'm not sure, but you may also need a portal_type registration also...
Like Lawrence said though, you're better off just using one of the current content type frameworks if you want to be able to catalog your data with plone's portal catalog. If you can deal with a separate catalog, take a look at repoze.catalog.
Plone needs every content object to provide an "allowedRolesAndUsers" index to return the object in searchResults.
There is probably a zcml snippet that will enable this for my content type. But I was able to get things working by adding another method as follows:
def allowedRolesAndUsers(self):
return ['Manager', 'Authenticated', 'Anonymous']
CatalogAware will be removed in Zope 4 and then can't be used any more.
cf https://github.com/zopefoundation/Products.ZCatalog/issues/26
I would like to ovverride only the "view" of the classic "description field" of the Plone standard content type (Document, Folder, blabla), cause I need to "structure" the text of this field with structured text like:
This is my description<br/>
with many lines<br/>
bla bla<br/>
Changing the template that renders the standard description field to convert newlines to breaks isn't hard, but will require a little care to avoid creating a security hole.
Override the skin layer kss_generic_macros.pt template, either in a theming product or the custom folder.
Then, you may use Products.PythonScripts.standard.newline_to_br to convert newlines to breaks. You'll need to insert the converted text with "structure" to prevent escaping of the breaks.
Since you'll be using "structure," you absolutely must also manually html escape the description (use html_quote from standard) before applying newline_to_br, or you'll create a vector for a XSS attack.
The key section of the macro, when fixed, might read:
<div metal:define-macro="description-field-view"
id="parent-fieldname-description"
tal:define="kss_class python:getKssClasses('description',
templateId='kss_generic_macros', macro='description-field-view');
pps modules/Products.PythonScripts.standard"
tal:condition="context/Description"
tal:attributes="class string:documentDescription$kss_class;">
<span metal:define-slot="inside"
tal:replace="structure python:pps.newline_to_br(pps.html_quote(context.Description()))">Description</span>
</div>
You really don't want HTML in the description field. This field is used in a number of places and expects plain text.
You are best using the approach above to add an additional field with a different name.
If you want to customize the description widget for all content types you could create an adapter using archetypes.schemaextender (notably the ISchemaModifier interface) like this:
from my.product.browser.interfaces import IMyProductLayer
from my.product.widgets import MyCustomWidget
from Products.ATContentTypes.interface.interfaces import IATContentType
from archetypes.schemaextender.interfaces import IBrowserLayerAwareExtender
from archetypes.schemaextender.interfaces import ISchemaModifier
class MyExtender(object):
# you could choose a more specific interface for a more fine grained override
adapts(IATContentType)
implements(IBrowserLayerAwareExtender, ISchemaModifier)
# this will limit out override to this browserlayer
layer = IMyProductLayer
def fiddle(self, schema):
# if you want to customize just the template of the original widget
# see links below
schema['description'].widget=MyCustomWidget(
label='...',
....
)
return schema
and then you can register it like this:
<adapter
factory=".extender.MyExtender"
provides="archetypes.schemaextender.interfaces.ISchemaModifier" />
Don't forget to register your browser layer IMyProductLayer or this adapter will never be used.
More info:
http://pypi.python.org/pypi/archetypes.schemaextender
http://weblion.psu.edu/services/documentation/developing-for-plone/fsd-extender/hiding-fields
https://svn.plone.org/svn/archetypes/MoreFieldsAndWidgets/Products.MasterSelectWidget (a custom widget example)
http://docs.plone.org/old-reference-manuals/plone_3_theming/buildingblocks/skin/templates/customizing-at-templates/customizing-widgets.html (customize just the template of a widget)