diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..60220d83c0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +omit = + src/plone.restapi/src/plone/restapi/tests/test_robot diff --git a/.gitignore b/.gitignore index 655a14ad77..7aba39d0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /local.cfg .coverage *.egg-info +*.eggs /.installed.cfg *.pyc /.Python @@ -21,4 +22,5 @@ docs/Makefile docs/make.bat docs/doctrees docs/html +htmlcov bower_components diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000000..59856bd9bd --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,8 @@ +Changelog +========= + +0.1-dev (unreleased) +-------------------- + +- Package created using templer + [] \ No newline at end of file diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000000..d83f0568cb --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +Note: place names and roles of the people who contribute to this package + in this file, one to a line, like so: + +- Joe Schmoe, Original Author +- Bob Slob, contributed monkey patches +- Jane Main, wrote flibberty module diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000..b4ab4131fa --- /dev/null +++ b/README.rst @@ -0,0 +1,4 @@ +.. contents:: + +Introduction +============ diff --git a/bootstrap.py b/bootstrap-buildout.py similarity index 89% rename from bootstrap.py rename to bootstrap-buildout.py index 6b7e45ccac..a629566735 100644 --- a/bootstrap.py +++ b/bootstrap-buildout.py @@ -35,7 +35,7 @@ Simply run this script in a directory containing a buildout.cfg, using the Python that you want bin/buildout to use. -Note that by using --find-links to point to local resources, you can keep +Note that by using --find-links to point to local resources, you can keep this script from going over the network. ''' @@ -59,6 +59,8 @@ parser.add_option("--allow-site-packages", action="store_true", default=False, help=("Let bootstrap.py use existing site packages")) +parser.add_option("--setuptools-version", + help="use a specific setuptools version") options, args = parser.parse_args() @@ -75,20 +77,24 @@ from urllib2 import urlopen ez = {} -exec(urlopen('https://bitbucket.org/pypa/setuptools/downloads/ez_setup.py' - ).read(), ez) +exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) + if not options.allow_site_packages: # ez_setup imports site, which adds site packages - # this will remove them from the path to ensure that incompatible versions + # this will remove them from the path to ensure that incompatible versions # of setuptools are not in the path import site - # inside a virtualenv, there is no 'getsitepackages'. + # inside a virtualenv, there is no 'getsitepackages'. # We can't remove these reliably if hasattr(site, 'getsitepackages'): for sitepackage_path in site.getsitepackages(): sys.path[:] = [x for x in sys.path if sitepackage_path not in x] setup_args = dict(to_dir=tmpeggs, download_delay=0) + +if options.setuptools_version is not None: + setup_args['version'] = options.setuptools_version + ez['use_setuptools'](**setup_args) import setuptools import pkg_resources @@ -128,10 +134,15 @@ _final_parts = '*final-', '*final' def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True + try: + return not parsed_version.is_prerelease + except AttributeError: + # Older setuptools + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( search_path=[setuptools_path]) if find_links: @@ -158,8 +169,7 @@ def _final_version(parsed_version): import subprocess if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: raise Exception( - "Failed to execute command:\n%s", - repr(cmd)[1:-1]) + "Failed to execute command:\n%s" % repr(cmd)[1:-1]) ###################################################################### # Import and run buildout diff --git a/buildout.cfg b/buildout.cfg index 0b3897bba8..c5ade3fad6 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -1,21 +1,17 @@ [buildout] -extends = http://dist.plone.org/release/4.3-latest/versions.cfg -find-links = - http://localhost:3141/root/pypi/+simple/ - http://dist.plone.org/release/4.3-latest/ - http://dist.plone.org/thirdparty/ - -#develop = . +extends = http://dist.plone.org/release/4.3.4/versions.cfg +extensions = mr.developer parts = instance test + coverage + report + test-coverage + code-analysis sphinxbuilder + sphinx-python templer - -[versions] -setuptools = 2.1 -zc.buildout = 2.2.1 -zope.interface = 4.0.5 +develop = . [instance] recipe = plone.recipe.zope2instance @@ -24,17 +20,51 @@ http-address = 8080 eggs = Plone Pillow -# plone.restapi [test] + plone.app.debugtoolbar + plone.restapi [test] [test] recipe = zc.recipe.testrunner eggs = ${instance:eggs} defaults = ['-s', 'plone.restapi', '--auto-color', '--auto-progress'] +[coverage] +recipe = zc.recipe.egg +eggs = coverage +initialization = + include = '--source=${buildout:directory}/src/plone.restapi' + sys.argv = sys.argv[:] + ['run', include, 'bin/test'] + +[report] +recipe = zc.recipe.egg +eggs = coverage +scripts = coverage=report +initialization = + sys.argv = sys.argv[:] + ['html', '-i'] + +[test-coverage] +recipe = collective.recipe.template +input = inline: + #!/bin/sh + ${buildout:directory}/bin/coverage + ${buildout:directory}/bin/report +output = ${buildout:directory}/bin/test-coverage +mode = 755 + +[code-analysis] +recipe = plone.recipe.codeanalysis +directory = ${buildout:directory}/src + [sphinxbuilder] recipe = collective.recipe.sphinxbuilder source = ${buildout:directory}/docs/source build = ${buildout:directory}/docs +interpreter = ${buildout:directory}/bin/${sphinx-python:interpreter} + +[sphinx-python] +recipe = zc.recipe.egg +eggs = sphinx_rtd_theme +interpreter = sphinxPython [templer] recipe = zc.recipe.egg @@ -44,3 +74,8 @@ eggs = templer.buildout templer.plone templer.dexterity + +[versions] +setuptools = 8.3 +zc.buildout = 2.3.1 +zope.interface = 4.0.5 diff --git a/docs/source/_json/document.json b/docs/source/_json/document.json new file mode 100644 index 0000000000..b530caed5b --- /dev/null +++ b/docs/source/_json/document.json @@ -0,0 +1,21 @@ +{ + "@context": "http://www.w3.org/ns/hydra/context.jsonld", + "@id": "http://localhost:55001/plone/front-page", + "@type": "Resource", + "contributors": [], + "creators": [ + "test_user_1_" + ], + "description": "Congratulations! You have successfully installed Plone.", + "effective": "1969-12-31T00:00:00+01:00", + "exclude_from_nav": "False", + "expires": "2499-12-31T00:00:00+01:00", + "icon": "++resource++plone.dexterity.item.gif", + "isPrincipiaFolderish": "0", + "language": "", + "meta_type": "Dexterity Item", + "relatedItems": [], + "rights": "", + "text": "
If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.
", + "title": "Welcome to Plone" +} \ No newline at end of file diff --git a/docs/source/_json/folder.json b/docs/source/_json/folder.json new file mode 100644 index 0000000000..98ba4f7260 --- /dev/null +++ b/docs/source/_json/folder.json @@ -0,0 +1,24 @@ +{ + "@context": "http://www.w3.org/ns/hydra/context.jsonld", + "@id": "http://localhost:55001/plone/folder1", + "@type": "Collection", + "contributors": [], + "creators": [ + "test_user_1_" + ], + "description": "", + "effective": "1969-12-31T00:00:00+01:00", + "exclude_from_nav": "False", + "expires": "2499-12-31T00:00:00+01:00", + "icon": "++resource++plone.dexterity.item.gif", + "isAnObjectManager": "1", + "isPrincipiaFolderish": "1", + "language": "", + "member": [], + "meta_type": "Dexterity Container", + "meta_types": [], + "nextPreviousEnabled": "False", + "relatedItems": [], + "rights": "", + "title": "" +} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 1a1656fb28..3055d7edd2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,7 +3,8 @@ # plone.restapi documentation build configuration file, created by # sphinx-quickstart on Mon Apr 28 13:04:12 2014. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its containing +# dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -18,14 +19,14 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ['sphinx_rtd_theme'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -91,7 +92,10 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +#html_theme = 'default' +import sphinx_rtd_theme +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/source/hydra.rst b/docs/source/hydra.rst index dc2990d506..a567ffbdd9 100644 --- a/docs/source/hydra.rst +++ b/docs/source/hydra.rst @@ -43,7 +43,6 @@ Plone Portal Root (A Hydra Collection):: ] } - - @context: Defines what kind of resource this is and the meaning of the terms used within this resource. - @id: Unique identifier for resources (IRIs). The @id property can be used to @@ -76,6 +75,25 @@ Plone Document (A Hydra Resource):: "text": "Lorem Ipsum
", } +Implementation +-------------- + +Plone Document: + +.. literalinclude:: _json/document.json + :language: jsonld + +Plone Folder: + +.. literalinclude:: _json/folder.json + :language: jsonld + + +Plone Portal Root: + +.. literalinclude:: _json/siteroot.json + :language: json-ld + Resource Operations / CRUD -------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 428964ae01..7bc9db70b9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,7 @@ Contents .. toctree:: :maxdepth: 3 + hydra item folder site-search diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..46725688b7 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[templer.local] +template = plone_basic + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..4e27ab96a1 --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +from setuptools import setup, find_packages + +version = '0.1' + +long_description = ( + open('README.rst').read() + + '\n' + + 'Contributors\n' + '============\n' + + '\n' + + open('CONTRIBUTORS.rst').read() + + '\n' + + open('CHANGES.rst').read() + + '\n') + +setup(name='plone.restapi', + version=version, + description="RESTful API for Plone", + long_description=long_description, + # Get more strings from + # http://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Environment :: Web Environment", + "Framework :: Plone", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.6", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='', + author='Timo Stollenwerk', + author_email='tisto@plone.org', + url='https://github.com/plone/plone.restapi/', + license='gpl', + packages=find_packages('src'), + package_dir={'': 'src'}, + namespace_packages=['plone', ], + include_package_data=True, + zip_safe=False, + install_requires=[ + 'setuptools', + 'plone.validatehook', + ], + extras_require={'test': [ + 'plone.app.contenttypes', + 'plone.app.testing[robot]>=4.2.2', + 'requests', + ]}, + entry_points=""" + # -*- Entry points: -*- + [z3c.autoinclude.plugin] + target = plone + """, + setup_requires=["PasteScript"], + paster_plugins=["templer.localcommands"], + ) diff --git a/src/plone/__init__.py b/src/plone/__init__.py new file mode 100644 index 0000000000..de40ea7ca0 --- /dev/null +++ b/src/plone/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/src/plone/restapi/__init__.py b/src/plone/restapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plone/restapi/adapter.py b/src/plone/restapi/adapter.py new file mode 100644 index 0000000000..7806545c85 --- /dev/null +++ b/src/plone/restapi/adapter.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +from Products.CMFCore.interfaces import IContentish +from Products.CMFCore.interfaces import IFolderish +from Products.CMFPlone.interfaces import IPloneSiteRoot + +from plone.app.textfield import RichText +from plone.restapi.utils import get_object_schema +from plone.restapi.interfaces import ISerializeToJson + +from zope.schema import Datetime +from zope.interface import implementer +from zope.component import adapter + +import json + + +@implementer(ISerializeToJson) +@adapter(IPloneSiteRoot) +def SerializeSiteRootToJson(context): + result = { + "@context": "http://www.w3.org/ns/hydra/context.jsonld", + "@id": context.absolute_url(), + '@type': 'Collection', + } + result['member'] = [ + { + '@id': member.absolute_url() + '/@@json', + 'title': member.title + } + for member_id, member in context.objectItems() + if IContentish.providedBy(member) + ] + return json.dumps(result, indent=2, sort_keys=True) + + +@implementer(ISerializeToJson) +@adapter(IContentish) +def SerializeToJson(context): + result = { + "@context": "http://www.w3.org/ns/hydra/context.jsonld", + "@id": context.absolute_url(), + } + if IFolderish.providedBy(context): + result['@type'] = 'Collection' + result['member'] = [ + { + '@id': member.absolute_url() + '/@@json', + 'title': member.title + } + for member_id, member in context.objectItems() + ] + else: + result['@type'] = 'Resource' + for title, schema_object in get_object_schema(context): + value = getattr(context, title, None) + if value is not None: + # RichText + if isinstance(schema_object, RichText): + result[title] = value.output + # DateTime + elif isinstance(schema_object, Datetime): + # Return DateTime in ISO-8601 format. See + # https://pypi.python.org/pypi/DateTime/3.0 and + # http://www.w3.org/TR/NOTE-datetime for details. + # XXX: We might want to change that in the future. + result[title] = value().ISO8601() + # Callables + elif callable(schema_object): + result[title] = value() + # Tuple + elif isinstance(value, tuple): + result[title] = list(value) + # List + elif isinstance(value, list): + result[title] = value + # String + elif isinstance(value, str): + result[title] = value + # Unicode + elif isinstance(value, unicode): + result[title] = value + else: + result[title] = str(value) + return json.dumps(result, indent=2, sort_keys=True) diff --git a/src/plone/restapi/browser/__init__.py b/src/plone/restapi/browser/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plone/restapi/browser/configure.zcml b/src/plone/restapi/browser/configure.zcml new file mode 100644 index 0000000000..6ad5854d09 --- /dev/null +++ b/src/plone/restapi/browser/configure.zcml @@ -0,0 +1,32 @@ +Lorem ipsum.
' + ) + + def test_serialize_to_json_adapter_returns_effective(self): + self.portal.doc1.setEffectiveDate(DateTime('2014/04/04')) + self.assertEqual( + json.loads(ISerializeToJson(self.portal.doc1))['effective'], + '2014-04-04T00:00:00' + ) + + def test_serialize_to_json_adapter_returns_expires(self): + self.portal.doc1.setExpirationDate(DateTime('2017/01/01')) + self.assertEqual( + json.loads(ISerializeToJson(self.portal.doc1))['expires'], + '2017-01-01T00:00:00' + ) + + def test_serialize_to_json_adapter_ignores_underscore_values(self): + self.assertFalse( + '__name__' in json.loads(ISerializeToJson(self.portal.doc1)) + ) + self.assertFalse( + 'manage_options' in json.loads(ISerializeToJson(self.portal.doc1)) + ) diff --git a/src/plone/restapi/tests/test_browser_traversal.py b/src/plone/restapi/tests/test_browser_traversal.py new file mode 100644 index 0000000000..47be1950ab --- /dev/null +++ b/src/plone/restapi/tests/test_browser_traversal.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +from plone.restapi.testing import\ + PLONE_RESTAPI_FUNCTIONAL_TESTING +from plone.app.testing import setRoles +from plone.app.testing import TEST_USER_ID +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.testing.z2 import Browser + +import unittest2 as unittest + +import json +import requests + + +class TestTraversal(unittest.TestCase): + + layer = PLONE_RESTAPI_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer['app'] + self.portal = self.layer['portal'] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ['Manager']) + self.portal.invokeFactory('Document', id='document1') + self.document = self.portal.document1 + self.document_url = self.document.absolute_url() + self.portal.invokeFactory('Folder', id='folder1') + self.folder = self.portal.folder1 + self.folder_url = self.folder.absolute_url() + import transaction + transaction.commit() + self.browser = Browser(self.app) + self.browser.handleErrors = False + self.browser.addHeader( + 'Authorization', + 'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,) + ) + + def test_json_view_document_traversal(self): + self.browser.open(self.document_url + '/@@json') + self.assertTrue(json.loads(self.browser.contents)) + self.assertEqual( + json.loads(self.browser.contents).get('@id'), + self.document_url + ) + + def test_json_view_folder_traversal(self): + self.browser.open(self.folder_url + '/@@json') + self.assertTrue(json.loads(self.browser.contents)) + self.assertEqual( + json.loads(self.browser.contents).get('@id'), + self.folder_url + ) + + def test_json_view_site_root_traversal(self): + self.browser.open(self.portal_url + '/@@json') + self.assertTrue(json.loads(self.browser.contents)) + self.assertEqual( + json.loads(self.browser.contents).get('@id'), + self.portal_url + ) + + def test_document_traversal(self): + response = requests.get( + self.document_url, + headers={'content-type': 'application/json'}, + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.headers.get('content-type'), + 'application/json', + 'When sending a GET request with content-type: application/json ' + + 'the server should respond with sending back application/json.' + ) + self.assertEqual( + response.json()['@id'], + self.document_url + ) + + def test_folder_traversal(self): + response = requests.get( + self.folder_url, + headers={'content-type': 'application/json'}, + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.headers.get('content-type'), + 'application/json', + 'When sending a GET request with content-type: application/json ' + + 'the server should respond with sending back application/json.' + ) + self.assertEqual( + response.json()['@id'], + self.folder_url + ) + + def test_site_root_traversal(self): + response = requests.get( + self.portal_url, + headers={'content-type': 'application/json'}, + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.headers.get('content-type'), + 'application/json', + 'When sending a GET request with content-type: application/json ' + + 'the server should respond with sending back application/json.' + ) + self.assertEqual( + response.json()['@id'], + self.portal_url + ) diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py new file mode 100644 index 0000000000..281213c0d1 --- /dev/null +++ b/src/plone/restapi/tests/test_documentation.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from plone.restapi.testing import\ + PLONE_RESTAPI_FUNCTIONAL_TESTING +from plone.app.testing import setRoles +from plone.app.testing import TEST_USER_ID +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.textfield.value import RichTextValue +from plone.testing.z2 import Browser + +import unittest2 as unittest + +import requests + + +def save_response_for_documentation(filename, response): + f = open('../../docs/source/_json/%s' % filename, 'w') + f.write(response.text) + f.close() + + +class TestTraversal(unittest.TestCase): + + layer = PLONE_RESTAPI_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer['app'] + self.request = self.layer['request'] + self.portal = self.layer['portal'] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ['Manager']) + self.portal.invokeFactory('Document', id='front-page') + self.document = self.portal['front-page'] + self.document.title = u"Welcome to Plone" + self.document.description = u"Congratulations! You have successfully installed Plone." + self.document.text = RichTextValue( + u"If you're seeing this instead of the web site you were " + + u"expecting, the owner of this web site has just installed " + + u"Plone. Do not contact the Plone Team or the Plone mailing " + + u"lists about this.", + 'text/plain', + 'text/html' + ) + import transaction + transaction.commit() + self.browser = Browser(self.app) + self.browser.handleErrors = False + self.browser.addHeader( + 'Authorization', + 'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,) + ) + + def test_documentation_document(self): + response = requests.get( + self.document.absolute_url(), + headers={'content-type': 'application/json'}, + auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + ) + save_response_for_documentation('document.json', response) diff --git a/src/plone/restapi/tests/test_example.py b/src/plone/restapi/tests/test_example.py new file mode 100644 index 0000000000..8475afb7ee --- /dev/null +++ b/src/plone/restapi/tests/test_example.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import unittest2 as unittest + +from Products.CMFCore.utils import getToolByName + +from plone.restapi.testing import \ + PLONE_RESTAPI_INTEGRATION_TESTING + + +class TestExample(unittest.TestCase): + + layer = PLONE_RESTAPI_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer['app'] + self.portal = self.layer['portal'] + self.qi_tool = getToolByName(self.portal, 'portal_quickinstaller') + + def test_product_is_installed(self): + """ Validate that our products GS profile has been run and the product + installed + """ + pid = 'plone.restapi' + installed = [p['id'] for p in self.qi_tool.listInstalledProducts()] + self.assertTrue(pid in installed, + 'package appears not to have been installed') diff --git a/src/plone/restapi/tests/test_robot.py b/src/plone/restapi/tests/test_robot.py new file mode 100644 index 0000000000..e9e124c865 --- /dev/null +++ b/src/plone/restapi/tests/test_robot.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from plone.app.testing import ROBOT_TEST_LEVEL +from plone.restapi.testing import PLONE_RESTAPI_FUNCTIONAL_TESTING +from plone.testing import layered +import robotsuite +import unittest +import os + + +def test_suite(): + suite = unittest.TestSuite() + current_dir = os.path.abspath(os.path.dirname(__file__)) + robot_dir = os.path.join(current_dir, 'robot') + robot_tests = [ + os.path.join('robot', doc) for doc in + os.listdir(robot_dir) if doc.endswith('.robot') and + doc.startswith('test_') + ] + for robot_test in robot_tests: + robottestsuite = robotsuite.RobotTestSuite(robot_test) + robottestsuite.level = ROBOT_TEST_LEVEL + suite.addTests([ + layered( + robottestsuite, + layer=PLONE_RESTAPI_FUNCTIONAL_TESTING + ), + ]) + return suite diff --git a/src/plone/restapi/tests/test_utils.py b/src/plone/restapi/tests/test_utils.py new file mode 100644 index 0000000000..0dc499952f --- /dev/null +++ b/src/plone/restapi/tests/test_utils.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +import unittest2 as unittest +from plone.restapi.utils import underscore_to_camelcase +from plone.restapi.utils import get_object_schema +from plone.restapi.testing import\ + PLONE_RESTAPI_INTEGRATION_TESTING +from plone.app.testing import setRoles +from plone.app.testing import TEST_USER_ID + + +class GetObjectSchemaUnitTest(unittest.TestCase): + + layer = PLONE_RESTAPI_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer['app'] + self.portal = self.layer['portal'] + setRoles(self.portal, TEST_USER_ID, ['Manager']) + + def test_document(self): + self.portal.invokeFactory('Document', id='doc1', title='Doc 1') + schema = [x[0] for x in get_object_schema(self.portal.doc1)] + self.assertEqual( + schema, + [ + 'text', + 'title', + 'allow_discussion', + 'exclude_from_nav', + 'relatedItems', + 'table_of_contents', + 'meta_type', + 'isPrincipiaFolderish', + 'icon', + 'rights', + 'contributors', + 'effective', + 'expires', + 'language', + 'subjects', + 'creators', + 'description', + 'changeNote' + ] + ) + + def test_folder(self): + self.portal.invokeFactory('Folder', id='folder1', title='Folder 1') + schema = [x[0] for x in get_object_schema(self.portal.folder1)] + self.assertEqual( + schema, + [ + 'title', + 'allow_discussion', + 'exclude_from_nav', + 'relatedItems', + 'nextPreviousEnabled', + 'isAnObjectManager', + 'meta_type', + 'meta_types', + 'isPrincipiaFolderish', + 'icon', + 'rights', + 'contributors', + 'effective', + 'expires', + 'language', + 'subjects', + 'creators', + 'description' + ] + ) + + +class UnderscoreToCamelcaseUnitTest(unittest.TestCase): + + def test_empty(self): + self.assertEqual(underscore_to_camelcase(''), '') + + def test_simple_term(self): + self.assertEqual(underscore_to_camelcase('lorem'), 'Lorem') + + def test_two_simple_terms(self): + self.assertEqual(underscore_to_camelcase('lorem_ipsum'), 'LoremIpsum') diff --git a/src/plone/restapi/utils.py b/src/plone/restapi/utils.py new file mode 100644 index 0000000000..ad6fb15090 --- /dev/null +++ b/src/plone/restapi/utils.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from zope.schema import getFields +from zope.interface import providedBy +from plone.behavior.interfaces import IBehaviorAssignable + + +def get_object_schema(obj): + object_schema = set() + for iface in providedBy(obj).flattened(): + for name, field in getFields(iface).items(): + no_underscore_method = not name.startswith('_') + no_manage_method = not name.startswith('manage') + if no_underscore_method and no_manage_method: + if name not in object_schema: + object_schema.add(name) + yield name, field + + assignable = IBehaviorAssignable(obj, None) + if assignable: + for behavior in assignable.enumerateBehaviors(): + for name, field in getFields(behavior.interface).items(): + if name not in object_schema: + object_schema.add(name) + yield name, field + + +def underscore_to_camelcase(underscore_string): + return ''.join( + x for x in underscore_string.title() if not x.isspace() + ).replace('_', '')