Getting published content item out of request["PUBLISHED"] in Plone - plone

I try to get context object in IPubAfterTraversal hook from event
#grok.subscribe(IPubAfterTraversal)
def admin_language_negotiator(event):
"""
Event handler which pokes the language after traversing and authentication is done, but before rendering.
"""
# Keep the current request language (negotiated on portal_languages)
# untouched
request = event.request
if not IAddonSpecific.providedBy(request):
# Add on is not active
return
context = request.get("PUBLISHED", None)
I'd like to do:
IContentish.providedBy(context) # Check if real content request or CSS/Image request
PUBLISHED is not content item context, however:
context
<FSPageTemplate at /Plone/en/plan/plan/document_view>
The PUBLISHED may or may not point to a view. What is the safest way to get published content item object, if any, out of HTTPRequest?

plone.app.theming does it like this:
def findContext(request):
"""Find the context from the request
"""
published = request.get('PUBLISHED', None)
context = getattr(published, '__parent__', None)
if context is None:
context = request.PARENTS[0]
return context
https://github.com/plone/plone.app.theming/blob/master/src/plone/app/theming/utils.py#L146

Related

Extend Plone-Controlpanel Form

Is it possible to extend the Controlpanel-View with each addon?
For Example
ca.db.core -> Makes basic fieldset/tab for DB Connection Settings
ca.db.person -> If installed, Adds to the "core" settings a new fieldset/tab for Person specific fields/settings
ca.db.schema -> If installed, also adds an new fieldset/tab for schema.org Fields
Yes it's possible, I once discussed this problem with a guy, who wrote the bda.plone.shop addon.
They faced the same problem, and solved it by using a ContextProxy object, which puts the different schema definitions together in one proxy object.
Using a proxy is IMHO a hack, but I don't know a better solution.
The proxy, tries to get/set a attribute from a list of schemas.
Be aware, you need to handle conflicting names, means if you have the same field name in more than one schema.
class ContextProxy(object):
def __init__(self, interfaces):
self.__interfaces = interfaces
alsoProvides(self, *interfaces)
def __setattr__(self, name, value):
if name.startswith('__') or name.startswith('_ContextProxy__'):
return object.__setattr__(self, name, value)
registry = getUtility(IRegistry)
for interface in self.__interfaces:
proxy = registry.forInterface(interface)
try:
getattr(proxy, name)
except AttributeError:
pass
else:
return setattr(proxy, name, value)
raise AttributeError(name)
def __getattr__(self, name):
if name.startswith('__') or name.startswith('_ContextProxy__'):
return object.__getattr__(self, name)
registry = getUtility(IRegistry)
for interface in self.__interfaces:
proxy = registry.forInterface(interface)
try:
return getattr(proxy, name)
except AttributeError:
pass
raise AttributeError(name)
Now you need to use the proxy in your ControlPanel form.
I assume you're using the RegistryEditForm from plone.registry:
class SettingsEditForm(controlpanel.RegistryEditForm):
schema = ISettings
label = _(u"Settings")
description = _(u"")
# IMPORTANT Note 1 - This is where you hook in your proxy
def getContent(self):
interfaces = [self.schema] # Base schema from ca.db.core
interfaces.extend(self.additionalSchemata) # List of additional schemas
return ContextProxy(interfaces)
# IMPORTANT Note 2 - You may get the additional schemas dynamically to extend the Settings Form. For example by name (startswith...)
# In this case they have a separate interface, which marks the relevant interfaces.
#property
def additionalSchemata(self):
registry = getUtility(IRegistry)
interface_names = set(record.interfaceName for record
in registry.records.values())
for name in interface_names:
if not name:
continue
interface = None
try:
interface = resolve(name)
except ImportError:
# In case of leftover registry entries of uninstalled Products
continue
if ISettingsProvider.providedBy(interface):
yield interface
...
You can find the full code here

adding params in faraday middleware

I am trying to write a middleware that will set additional params to the query string. The use case is to be able to add additional authentication tokens, for eg, to the request as required by the backend, but wanting to inject it independent of the request creation itself.
This is what my middleware(pseudo code) looks like:
class MyMiddleware < Struct.new(:app, :key, :value)
def call(env)
env.params[key] = value #1
#env.params = {key => value} #2
app.call env
end
end
above raises NoMethodError (undefined method[]=' for nil:NilClass)`
above sets the params hash but the parameter is not encoded as part of the query string. The query string remains what it was before the middlewares start processing the request.
My analysis is that since the query string gets built from the params in rack_builder.rb:191
def build_response(connection, request)
app.call(build_env(connection, request))
end
def build_env(connection, request)
Env.new(request.method, request.body,
connection.build_exclusive_url(request.path, request.params), #<== rack_builder.rb:191
request.options, request.headers, connection.ssl,
connection.parallel_manager)
end
Middlewares don't get the opportunity to set additional params. While env has a params property, it is nil and doesn't appear to be touched during or after the middlewares get invoked.
So, I have the following questions:
1. Is there a supported way to achieve this?
2. If not, is there a way to rebuild the query string as part of the middleware executing?
3. Would it be better to defer the URL building to after most of the request middleware chain is executed (but of course, before the adapter gets to do its thing; or do it in the adapter)?
4. Any other options?
Appreciate any guidance.
The answer is here at github: https://github.com/lostisland/faraday/issues/483 by #mislav
Inside the middleware, the request URL is already fully constructed together with all the configured query parameters in a string. The only way to add or remove query parameters is to edit the env.url.query string, e.g.:
MyMiddleware = Struct.new(:app, :token) do
def call(env)
env.url.query = add_query_param(env.url.query, "token", token)
app.call env
end
def add_query_param(query, key, value)
query = query.to_s
query << "&" unless query.empty?
query << "#{Faraday::Utils.escape key}=#{Faraday::Utils.escape value}"
end
end
conn = Faraday.new "http://example.com" do |f|
f.use MyMiddleware, "MYTOKEN"
f.adapter :net_http
end
However, if your middleware is going to be like MyMiddleware above and just add a static query parameter to all requests, the much simpler approach is to avoid the middleware and just configure the Faraday connection instance to apply the same query parameter to all requests:
conn = Faraday.new "http://example.com" do |f|
f.params[:token] = "MYTOKEN"
f.adapter :net_http
end

plone.directives form - passing variables with a redirect

I have a plone form that basically gets search terms, performs a search, and then directs the user to another form. For this second form, I need to pass a couple variables.
class MySearch(form.SchemaForm):
grok.context(IMyContext)
grok.name('my-search')
ignoreContext = True
schema = ISearchSchema
#button.buttonAndHandler(_(u'Search Method'))
def searchMethod(self, action):
""" group update/removal """
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
results = somecall(data['term'])
if results:
self.request.set('myvar',results['myvar'])
self.request.response.redirect('##my-results')
else:
IStatusMessage(self.request).addStatusMessage(_(u"No results found"),"info")
return
This doesn't work - I guess a new request is generated so myvar is immediately lost. I could put this in a query string and include it in the redirect, but would prefer to send it as POST data if possible. I also tried something like
return getMultiAdapter((self.context,self.request),name='my-results')()
to see if I could use that as a starting point to passing in variables, but that just returns me to my-search.
The parameters set on the request object are not taken into account (nor should they) when issuing a redirect.
Append a query string to the redirection URL instead; urllib.urlencode() does the job admirably:
from urllib import urlencode
self.request.response.redirect('##my-results?' + urlencode({'myvar': results['myvar']}))
The .redirect() call returns a 303 See Other response with the URL you passed in the the method as the Location header; the browser then opens that new location, and will include any GET parameters you added to the URL.

Add new record to plone.registry without re-running GenericSetup / re-installing the product

in a Plone add-on product, I have a control panel page where some config options can be set. They are stored in plone.registry. The control panel adapter fetches the different fields in its __init__ method by querying the interface, like:
class MultiLanguageExtraOptionsAdapter(LanguageControlPanelAdapter):
implementsOnly(IMultiLanguageExtraOptionsSchema)
def __init__(self, context):
super(MultiLanguageExtraOptionsAdapter, self).__init__(context)
self.registry = getUtility(IRegistry)
self.settings = self.registry.forInterface(
IMultiLanguageExtraOptionsSchema)
Now I add an additional field to the interface IMultiLanguageExtraOptionsSchema and restart plone. On the control panel page I then an error:
KeyError: 'Interface `plone.app.multilingual.interfaces.IMultiLanguageExtraOptionsSchema` defines a field `blah`, for which there is no record.'
(This is expected for the forInterfacemethod , as described on the plone.registry README. The record is not there.)
Of course, if I add that field via GenericSetup (registry.xml), and I re-install the product / re-run the "Control Panel" step, all is well:
<registry>
<records interface="plone.app.multilingual.interfaces.IMultiLanguageExtraOptionsSchema">
<value key="blah"></value>
<records>
<registry>
But I don't want to force users to re-install a product, just because there's a new option in the product-specific control panel. So my question: Is there a recommended way for getting a new record for a new field into plone.registry?
You could try/catch the KeyError and then make sure all registry settings are registered:
try:
self.settings = self.registry.forInterface(IMultiLanguageExtraOptionsSchema)
except KeyError:
registry = getUtility(IRegistry)
registry.registerInterface(IMultiLanguageExtraOptionsSchema)
I would recommend to write an upgrade step though (which would force your users to reinstall the product of course).
upgrades.py:
def update_registry(context):
registry = getUtility(IRegistry)
registry.registerInterface(IMultiLanguageExtraOptionsSchema)
upgrades.zcml::
<genericsetup:upgradeStep
source="*"
destination="1100"
title="Update plone.app.multilingual setting registry"
description=""
profile="plone.app.multilingual:default"
handler=".upgrades.update_registry"
/>
See
https://github.com/collective/collective.mailchimp/blob/master/collective/mailchimp/upgrades.py
and
https://github.com/collective/collective.mailchimp/blob/master/collective/mailchimp/upgrades.zcml
for an example.
If you pass False as the second parameter to forInterface:
registry.forInterface(IMultiLanguageExtraOptionsSchema, False)
then it won't throw an error if fields from the schema are missing from the registry, but will simply return the field's default value.
Safe getting settings from registry:
def get_registry_settings(interface, name):
registry = getUtility(IRegistry)
settings = registry.forInterface(interface, check=False)
value = getattr(settings, name)
if value == settings.__schema__[name].missing_value:
value = settings.__schema__[name].default
return value

How could I render just the form from an autoform?

I'm in the context of a BrowserView. I want to render a form inside the page.
class CommandeView(BrowserView):
...
def render_myform(self):
livraison_form = LivraisonForm(self.context, self.request)
livraison_form.update()
return livraison_form.render()
class ILivraisonForm(interface.Interface):
livraison = schema.Choice(title=_(u"Mode de livraison"),
vocabulary=livraison_vocabulary)
class LivraisonForm(AutoExtensibleForm, form.EditForm):
schema = ILivraisonForm
class LivraisonFormAdapter(object):
component.adapts(ICommande)
interface.implements(ILivraisonForm)
def __init__(self, context):
self.context = context
self.livraison = self.context.livraison
I would like render_myform to render only the form but it return a HTML Plone page.
Any tips are welcomed.
Haven't checked this in-depth, but the form is probably wrapped by the plone FormWrapper class.
Look for a .form attribute on the LivraisonForm instance:
def render_myform(self):
livraison_form = LivraisonForm(self.context, self.request).form
livraison_form.update()
return livraison_form.render()
It may be you need to add an additional interface to the request for this to work (the wrapper normally does this for you. I'm a little low on time right now, so you get this untested:
from plone.z3cform import z2
class CommandeView(BrowserView):
def render_myform(self):
z2.switch_on(self) # Apply layer interface, set up locale, add `getURL` method
livraison_form = LivraisonForm(self.context, self.request).form
livraison_form.update()
return livraison_form.render()
The Plone Knowledgebase uses this same setup to show a form in a viewlet.

Resources