Initial checkin
svn path=/plone.formwidget.contenttree/trunk/; revision=22458
This commit is contained in:
commit
3055d0d10f
|
@ -0,0 +1,10 @@
|
|||
plone.formwidget.contenttree is a z3c.form widget for use with Plone. It
|
||||
uses the jQuery Autocomplete widget, and has graceful fallback for non-
|
||||
Javascript browsers.
|
||||
|
||||
There is a single-select version (AutocompleteSelectionFieldWidget) for
|
||||
Choice fields, and a multi-select one (AutocompleteMultiSelectionFieldWidget)
|
||||
for collection fields (e.g. List, Tuple) with a value_type of Choice.
|
||||
|
||||
When using this widget, the vocabulary/source has to provide the IQuerySource
|
||||
interface from z3c.formwidget.query and have a search() method.
|
|
@ -0,0 +1,8 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
1.0a1 - Unreleased
|
||||
----------------
|
||||
|
||||
* Initial release
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
plone.formwidget.contenttree Installation
|
||||
=======================
|
||||
|
||||
* When you're reading this you have probably already run
|
||||
``easy_install plone.formwidget.contenttree``. Find out how to install setuptools
|
||||
(and EasyInstall) here:
|
||||
http://peak.telecommunity.com/DevCenter/EasyInstall
|
||||
|
||||
* Create a file called ``plone.formwidget.contenttree-configure.zcml`` in the
|
||||
``/path/to/instance/etc/package-includes`` directory. The file
|
||||
should only contain this::
|
||||
|
||||
<include package="plone.formwidget.contenttree" />
|
||||
|
||||
|
||||
Alternatively, if you are using zc.buildout and the plone.recipe.zope2instance
|
||||
recipe to manage your project, you can do this:
|
||||
|
||||
* Add ``plone.formwidget.contenttree`` to the list of eggs to install, e.g.:
|
||||
|
||||
[buildout]
|
||||
...
|
||||
eggs =
|
||||
...
|
||||
plone.formwidget.contenttree
|
||||
|
||||
* Tell the plone.recipe.zope2instance recipe to install a ZCML slug:
|
||||
|
||||
[instance]
|
||||
recipe = plone.recipe.zope2instance
|
||||
...
|
||||
zcml =
|
||||
plone.formwidget.contenttree
|
||||
|
||||
* Re-run buildout, e.g. with:
|
||||
|
||||
$ ./bin/buildout
|
||||
|
||||
You can skip the ZCML slug if you are going to explicitly include the package
|
||||
from another package's configure.zcml file.
|
|
@ -0,0 +1,222 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
|
@ -0,0 +1,16 @@
|
|||
plone.formwidget.contenttree is copyright Martin Aspeli
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston,
|
||||
MA 02111-1307 USA.
|
|
@ -0,0 +1,6 @@
|
|||
# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
|
||||
try:
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
||||
except ImportError:
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
|
@ -0,0 +1,6 @@
|
|||
# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
|
||||
try:
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
||||
except ImportError:
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
|
@ -0,0 +1,5 @@
|
|||
Content tree widget
|
||||
===================
|
||||
|
||||
plone.formwidget.contenttree provides a content tree browser widget based on
|
||||
the jqueryFileTree widget.
|
|
@ -0,0 +1,82 @@
|
|||
plone.formwidget.contenttree to-do
|
||||
==================================
|
||||
|
||||
This is Limi's mockup about selection widgets. To achieve this, we should:
|
||||
|
||||
1) Change plone.formwidget.autocomplete so that instead of using ;-delimited
|
||||
tokens in the text field, it builds a list of radio buttons/check boxes.
|
||||
This should be easy enough with custom result() handler in the jQuery
|
||||
Autocomplete widget. The widget can act in single-select mode, even for
|
||||
multi-select fields.
|
||||
|
||||
2) Make plone.formwidget.contenttree extend plone.formwidget.autocomplete.
|
||||
The searchable source for the content tree widget should work with the
|
||||
autocomplete widget already.
|
||||
|
||||
3) Add a browse button, and pop up a box with the tree widget.
|
||||
|
||||
4) Make selection callback do nothing except store value
|
||||
|
||||
5) Make Add and Close buttons act as below.
|
||||
|
||||
6) Ensure the extract() method in both widgets works. It is likely that we
|
||||
can make do with the one from z3c.formwidget.query, since we're now just
|
||||
talking about radio buttons.
|
||||
|
||||
The design:
|
||||
-----------
|
||||
|
||||
- Supports search and - optionally - browsing
|
||||
- Supports client-side or server-side data sources
|
||||
- Both for values selection (user/group/tags) and content selection
|
||||
- Multi-select and single select
|
||||
|
||||
Tags/users/groups use case (aka.autocomplete - flat namespace):
|
||||
|
||||
Tags: [x] Plone
|
||||
[x] Zope
|
||||
[x] Python
|
||||
[Search... ]
|
||||
| autocomplete |
|
||||
| autocomplete |
|
||||
| autocomplete |
|
||||
|______________|
|
||||
|
||||
|
||||
Content selection use case (autocomplete + browse - nested namespace)
|
||||
|
||||
Tags: [x] Document 1
|
||||
[x] Document 2
|
||||
[x] Image 1
|
||||
[Search... ] [Browse...]
|
||||
| autocomplete |
|
||||
| autocomplete |
|
||||
| autocomplete |
|
||||
|______________|
|
||||
|
||||
Clicking [Browse...] gives you a lightbox-style (ie. not a real window)
|
||||
pop-up dialog box that resembles a standard file system picker:
|
||||
|
||||
|
||||
___________________________________________
|
||||
| Favorites |_________________[Search... ]|
|
||||
| - Folder1 | |
|
||||
| - Folder2 | Tree selection goes here |
|
||||
| - Folder3 | |
|
||||
| | |
|
||||
| | [Add] [Close] |
|
||||
|__________________________________________|
|
||||
|
||||
|
||||
Note that it also supports search, since very often people's first instinct
|
||||
is to browse, and when they can't find it, they look for search.
|
||||
|
||||
Other notes:
|
||||
|
||||
- Multiselect is still hard, but for now: if multiselect is supported, the
|
||||
buttons are: [Add] and [Close], if it's single-select, they are [Add] and
|
||||
[Cancel].
|
||||
|
||||
- Adding an item in the multiselect case shows a "Item X added" status
|
||||
message, but doesn't close the window. Adding in the single-select case
|
||||
closes the window.
|
|
@ -0,0 +1,7 @@
|
|||
from zope.i18nmessageid import MessageFactory
|
||||
MessageFactory = MessageFactory('plone.formwidget.contenttree')
|
||||
|
||||
from plone.formwidget.contenttree.widget import ContentTreeFieldWidget
|
||||
from plone.formwidget.contenttree.widget import MultiContentTreeFieldWidget
|
||||
|
||||
from plone.formwidget.contenttree.source import PathSourceBinder
|
|
@ -0,0 +1,42 @@
|
|||
<configure
|
||||
xmlns="http://namespaces.zope.org/zope"
|
||||
xmlns:z3c="http://namespaces.zope.org/z3c"
|
||||
xmlns:browser="http://namespaces.zope.org/browser"
|
||||
xmlns:gs="http://namespaces.zope.org/genericsetup"
|
||||
i18n_domain="plone.formwidget.contenttree">
|
||||
|
||||
<include package="plone.z3cform" />
|
||||
|
||||
<adapter factory=".navtree.QueryBuilder" />
|
||||
<adapter factory=".navtree.NavtreeStrategy" />
|
||||
|
||||
<browser:page
|
||||
name="contenttree-fetch"
|
||||
for=".interfaces.IContentTreeWidget"
|
||||
permission="zope.Public"
|
||||
class=".widget.Fetch"
|
||||
/>
|
||||
|
||||
<browser:resourceDirectory
|
||||
name="plone.formwidget.contenttree"
|
||||
directory="jqueryFileTree"
|
||||
/>
|
||||
|
||||
<gs:registerProfile
|
||||
name="default"
|
||||
title="Content tree widget"
|
||||
directory="profiles/default"
|
||||
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
|
||||
provides="Products.GenericSetup.interfaces.EXTENSION"
|
||||
/>
|
||||
|
||||
<!-- Demo -->
|
||||
<browser:page
|
||||
for="*"
|
||||
name="test-tree-widget"
|
||||
class="plone.formwidget.contenttree.demo.TestView"
|
||||
permission="cmf.ModifyPortalContent"
|
||||
/>
|
||||
<adapter factory="plone.formwidget.contenttree.demo.TestAdapter" />
|
||||
|
||||
</configure>
|
|
@ -0,0 +1,41 @@
|
|||
from zope.interface import Interface, implements
|
||||
from zope.component import adapts
|
||||
from zope import schema
|
||||
|
||||
from plone.formwidget.contenttree import ContentTreeFieldWidget
|
||||
from plone.formwidget.contenttree import MultiContentTreeFieldWidget
|
||||
from plone.formwidget.contenttree import PathSourceBinder
|
||||
|
||||
from z3c.form import form, button, field
|
||||
from plone.z3cform import layout
|
||||
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
|
||||
class ITestForm(Interface):
|
||||
|
||||
buddy = schema.Choice(title=u"Buddy object",
|
||||
source=PathSourceBinder(portal_type='Document'))
|
||||
|
||||
class TestAdapter(object):
|
||||
implements(ITestForm)
|
||||
adapts(Interface)
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def _get_buddy(self):
|
||||
return None
|
||||
def _set_buddy(self, value):
|
||||
print "setting", value
|
||||
buddy = property(_get_buddy, _set_buddy)
|
||||
|
||||
class TestForm(form.Form):
|
||||
fields = field.Fields(ITestForm)
|
||||
fields['buddy'].widgetFactory = ContentTreeFieldWidget
|
||||
|
||||
@button.buttonAndHandler(u'Ok')
|
||||
def handle_ok(self, action):
|
||||
data, errors = self.extractData()
|
||||
print data, errors
|
||||
|
||||
TestView = layout.wrap_form(TestForm)
|
|
@ -0,0 +1,6 @@
|
|||
<ul style="display:none"
|
||||
tal:define="level options/level|python:0; children options/children|nothing"
|
||||
tal:attributes="class python:'navTree navTreeLevel'+str(level)"
|
||||
tal:condition="python: len(children) > 0">
|
||||
<span tal:replace="structure python:view.recurse_template(children=children, level=level+1)" />
|
||||
</ul>
|
|
@ -0,0 +1,13 @@
|
|||
<script type="text/javascript" tal:content="view/js"></script>
|
||||
<div tal:attributes="id string:${view/id}-queryselect">
|
||||
<tal:block replace="structure view/subform/render" />
|
||||
<tal:block condition="nocall:view/terms">
|
||||
<div tal:replace="structure view/renderQueryWidget" />
|
||||
</tal:block>
|
||||
</div>
|
||||
<div class="visualClear contenttreeWidget" style="display:none"
|
||||
tal:attributes="id string:${view/id}-contenttree">
|
||||
<ul class="navTree navTreeLevel0">
|
||||
<li tal:replace="structure view/render_tree" />
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<tal:master define="level options/level|python:0; children options/children | nothing;">
|
||||
<tal:navitem repeat="node children">
|
||||
<li tal:define="show_children node/show_children;
|
||||
children node/children;
|
||||
item_url node/getURL;
|
||||
item_path node/path;
|
||||
item_icon node/item_icon;
|
||||
selectable node/selectable;
|
||||
li_folder_class python:show_children and ' navTreeFolderish' or '';
|
||||
li_selectable_class python:selectable and ' selectable' or '';
|
||||
li_collapsed_class python:(len(children) > 0 and show_children) and ' expanded' or ' collapsed';
|
||||
li_class string:${li_folder_class}${li_selectable_class}${li_collapsed_class}"
|
||||
tal:attributes="class string:navTreeItem visualNoMarker${li_class}">
|
||||
<tal:level define="item_class string:state-${node/normalized_review_state}">
|
||||
<tal:block define="item_class item_class">
|
||||
<a tal:attributes="href item_path; rel level;
|
||||
title node/Description;
|
||||
class string:$item_class">
|
||||
<img tal:replace="structure item_icon/html_tag" />
|
||||
<span tal:content="node/Title">Selected Item Title</span>
|
||||
</a>
|
||||
</tal:block>
|
||||
<ul tal:attributes="class python:'navTree navTreeLevel'+str(level)"
|
||||
tal:condition="python: len(children) > 0 and show_children">
|
||||
<span tal:replace="structure python:view.recurse_template(children=children, level=level+1)" />
|
||||
</ul>
|
||||
</tal:level>
|
||||
</li>
|
||||
</tal:navitem>
|
||||
</tal:master>
|
|
@ -0,0 +1,33 @@
|
|||
from zope.interface import Interface, Attribute
|
||||
from zope import schema
|
||||
|
||||
from z3c.formwidget.query.interfaces import IQuerySource
|
||||
|
||||
class IContentFilter(Interface):
|
||||
"""A filter that specifies what content is addable, where
|
||||
"""
|
||||
|
||||
criteria = Attribute("A dict with catalog search parameters")
|
||||
|
||||
def __call__(self, brain, index_data):
|
||||
"""Return True or False depending on whether the given brain, which
|
||||
was found from the given index data (a dict), should be included.
|
||||
"""
|
||||
|
||||
class IContentSource(IQuerySource):
|
||||
"""A source that can specify content
|
||||
"""
|
||||
|
||||
navigation_tree_query = Attribute("A dict to pass to portal_catalog when "
|
||||
"searching for items to display. This dictates "
|
||||
"how the tree is structured, and also provides an "
|
||||
"upper bound for items allowed by the source.")
|
||||
|
||||
selectable_filter = schema.Object(title=u"Filter",
|
||||
description=u"The filter will be applied to any returned search results",
|
||||
schema=IContentFilter)
|
||||
|
||||
class IContentTreeWidget(Interface):
|
||||
"""Marker interface for the content tree widget
|
||||
"""
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.contenttreeWidget {
|
||||
|
||||
}
|
||||
|
||||
.contenttreeWidget li a {
|
||||
border-bottom: solid 1px transparent;
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// This is based on jQueryFileTree by Cory S.N. LaViska
|
||||
if(jQuery) (function($){
|
||||
|
||||
$.extend($.fn, {
|
||||
contentTree: function(o, h) {
|
||||
|
||||
// Defaults
|
||||
if(!o) var o = {};
|
||||
if(o.script == undefined) o.script = 'fetch';
|
||||
|
||||
if(o.folderEvent == undefined) o.folderEvent = 'dblclick';
|
||||
if(o.selectEvent == undefined) o.selectEvent = 'click';
|
||||
|
||||
if(o.expandSpeed == undefined) o.expandSpeed = -1;
|
||||
if(o.collapseSpeed == undefined) o.collapseSpeed = -1;
|
||||
|
||||
if(o.multiFolder == undefined) o.multiFolder = true;
|
||||
if(o.multiSelect == undefined) o.multiSelect = false;
|
||||
|
||||
o.root = $(this);
|
||||
|
||||
function loadTree(c, t, r) {
|
||||
$(c).addClass('wait');
|
||||
$.post(o.script, { href: t, rel: r}, function(data) {
|
||||
$(c).removeClass('wait').append(data);
|
||||
$(c).find('ul:hidden').slideDown({ duration: o.expandSpeed });
|
||||
bindTree(c);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFolderEvent() {
|
||||
var li = $(this).parent();
|
||||
if(li.hasClass('collapsed')) {
|
||||
if(!o.multiFolder) {
|
||||
li.parent().find('ul:visible').slideUp({ duration: o.collapseSpeed });
|
||||
li.parent().find('li.navTreeFolderish').removeClass('expanded').addClass('collapsed');
|
||||
}
|
||||
|
||||
if(li.find('ul').length == 0)
|
||||
loadTree(li, escape($(this).attr('href')), escape($(this).attr('rel')));
|
||||
else
|
||||
li.find('ul:hidden').slideDown({ duration: o.expandSpeed });
|
||||
|
||||
li.removeClass('collapsed').addClass('expanded');
|
||||
} else {
|
||||
li.find('ul').slideUp({ duration: o.collapseSpeed });
|
||||
li.removeClass('expanded').addClass('collapsed');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleSelectEvent() {
|
||||
var li = $(this).parent();
|
||||
var selected = true;
|
||||
if(!li.hasClass('navTreeCurrentItem')) {
|
||||
if(!o.multiSelect) {
|
||||
o.root.find('li.navTreeCurrentItem').removeClass('navTreeCurrentItem');
|
||||
}
|
||||
|
||||
li.addClass('navTreeCurrentItem');
|
||||
selected = true;
|
||||
} else {
|
||||
li.removeClass('navTreeCurrentItem');
|
||||
selected = false;
|
||||
}
|
||||
|
||||
h($(this).attr('href'), $(this).attr('rel'), selected);
|
||||
}
|
||||
|
||||
function bindTree(t) {
|
||||
$(t).find('li a').bind('click', function() { return false; });
|
||||
$(t).find('li.navTreeFolderish a').bind(o.folderEvent, handleFolderEvent);
|
||||
$(t).find('li.selectable a').bind(o.selectEvent, handleSelectEvent);
|
||||
}
|
||||
|
||||
$(this).each(function() {
|
||||
bindTree($(this));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
|
@ -0,0 +1,86 @@
|
|||
from zope.interface import implements, Interface
|
||||
from zope.component import adapts, getMultiAdapter
|
||||
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
|
||||
from Products.CMFPlone.browser.navtree import SitemapNavtreeStrategy
|
||||
from Products.CMFPlone import utils
|
||||
|
||||
from plone.app.layout.navigation.interfaces import INavtreeStrategy
|
||||
from plone.app.layout.navigation.interfaces import INavigationQueryBuilder
|
||||
|
||||
from plone.app.layout.navigation.root import getNavigationRoot
|
||||
from plone.app.layout.navigation.navtree import NavtreeStrategyBase
|
||||
|
||||
from plone.formwidget.contenttree.interfaces import IContentSource
|
||||
from plone.formwidget.contenttree.interfaces import IContentTreeWidget
|
||||
|
||||
class QueryBuilder(object):
|
||||
"""Build a navtree query for a content source
|
||||
"""
|
||||
implements(INavigationQueryBuilder)
|
||||
adapts(Interface, IContentSource)
|
||||
|
||||
def __init__(self, context, source):
|
||||
self.context = context
|
||||
self.source = source
|
||||
|
||||
def __call__(self):
|
||||
context = self.context
|
||||
source = self.source
|
||||
|
||||
portal_properties = getToolByName(context, 'portal_properties')
|
||||
navtree_properties = getattr(portal_properties, 'navtree_properties')
|
||||
|
||||
portal_url = getToolByName(context, 'portal_url')
|
||||
|
||||
query = {}
|
||||
|
||||
# Construct the path query
|
||||
|
||||
rootPath = getNavigationRoot(context)
|
||||
currentPath = '/'.join(context.getPhysicalPath())
|
||||
|
||||
# If we are above the navigation root, a navtree query would return
|
||||
# nothing (since we explicitly start from the root always). Hence,
|
||||
# use a regular depth-1 query in this case.
|
||||
|
||||
if not currentPath.startswith(rootPath):
|
||||
query['path'] = {'query' : rootPath, 'depth' : 1}
|
||||
else:
|
||||
query['path'] = {'query' : currentPath, 'navtree' : 1}
|
||||
|
||||
# Only list the applicable types
|
||||
query['portal_type'] = utils.typesToList(context)
|
||||
|
||||
# Apply the desired sort
|
||||
sortAttribute = navtree_properties.getProperty('sortAttribute', None)
|
||||
if sortAttribute is not None:
|
||||
query['sort_on'] = sortAttribute
|
||||
sortOrder = navtree_properties.getProperty('sortOrder', None)
|
||||
if sortOrder is not None:
|
||||
query['sort_order'] = sortOrder
|
||||
|
||||
return query
|
||||
|
||||
class NavtreeStrategy(SitemapNavtreeStrategy):
|
||||
"""The navtree strategy used for the content tree widget
|
||||
"""
|
||||
implements(INavtreeStrategy)
|
||||
adapts(Interface, IContentTreeWidget)
|
||||
|
||||
def __init__(self, context, widget):
|
||||
super(NavtreeStrategy, self).__init__(context, None)
|
||||
|
||||
self.context = context
|
||||
self.widget = widget
|
||||
self.showAllParents = True # override from base class
|
||||
|
||||
def nodeFilter(self, node):
|
||||
# Don't filter any nodes here
|
||||
return True
|
||||
|
||||
def decoratorFactory(self, node):
|
||||
new_node = super(NavtreeStrategy, self).decoratorFactory(node)
|
||||
new_node['selectable'] = self.widget.bound_source._filter(new_node['item'])
|
||||
return new_node
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0"?>
|
||||
<object name="portal_css">
|
||||
|
||||
<stylesheet id="++resource++plone.formwidget.contenttree/contenttree.css"
|
||||
title="" cacheable="True" compression="safe" cookable="True"
|
||||
enabled="1" expression="" media="screen"
|
||||
rel="stylesheet" rendering="import" />
|
||||
|
||||
</object>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0"?>
|
||||
<object name="portal_javascripts">
|
||||
|
||||
<javascript id="++resource++plone.formwidget.contenttree/contenttree.js"
|
||||
cacheable="True" compression="none" cookable="True" enabled="True"
|
||||
expression="" inline="False" />
|
||||
|
||||
</object>
|
|
@ -0,0 +1,149 @@
|
|||
import itertools
|
||||
|
||||
from zope.interface import implements
|
||||
from zope.component import getMultiAdapter
|
||||
|
||||
from zope.schema.interfaces import IContextSourceBinder
|
||||
from zope.schema.vocabulary import SimpleTerm
|
||||
|
||||
from plone.formwidget.contenttree.interfaces import IContentSource
|
||||
from plone.formwidget.contenttree.interfaces import IContentFilter
|
||||
|
||||
from plone.app.vocabularies.catalog import parse_query
|
||||
|
||||
from plone.app.layout.navigation.interfaces import INavigationQueryBuilder
|
||||
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from Products.ZCTextIndex.ParseTree import ParseError
|
||||
|
||||
class CustomFilter(object):
|
||||
"""A filter that can be used to test simple values in brain metadata and
|
||||
indexes.
|
||||
|
||||
Limitations:
|
||||
|
||||
- Will probably only work on FieldIndex and KeywordIndex indexes
|
||||
|
||||
"""
|
||||
implements(IContentFilter)
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.criteria = {}
|
||||
for key, value in kw.items():
|
||||
if not isinstance(value, (list, tuple, set, frozenset,)):
|
||||
self.criteria[key] = [value]
|
||||
elif isinstance(value, (set, frozenset,)):
|
||||
self.criteria[key] = list(value)
|
||||
else:
|
||||
self.criteria[key] = value
|
||||
|
||||
def __call__(self, brain, index_data):
|
||||
for key, value in self.criteria.items():
|
||||
test_value = index_data.get(key, None)
|
||||
if test_value is not None:
|
||||
if not isinstance(test_value, (list, tuple, set, frozenset,)):
|
||||
test_value = set([test_value])
|
||||
elif isinstance(value, (list, tuple)):
|
||||
test_value = set(test_value)
|
||||
if not test_value.intersection(value):
|
||||
return False
|
||||
return True
|
||||
|
||||
class PathSource(object):
|
||||
implements(IContentSource)
|
||||
|
||||
def __init__(self, context, selectable_filter, navigation_tree_query=None):
|
||||
self.context = context
|
||||
|
||||
query_builder = getMultiAdapter((context, self), INavigationQueryBuilder)
|
||||
query = query_builder()
|
||||
|
||||
if navigation_tree_query is not None:
|
||||
query.update(navigation_tree_query)
|
||||
|
||||
self.navigation_tree_query = query
|
||||
self.selectable_filter = selectable_filter
|
||||
|
||||
self.catalog = getToolByName(context, "portal_catalog")
|
||||
|
||||
portal_tool = getToolByName(context, "portal_url")
|
||||
self.portal_path = portal_tool.getPortalPath()
|
||||
|
||||
# Tokenised vocabulary API
|
||||
|
||||
def __iter__(self):
|
||||
return [].__iter__()
|
||||
|
||||
def __contains__(self, value):
|
||||
try:
|
||||
brain = self._brain_for_path(self._path_for_value(value))
|
||||
return self._filter(brain)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def getTermByToken(self, token):
|
||||
brain = self._brain_for_path(self._path_for_token(token))
|
||||
if not self._filter(brain):
|
||||
raise LookupError(token)
|
||||
return self._term_for_brain(brain)
|
||||
|
||||
def getTerm(self, value):
|
||||
brain = self._brain_for_path(self._path_for_value(value))
|
||||
if not self._filter(brain):
|
||||
raise LookupError(value)
|
||||
return self._term_for_brain(brain)
|
||||
|
||||
# Query API - used to locate content, e.g. in non-JS mode
|
||||
|
||||
def search(self, query, limit=20):
|
||||
catalog_query = self.selectable_filter.criteria.copy()
|
||||
catalog_query.update(parse_query(query, self.portal_path))
|
||||
|
||||
if limit and 'sort_limit' not in catalog_query:
|
||||
catalog_query['sort_limit'] = limit
|
||||
|
||||
try:
|
||||
results = (self._term_for_brain(brain)
|
||||
for brain in self.catalog(**catalog_query)
|
||||
if self._filter(brain))
|
||||
except ParseError:
|
||||
return []
|
||||
|
||||
if catalog_query.has_key('path'):
|
||||
path = catalog_query['path']['query']
|
||||
if path != '':
|
||||
return itertools.chain((self._term_for_brain(self._brain_for_path(path)),),
|
||||
results)
|
||||
|
||||
return results
|
||||
|
||||
# Helper functions
|
||||
|
||||
def _filter(self, brain):
|
||||
index_data = self.catalog.getIndexDataForRID(brain.getRID())
|
||||
return self.selectable_filter(brain, index_data)
|
||||
|
||||
def _path_for_token(self, token):
|
||||
return self.portal_path + token
|
||||
|
||||
def _path_for_value(self, value):
|
||||
return self.portal_path + value
|
||||
|
||||
def _brain_for_path(self, path):
|
||||
rid = self.catalog.getrid(path)
|
||||
return self.catalog._catalog[rid]
|
||||
|
||||
def _term_for_brain(self, brain):
|
||||
path = brain.getPath()[len(self.portal_path):]
|
||||
return SimpleTerm(path, path, brain.Title)
|
||||
|
||||
class PathSourceBinder(object):
|
||||
implements(IContextSourceBinder)
|
||||
|
||||
def __init__(self, navigation_tree_query=None, **kw):
|
||||
self.selectable_filter = CustomFilter(**kw)
|
||||
self.navigation_tree_query = navigation_tree_query
|
||||
|
||||
def __call__(self, context):
|
||||
return PathSource(context, selectable_filter=self.selectable_filter,
|
||||
navigation_tree_query=self.navigation_tree_query)
|
|
@ -0,0 +1,12 @@
|
|||
<configure
|
||||
xmlns="http://namespaces.zope.org/zope"
|
||||
xmlns:z3c="http://namespaces.zope.org/z3c"
|
||||
xmlns:browser="http://namespaces.zope.org/browser"
|
||||
i18n_domain="plone.formwidget.contenttree">
|
||||
|
||||
<include file="meta.zcml" package="Products.GenericSetup"/>
|
||||
|
||||
<include package="plone.z3cform" file="testing.zcml" />
|
||||
<include package="plone.formwidget.contenttree" file="configure.zcml" />
|
||||
|
||||
</configure>
|
|
@ -0,0 +1,16 @@
|
|||
import os
|
||||
import unittest
|
||||
|
||||
from zope.testing import doctest
|
||||
from zope.app.testing.functional import ZCMLLayer
|
||||
|
||||
testing_zcml_path = os.path.join(os.path.dirname(__file__), 'testing.zcml')
|
||||
testing_zcml_layer = ZCMLLayer(testing_zcml_path, 'plone.formwidget.contenttree', 'testing_zcml_layer')
|
||||
|
||||
def test_suite():
|
||||
readme_txt = doctest.DocFileSuite('README.txt')
|
||||
readme_txt.layer = testing_zcml_layer
|
||||
|
||||
return unittest.TestSuite([
|
||||
readme_txt,
|
||||
])
|
|
@ -0,0 +1,162 @@
|
|||
from zope.interface import implements, implementer
|
||||
from zope.component import getMultiAdapter
|
||||
|
||||
import z3c.form.interfaces
|
||||
import z3c.form.widget
|
||||
import z3c.form.util
|
||||
|
||||
from z3c.formwidget.query.widget import QuerySourceRadioWidget
|
||||
from z3c.formwidget.query.widget import QuerySourceCheckboxWidget
|
||||
|
||||
from plone.app.layout.navigation.interfaces import INavtreeStrategy
|
||||
from plone.app.layout.navigation.navtree import buildFolderTree
|
||||
|
||||
from plone.formwidget.contenttree.interfaces import IContentTreeWidget
|
||||
from plone.formwidget.contenttree import MessageFactory as _
|
||||
|
||||
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
|
||||
|
||||
from AccessControl import getSecurityManager
|
||||
from Acquisition import Explicit
|
||||
|
||||
from Products.Five.browser import BrowserView
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
|
||||
class Fetch(BrowserView):
|
||||
|
||||
fragment_template = ViewPageTemplateFile('fragment.pt')
|
||||
recurse_template = ViewPageTemplateFile('input_recurse.pt')
|
||||
|
||||
def validate_access(self):
|
||||
content = self.context.form.context
|
||||
view_name = self.context.form.__name__
|
||||
view_instance = getMultiAdapter((content, self.request), name=view_name).__of__(content)
|
||||
|
||||
# May raise Unauthorized
|
||||
getSecurityManager().validate(content, content, view_name, view_instance)
|
||||
|
||||
def __call__(self):
|
||||
|
||||
# We want to check that the user was indeed allowed to access the
|
||||
# form for this widget. We can only this now, since security isn't
|
||||
# applied yet during traversal.
|
||||
self.validate_access()
|
||||
|
||||
widget = self.context
|
||||
context = widget.context
|
||||
source = widget.bound_source
|
||||
|
||||
directory = self.request.form.get('href', None)
|
||||
level = self.request.form.get('rel', 0)
|
||||
|
||||
navtree_query = source.navigation_tree_query.copy()
|
||||
navtree_query['path'] = {'depth': 1, 'query': directory}
|
||||
|
||||
if 'is_default_page' not in navtree_query:
|
||||
navtree_query['is_default_page'] = False
|
||||
|
||||
strategy = getMultiAdapter((context, widget), INavtreeStrategy)
|
||||
catalog = getToolByName(context, 'portal_catalog')
|
||||
|
||||
children = []
|
||||
for brain in catalog(navtree_query):
|
||||
newNode = {'item' : brain,
|
||||
'depth' : -1, # not needed here
|
||||
'currentItem' : False,
|
||||
'currentParent' : False,
|
||||
'children' : []}
|
||||
if strategy.nodeFilter(newNode):
|
||||
newNode = strategy.decoratorFactory(newNode)
|
||||
children.append(newNode)
|
||||
|
||||
return self.fragment_template(children=children, level=int(level))
|
||||
|
||||
class ContentTreeBase(Explicit):
|
||||
implements(IContentTreeWidget)
|
||||
|
||||
# XXX: Due to the way the rendering of the QuerySourceRadioWidget works,
|
||||
# if we call this 'template' or use a <z3c:widgetTemplate /> directive,
|
||||
# we'll get infinite recursion when trying to render the radio buttons.
|
||||
|
||||
widget_template = ViewPageTemplateFile('input.pt')
|
||||
recurse_template = ViewPageTemplateFile('input_recurse.pt')
|
||||
|
||||
# Parameters passed to the JavaScript function
|
||||
folderEvent = 'dblclick'
|
||||
selectEvent = 'click'
|
||||
expandSpeed = 200
|
||||
collapseSpeed = 200
|
||||
multiFolder = True
|
||||
|
||||
# Whether to act as single or multi select
|
||||
multiple = False
|
||||
|
||||
def render_tree(self):
|
||||
context = self.context
|
||||
source = self.bound_source
|
||||
|
||||
strategy = getMultiAdapter((context, self), INavtreeStrategy)
|
||||
data = buildFolderTree(context,
|
||||
obj=context,
|
||||
query=source.navigation_tree_query,
|
||||
strategy=strategy)
|
||||
|
||||
return self.recurse_template(children=data.get('children', []), level=1)
|
||||
|
||||
def render(self):
|
||||
return self.widget_template(self)
|
||||
|
||||
def js(self):
|
||||
|
||||
form_context = self.form.__parent__
|
||||
form_name = self.form.__name__
|
||||
widget_name = self.name.split('.')[-1]
|
||||
|
||||
url = "%s/@@%s/++widget++%s/@@contenttree-fetch" % (form_context.absolute_url(), form_name, widget_name)
|
||||
|
||||
tokens = [self.terms.getTerm(value).token for value in self.value if value]
|
||||
|
||||
return """
|
||||
(function($) {
|
||||
$().ready(function() {
|
||||
$('#%(id)s-contenttree').css('display', 'block');
|
||||
$('#%(id)s-contenttree').contentTree({
|
||||
script: '%(url)s',
|
||||
folderEvent: '%(folderEvent)s',
|
||||
selectEvent: '%(selectEvent)s',
|
||||
expandSpeed: %(expandSpeed)d,
|
||||
collapseSpeed: %(collapseSpeed)s,
|
||||
multiFolder: %(multiFolder)s,
|
||||
multiSelect: %(multiSelect)s,
|
||||
}, function(href, rel, selected) { alert(selected + ' href: ' + href + ', rel: ' + rel); }
|
||||
);
|
||||
|
||||
// TODO: Set current selection
|
||||
});
|
||||
})(jQuery);
|
||||
""" % dict(url=url,
|
||||
id=self.name.replace('.', '-'),
|
||||
folderEvent=self.folderEvent,
|
||||
selectEvent=self.selectEvent,
|
||||
expandSpeed=self.expandSpeed,
|
||||
collapseSpeed=self.collapseSpeed,
|
||||
multiFolder=str(self.multiFolder).lower(),
|
||||
multiSelect=str(self.multiple).lower())
|
||||
|
||||
class ContentTreeWidget(ContentTreeBase, QuerySourceRadioWidget):
|
||||
"""ContentTree widget that allows single selection.
|
||||
"""
|
||||
|
||||
class MultiContentTreeWidget(ContentTreeBase, QuerySourceCheckboxWidget):
|
||||
"""ContentTree widget that allows multiple selection
|
||||
"""
|
||||
|
||||
multiple = True
|
||||
|
||||
@implementer(z3c.form.interfaces.IFieldWidget)
|
||||
def ContentTreeFieldWidget(field, request):
|
||||
return z3c.form.widget.FieldWidget(field, ContentTreeWidget(request))
|
||||
|
||||
@implementer(z3c.form.interfaces.IFieldWidget)
|
||||
def MultiContentTreeFieldWidget(field, request):
|
||||
return z3c.form.widget.FieldWidget(field, MultiContentTreeWidget(request))
|
|
@ -0,0 +1,34 @@
|
|||
from setuptools import setup, find_packages
|
||||
import os
|
||||
|
||||
version = '1.0b2'
|
||||
|
||||
setup(name='plone.formwidget.contenttree',
|
||||
version=version,
|
||||
description="AJAX selection widget for Plone",
|
||||
long_description=open("README.txt").read() + "\n" +
|
||||
open(os.path.join("docs", "HISTORY.txt")).read(),
|
||||
# Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
"Framework :: Plone",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
keywords='Plone selection widget AJAX',
|
||||
author='Martin Aspeli',
|
||||
author_email='optilude@gmail.com',
|
||||
url='http://plone.org',
|
||||
license='GPL',
|
||||
packages=find_packages(exclude=['ez_setup']),
|
||||
namespace_packages=['plone', 'plone.formwidget'],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=[
|
||||
'setuptools',
|
||||
'z3c.formwidget.query',
|
||||
'plone.z3cform',
|
||||
],
|
||||
entry_points="""
|
||||
# -*- Entry points: -*-
|
||||
""",
|
||||
)
|
Reference in New Issue