# HG changeset patch # User Thierry Florac # Date 1424339781 -3600 # Node ID 63811b2a5670f52942cded7a47078f715217b8a5 First commit diff -r 000000000000 -r 63811b2a5670 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,19 @@ + +syntax: regexp +^develop-eggs$ +syntax: regexp +^parts$ +syntax: regexp +^bin$ +syntax: regexp +^\.installed\.cfg$ +syntax: regexp +^\.settings$ +syntax: regexp +^build$ +syntax: regexp +^dist$ +syntax: regexp +^\.idea$ +syntax: regexp +.*\.pyc$ diff -r 000000000000 -r 63811b2a5670 .installed.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.installed.cfg Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,81 @@ +[buildout] +installed_develop_eggs = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs/pyams-file.egg-link +parts = i18n pyflakes test package + +[i18n] +__buildout_installed__ = /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pybabel + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/polint + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pot-create +__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.5-py3.4.egg zc.buildout-2.3.1-py3.4.egg +_b = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +_d = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +develop-eggs-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +eggs = babel + lingua +eggs-directory = /var/local/env/pyams/eggs +recipe = zc.recipe.egg + +[pyflakes] +__buildout_installed__ = /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pyflakes + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pyflakes +__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.5-py3.4.egg zc.buildout-2.3.1-py3.4.egg +_b = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +_d = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +develop-eggs-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +eggs = pyflakes +eggs-directory = /var/local/env/pyams/eggs +entry-points = pyflakes=pyflakes.scripts.pyflakes:main +initialization = if not sys.argv[1:]: sys.argv[1:] = ["src"] +recipe = zc.recipe.egg +scripts = pyflakes + +[test] +__buildout_installed__ = /home/borax/Dropbox/src/PyAMS/pyams_file/parts/test + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/test +__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.5-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-8ad56e9feeb914de21341cbb261cc34e +_b = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +_d = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +develop-eggs-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +eggs = pyams_file [test] +eggs-directory = /var/local/env/pyams/eggs +location = /home/borax/Dropbox/src/PyAMS/pyams_file/parts/test +recipe = zc.recipe.testrunner +script = /home/borax/Dropbox/src/PyAMS/pyams_file/bin/test + +[package] +__buildout_installed__ = /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pcreate + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/proutes + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pshell + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pviews + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/ptweens + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pserve + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/pdistreport + /home/borax/Dropbox/src/PyAMS/pyams_file/bin/prequest +__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.5-py3.4.egg zc.buildout-2.3.1-py3.4.egg +_b = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +_d = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/bin +develop-eggs-directory = /home/borax/Dropbox/src/PyAMS/pyams_file/develop-eggs +eggs = pyams_file + pyramid + python-magic + zope.component + zope.interface +eggs-directory = /var/local/env/pyams/eggs +recipe = zc.recipe.egg + +[buildout] +parts = pyflakes test package i18n + +[buildout] +parts = test package i18n pyflakes + +[buildout] +parts = package i18n pyflakes test diff -r 000000000000 -r 63811b2a5670 LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,42 @@ +Zope Public License (ZPL) Version 2.1 +===================================== + +A copyright notice accompanies this license document that identifies +the copyright holders. + +This license has been certified as open source. It has also been designated +as GPL compatible by the Free Software Foundation (FSF). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions in source code must retain the accompanying copyright + notice, this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the accompanying copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Names of the copyright holders must not be used to endorse or promote + products derived from this software without prior written permission + from the copyright holders. + 4. The right to distribute this software or to use it for any purpose does + not give you the right to use Servicemarks (sm) or Trademarks (tm) of the + copyright holders. Use of them is covered by separate agreement with the + copyright holders. + 5. If any files are modified, you must cause the modified files to carry + prominent notices stating that you changed the files and the date of any + change. + + +Disclaimer +========== + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff -r 000000000000 -r 63811b2a5670 MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,5 @@ +include *.txt +recursive-include docs * +recursive-include src * +global-exclude *.pyc +global-exclude *.*~ diff -r 000000000000 -r 63811b2a5670 bootstrap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bootstrap.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,178 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os +import shutil +import sys +import tempfile + +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +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 +this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", help="use a specific zc.buildout version") + +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", "--config-file", + help=("Specify the path to the buildout configuration " + "file to be used.")) +parser.add_option("-f", "--find-links", + help=("Specify a URL to search for buildout releases")) +parser.add_option("--allow-site-packages", + action="store_true", default=False, + help=("Let bootstrap.py use existing site packages")) + + +options, args = parser.parse_args() + +###################################################################### +# load/install setuptools + +try: + if options.allow_site_packages: + import setuptools + import pkg_resources + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +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 + # of setuptools are not in the path + import site + # 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) +ez['use_setuptools'](**setup_args) +import setuptools +import pkg_resources + +# This does not (always?) update the default working set. We will +# do it. +for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +###################################################################### +# Install buildout + +ws = pkg_resources.working_set + +cmd = [sys.executable, '-c', + 'from setuptools.command.easy_install import main; main()', + '-mZqNxd', tmpeggs] + +find_links = os.environ.get( + 'bootstrap-testing-find-links', + options.find_links or + ('http://downloads.buildout.org/' + if options.accept_buildout_test_releases else None) + ) +if find_links: + cmd.extend(['-f', find_links]) + +setuptools_path = ws.find( + pkg_resources.Requirement.parse('setuptools')).location + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _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 + index = setuptools.package_index.PackageIndex( + search_path=[setuptools_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +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]) + +###################################################################### +# Import and run buildout + +ws.add_entry(tmpeggs) +ws.require(requirement) +import zc.buildout.buildout + +if not [a for a in args if '=' not in a]: + args.append('bootstrap') + +# if -c was provided, we push it back into args for buildout' main function +if options.config_file is not None: + args[0:0] = ['-c', options.config_file] + +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff -r 000000000000 -r 63811b2a5670 buildout.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/buildout.cfg Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,60 @@ +[buildout] +eggs-directory = /var/local/env/pyams/eggs + +socket-timeout = 3 +show-picked-versions = true +newest = false + +allow-hosts = + bitbucket.org + *.python.org + *.sourceforge.net + github.com + +#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg +versions = versions +newest = false +#allow-picked-versions = false + +src = src +develop = . + +parts = + package + i18n + pyflakes + test + +[package] +recipe = zc.recipe.egg +eggs = + pyams_file + pyramid + python-magic + zope.component + zope.interface + +[i18n] +recipe = zc.recipe.egg +eggs = + babel + lingua + +[pyflakes] +recipe = zc.recipe.egg +eggs = pyflakes +scripts = pyflakes +entry-points = pyflakes=pyflakes.scripts.pyflakes:main +initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"] + +[pyflakesrun] +recipe = collective.recipe.cmd +on_install = true +cmds = ${buildout:develop}/bin/${pyflakes:scripts} + +[test] +recipe = zc.recipe.testrunner +eggs = pyams_file [test] + +[versions] +pyams_base = 0.1.0 diff -r 000000000000 -r 63811b2a5670 docs/HISTORY.txt diff -r 000000000000 -r 63811b2a5670 docs/README.txt diff -r 000000000000 -r 63811b2a5670 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,68 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +This module contains pyams_file package +""" +import os +from setuptools import setup, find_packages + +DOCS = os.path.join(os.path.dirname(__file__), + 'docs') + +README = os.path.join(DOCS, 'README.txt') +HISTORY = os.path.join(DOCS, 'HISTORY.txt') + +version = '0.1.0' +long_description = open(README).read() + '\n\n' + open(HISTORY).read() + +tests_require = [] + +setup(name='pyams_file', + version=version, + description="PyAMS file interfaces and classes", + long_description=long_description, + classifiers=[ + "License :: OSI Approved :: Zope Public License", + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='Pyramid PyAMS file', + author='Thierry Florac', + author_email='tflorac@ulthar.net', + url='http://hg.ztfy.org/pyams/pyams_file', + license='ZPL', + packages=find_packages('src'), + package_dir={'': 'src'}, + namespace_packages=[], + include_package_data=True, + package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']}, + zip_safe=False, + # uncomment this to be able to run tests with setup.py + test_suite="pyams_file.tests.test_utilsdocs.test_suite", + tests_require=tests_require, + extras_require=dict(test=tests_require), + install_requires=[ + 'setuptools', + # -*- Extra requirements: -*- + 'pillow', + 'pyramid', + 'python-magic', + 'zope.component', + 'zope.interface', + ], + entry_points=""" + # -*- Entry points: -*- + """, + ) diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/PKG-INFO --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/PKG-INFO Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,18 @@ +Metadata-Version: 1.1 +Name: pyams-file +Version: 0.1.0 +Summary: PyAMS file interfaces and classes +Home-page: http://hg.ztfy.org/pyams/pyams_file +Author: Thierry Florac +Author-email: tflorac@ulthar.net +License: ZPL +Description: + + +Keywords: Pyramid PyAMS file +Platform: UNKNOWN +Classifier: License :: OSI Approved :: Zope Public License +Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python +Classifier: Framework :: Pyramid +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/SOURCES.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/SOURCES.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,49 @@ +MANIFEST.in +setup.py +docs/HISTORY.txt +docs/README.txt +src/pyams_file/__init__.py +src/pyams_file/configure.zcml +src/pyams_file/file.py +src/pyams_file/image.py +src/pyams_file/property.py +src/pyams_file/schema.py +src/pyams_file/thumbnail.py +src/pyams_file.egg-info/PKG-INFO +src/pyams_file.egg-info/SOURCES.txt +src/pyams_file.egg-info/dependency_links.txt +src/pyams_file.egg-info/entry_points.txt +src/pyams_file.egg-info/namespace_packages.txt +src/pyams_file.egg-info/not-zip-safe +src/pyams_file.egg-info/requires.txt +src/pyams_file.egg-info/top_level.txt +src/pyams_file/archive/__init__.py +src/pyams_file/archive/bz2.py +src/pyams_file/archive/configure.zcml +src/pyams_file/archive/gz.py +src/pyams_file/archive/tar.py +src/pyams_file/archive/zip.py +src/pyams_file/doctests/README.txt +src/pyams_file/interfaces/__init__.py +src/pyams_file/interfaces/archive.py +src/pyams_file/locales/pyams_file.pot +src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo +src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po +src/pyams_file/tests/__init__.py +src/pyams_file/tests/test_utilsdocs.py +src/pyams_file/tests/test_utilsdocstrings.py +src/pyams_file/views/__init__.py +src/pyams_file/views/file.py +src/pyams_file/widget/__init__.py +src/pyams_file/widget/configure.zcml +src/pyams_file/widget/templates/file-display.pt +src/pyams_file/widget/templates/file-input.pt +src/pyams_file/widget/templates/image-display.pt +src/pyams_file/widget/templates/image-input.pt +src/pyams_file/zmi/__init__.py +src/pyams_file/zmi/configure.zcml +src/pyams_file/zmi/file.py +src/pyams_file/zmi/image.py +src/pyams_file/zmi/templates/image-crop.pt +src/pyams_file/zmi/templates/image-pano-thumbnail.pt +src/pyams_file/zmi/templates/image-square-thumbnail.pt \ No newline at end of file diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/dependency_links.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/dependency_links.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/entry_points.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/entry_points.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,3 @@ + + # -*- Entry points: -*- + \ No newline at end of file diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/namespace_packages.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/namespace_packages.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/not-zip-safe --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/not-zip-safe Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/requires.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/requires.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,8 @@ +setuptools +pillow +pyramid +python-magic +zope.component +zope.interface + +[test] diff -r 000000000000 -r 63811b2a5670 src/pyams_file.egg-info/top_level.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file.egg-info/top_level.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,1 @@ +pyams_file diff -r 000000000000 -r 63811b2a5670 src/pyams_file/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,35 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +from pyramid.i18n import TranslationStringFactory +_ = TranslationStringFactory('pyams_file') + + +def includeme(config): + """Pyramid include""" + + # add translations + config.add_translation_dirs('pyams_file:locales') + + # load registry components + try: + import pyams_zmi + except ImportError: + config.scan(ignore='pyams_file.zmi') + else: + config.scan() + + if hasattr(config, 'load_zcml'): + config.load_zcml('configure.zcml') diff -r 000000000000 -r 63811b2a5670 src/pyams_file/archive/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/archive/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,1 @@ +# diff -r 000000000000 -r 63811b2a5670 src/pyams_file/archive/bz2.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/archive/bz2.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,60 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import bz2 + +# import interfaces +from pyams_file.interfaces.archive import IArchiveExtractor + +# import packages +from pyams_file.archive.tar import TarArchiveExtractor +from pyams_file.file import get_magic_content_type +from pyams_utils.registry import utility_config + + +CHUNK_SIZE = 4096 + + +@utility_config(name='application/x-bzip2', provides=IArchiveExtractor) +class BZip2ArchiveExtractor(object): + """BZip2 file format archive extractor""" + + def initialize(self, data): + if isinstance(data, tuple): + data = data[0] + self.data = data + self.bz2 = bz2.BZ2Decompressor() + + def get_contents(self): + position = 0 + compressed = self.data[position:position + CHUNK_SIZE] + decompressed = self.bz2.decompress(compressed) + while (not decompressed) and (position < len(self.data)): + compressed = self.data[position:position + CHUNK_SIZE] + decompressed = self.bz2.decompress(compressed) + position += CHUNK_SIZE + mime_type = get_magic_content_type(decompressed[:CHUNK_SIZE]) + if mime_type == 'application/x-tar': + tar = TarArchiveExtractor() + tar.initialize(self.data, mode='r:bz2') + for element in tar.get_contents(): + yield element + else: + while position < len(self.data): + compressed = self.data[position:position + CHUNK_SIZE] + decompressed += self.bz2.decompress(compressed) + position += CHUNK_SIZE + yield (decompressed, '') diff -r 000000000000 -r 63811b2a5670 src/pyams_file/archive/gz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/archive/gz.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,52 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from io import BytesIO +import gzip + +# import interfaces +from pyams_file.interfaces.archive import IArchiveExtractor + +# import packages +from pyams_file.archive.tar import TarArchiveExtractor +from pyams_file.file import get_magic_content_type +from pyams_utils.registry import utility_config + + +@utility_config(name='application/x-gzip', provides=IArchiveExtractor) +class GZipArchiveExtractor(object): + """GZip file format archive extractor""" + + def initialize(self, data): + if isinstance(data, tuple): + data = data[0] + self.data = data + self.gzip_file = gzip.GzipFile(fileobj=BytesIO(data), mode='r') + + def get_contents(self): + gzip_data = self.gzip_file.read(4096) + mime_type = get_magic_content_type(gzip_data) + if mime_type == 'application/x-tar': + tar = TarArchiveExtractor() + tar.initialize(self.data, mode='r:gz') + for element in tar.get_contents(): + yield element + else: + next_data = self.gzip_file.read() + while next_data: + gzip_data += next_data + next_data = self.gzip_file.read() + yield (gzip_data, '') diff -r 000000000000 -r 63811b2a5670 src/pyams_file/archive/tar.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/archive/tar.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,53 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from io import BytesIO +import tarfile + +# import interfaces +from pyams_file.interfaces.archive import IArchiveExtractor + +# import packages +from pyams_file.file import get_magic_content_type +from pyams_utils.registry import query_utility, utility_config + + +@utility_config(name='application/x-tar', provides=IArchiveExtractor) +class TarArchiveExtractor(object): + """TAR file format archive extractor""" + + def initialize(self, data, mode='r'): + if isinstance(data, tuple): + data = data[0] + self.tar = tarfile.open(fileobj=BytesIO(data), mode=mode) + + def get_contents(self): + members = self.tar.getmembers() + for member in members: + filename = member.name + content = self.tar.extractfile(member) + if content is not None: + content = content.read() + if not content: + continue + mime_type = get_magic_content_type(content[:4096]) + extractor = query_utility(IArchiveExtractor, name=mime_type) + if extractor is not None: + extractor.initialize(content) + for element in extractor.get_contents(): + yield element + else: + yield (content, filename) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/archive/zip.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/archive/zip.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from io import BytesIO +import zipfile + +# import interfaces +from pyams_file.interfaces.archive import IArchiveExtractor + +# import packages +from pyams_file.file import get_magic_content_type +from pyams_utils.registry import query_utility, utility_config + + +@utility_config(name='application/zip', provides=IArchiveExtractor) +class ZipArchiveExtractor(object): + """ZIP file format archive extractor""" + + def initialize(self, data, mode='r'): + if isinstance(data, tuple): + data = data[0] + self.zip_data = zipfile.ZipFile(BytesIO(data), mode=mode) + + def get_contents(self): + members = self.zip_data.infolist() + for member in members: + filename = member.filename + content = self.zip_data.read(filename) + mime_type = get_magic_content_type(content[:4096]) + extractor = query_utility(IArchiveExtractor, name=mime_type) + if extractor is not None: + extractor.initialize(content) + for element in extractor.get_contents(): + yield element + else: + yield (content, filename) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/configure.zcml Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/doctests/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/doctests/README.txt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,3 @@ +================== +pyams_file package +================== diff -r 000000000000 -r 63811b2a5670 src/pyams_file/file.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/file.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,219 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from io import BytesIO +from PIL import Image +import os +try: + import magic +except ImportError: + magic = None + +# import interfaces +from pyams_file.interfaces import IFile, IImage, IVideo, IAudio, IFileInfo, FileModifiedEvent +from zope.location.interfaces import IContained + +# import packages +from persistent import Persistent +from pyams_utils.request import check_request +from ZODB.blob import Blob +from zope.container.contained import Contained +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +BLOCK_SIZE = 1 << 16 + + +@implementer(IFile, IFileInfo, IContained) +class File(Persistent, Contained): + """Generic file persistent object""" + + title = FieldProperty(IFileInfo['title']) + description = FieldProperty(IFileInfo['description']) + filename = FieldProperty(IFileInfo['filename']) + language = FieldProperty(IFileInfo['language']) + + def __init__(self, data='', content_type=None, source=None): + self.content_type = content_type + self._blob = None + if data: + self.data = data + elif source: + if os.path.exists(source): + try: + f = open(source, 'rb') + self.data = f + finally: + f.close() + + def get_blob(self, mode='r'): + if self._blob is None: + return None + return self._blob.open(mode=mode) + + def get_detached_blob(self): + if self._blob is None: + return None + return open(self._blob.committed(), 'rb') + + def _get_data(self): + f = self.get_blob() + if f is None: + return None + try: + data = f.read() + return data + finally: + f.close() + + def _set_data(self, data): + if self._blob is None: + self._blob = Blob() + if isinstance(data, str): + data = data.encode('utf-8') + elif hasattr(data, 'seek'): + data.seek(0) + f = self.get_blob('w') + try: + if hasattr(data, 'read'): + self._size = 0 + _data = data.read(BLOCK_SIZE) + size = len(_data) + while size > 0: + f.write(_data) + self._size += size + _data = data.read(BLOCK_SIZE) + size = len(_data) + else: + f.write(data) + self._size = len(data) + finally: + f.close() + + data = property(_get_data, _set_data) + + def get_size(self): + return self._size + + def __enter__(self): + return self.get_blob(mode='c') + + def __exit__(self, exc_type, exc_val, exc_tb): + # exc_val.value.close() + pass + + def __iter__(self): + if self._blob is None: + raise StopIteration() + with self as f: + while True: + chunk = f.read(BLOCK_SIZE) + if not chunk: + raise StopIteration(f) + yield chunk + + def __nonzero__(self): + return self._size > 0 + + +@implementer(IImage) +class ImageFile(File): + """Image file persistent object""" + + image_size = (-1, -1) + + def _set_data(self, data): + if isinstance(data, str): + data = BytesIO(data.encode('utf-8')) + File._set_data(self, data) + if hasattr(data, 'seek'): + data.seek(0) + img = Image.open(data) + self.image_size = img.size + + data = property(File._get_data, _set_data) + + def get_image_size(self): + return self.image_size + + def resize(self, width, height, keep_ratio=True): + image = Image.open(self.get_blob(mode='c')) + image_size = image.size + if width >= image_size[0] and height >= image_size[1]: + return + new_image = BytesIO() + w_ratio = 1. * width / image_size[0] + h_ratio = 1. * height / image_size[1] + if keep_ratio: + ratio = min(w_ratio, h_ratio) + image.resize((round(ratio * image_size[0]), round(ratio * image_size[1])), Image.ANTIALIAS) \ + .save(new_image, image.format, quality=99) + else: + image.resize((round(w_ratio * image_size[0]), round(h_ratio * image_size[1])), Image.ANTIALIAS) \ + .save(new_image, image.format, quality=99) + self.data = new_image + request = check_request() + request.registry.notify(FileModifiedEvent(self)) + + def crop(self, x1, y1, x2, y2): + image = Image.open(self.get_blob(mode='c')) + new_image = BytesIO() + image.crop((x1, y1, x2, y2)) \ + .save(new_image, image.format, quelity=99) + self.data = new_image + request = check_request() + request.registry.notify(FileModifiedEvent(self)) + + +@implementer(IVideo) +class VideoFile(File): + """Video file persistent object""" + + +@implementer(IAudio) +class AudioFile(File): + """Audio file persistent object""" + + +def get_magic_content_type(input): + """Get content-type based on magic library""" + if hasattr(input, 'seek'): + input.seek(0) + if hasattr(input, 'read'): + input = input.read() + if magic is not None: + return magic.from_buffer(input, mime=True) + else: + return None + + +def FileFactory(data): + """File object factory + + Automatically create the right file type based on magic + content-type recognition + """ + content_type = get_magic_content_type(data) + if content_type.startswith(b'image/'): + factory = ImageFile + elif content_type.startswith(b'video/'): + factory = VideoFile + elif content_type.startswith(b'audio/'): + factory = AudioFile + else: + factory = File + return factory(data, content_type) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/image.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/image.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,193 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from io import BytesIO +from PIL import Image, ImageFilter + +# import interfaces +from pyams_file.interfaces import IImage, IThumbnailer, IThumbnailGeometry, IThumbnail + +# import packages +from pyams_utils.adapter import ContextAdapter, adapter_config +from zope.component import adapter +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +WEB_FORMATS = ('JPEG', 'PNG', 'GIF') + + +@implementer(IThumbnailGeometry) +class ThumbnailGeometrry(object): + """Image thumbnail geometry""" + + x1 = FieldProperty(IThumbnailGeometry['x1']) + y1 = FieldProperty(IThumbnailGeometry['y1']) + x2 = FieldProperty(IThumbnailGeometry['x2']) + y2 = FieldProperty(IThumbnailGeometry['y2']) + + +@adapter_config(context=IImage, provides=IThumbnailer) +class ImageThumbnailer(ContextAdapter): + """Image thumbnailer adapter""" + + def get_default_geometry(self, options=None): + """Default thumbnail geometry""" + geometry = ThumbnailGeometrry() + width, height = self.context.get_image_size() + geometry.x1 = 0 + geometry.y1 = 0 + geometry.x2 = width + geometry.y2 = height + return geometry + + def create_thumbnail(self, thumbnail_name, format=None): + # check thumbnail name + if isinstance(thumbnail_name, str): + width, height = tuple((int(x) for x in thumbnail_name.split('x'))) + elif isinstance(thumbnail_name, tuple): + width, height = thumbnail_name + else: + return None + image = Image.open(self.context.get_blob(mode='c')) + # check format + if not format: + format = image.format + format = format.upper() + if format not in WEB_FORMATS: + format = 'JPEG' + # check image mode + if image.mode == 'P': + image = image.convert('RGBA') + # generate thumbnail + new_image = BytesIO() + image.resize((width, height), Image.ANTIALIAS) \ + .filter(ImageFilter.SHARPEN) \ + .save(new_image, format) + return new_image, format.lower() + + +@adapter_config(name='square', context=IImage, provides=IThumbnailer) +class ImageSquareThumbnailer(ContextAdapter): + """Image square thumbnail adapter""" + + def get_default_geometry(self, options=None): + """Default square thumbnail geometry""" + geometry = ThumbnailGeometrry() + width, height = self.context.get_image_size() + if width >= height: + geometry.x1 = round((width / 2) - (height / 2)) + geometry.y1 = 0 + geometry.x2 = round((width / 2) + (height / 2)) + geometry.y2 = height + else: + geometry.x1 = 0 + geometry.y1 = round((height / 2) - (width / 2)) + geometry.x2 = width + geometry.y2 = round((height / 2) + (width / 2)) + return geometry + + def create_thumbnail(self, thumbnail_name, format=None): + geometry = IThumbnail(self.context).get_thumbnail_geometry(thumbnail_name) + image = Image.open(self.context.get_blob(mode='c')) + # get thumbnail size + if isinstance(thumbnail_name, str): + if ':' in thumbnail_name: + thumbnailer_name, size = thumbnail_name.split(':', 1) + width, height = tuple((int(x) for x in size.split('x'))) + else: + width, height = tuple((int(x) for x in thumbnail_name.split('x'))) + elif isinstance(thumbnail_name, tuple): + width, height = thumbnail_name + else: + return None + # check format + if not format: + format = image.format + format = format.upper() + if format not in WEB_FORMATS: + format = 'JPEG' + # check image mode + if image.mode == 'P': + image = image.convert('RGBA') + # generate thumbnail + new_image = BytesIO() + image.crop((geometry.x1, geometry.y1, geometry.x2, geometry.y2)) \ + .resize((width, height), Image.ANTIALIAS) \ + .filter(ImageFilter.SHARPEN) \ + .save(new_image, format) + return new_image, format.lower() + + +@adapter_config(name='pano', context=IImage, provides=IThumbnailer) +class ImagePanoThumbnailer(ContextAdapter): + """Image panoramic thumbnail adapter""" + + def get_default_geometry(self, options=None): + """Default panoramic thumbnail geometry""" + geometry = ThumbnailGeometrry() + width, height = self.context.get_image_size() + pano_max_height = width * 9 / 16 + if pano_max_height >= height: + # image wider + pano_width = height * 9 / 16 + geometry.x1 = round((width / 2) - (pano_width / 2)) + geometry.y1 = 0 + geometry.x2 = round((width / 2) + (pano_width / 2)) + geometry.y2 = height + else: + pano_height = pano_max_height + geometry.x1 = 0 + geometry.y1 = round((height / 2) - (pano_height / 2)) + geometry.x2 = width + geometry.y2 = round((height / 2) + (pano_height / 2)) + return geometry + + def create_thumbnail(self, thumbnail_name, format=None): + geometry = IThumbnail(self.context).get_thumbnail_geometry(thumbnail_name) + image = Image.open(self.context.get_blob(mode='c')) + # get thumbnail size + if isinstance(thumbnail_name, str): + if ':' in thumbnail_name: + thumbnailer_name, size = thumbnail_name.split(':', 1) + width, height = tuple((int(x) for x in size.split('x'))) + else: + width, height = tuple((int(x) for x in thumbnail_name.split('x'))) + elif isinstance(thumbnail_name, tuple): + width, height = thumbnail_name + else: + return None + # check aspect ratio + thumb_size = abs(geometry.x2 - geometry.x1), abs(geometry.y2 - geometry.y1) + w_ratio = 1. * width / thumb_size[0] + h_ratio = 1. * height / thumb_size[1] + ratio = min(w_ratio, h_ratio) + # check format + if not format: + format = image.format + format = format.upper() + if format not in WEB_FORMATS: + format = 'JPEG' + # check image mode + if image.mode == 'P': + image = image.convert('RGBA') + # generate thumbnail + new_image = BytesIO() + image.crop((geometry.x1, geometry.y1, geometry.x2, geometry.y2)) \ + .resize((round(ratio * thumb_size[0]), round(ratio * thumb_size[1])), Image.ANTIALIAS) \ + .filter(ImageFilter.SHARPEN) \ + .save(new_image, format) + return new_image, format.lower() diff -r 000000000000 -r 63811b2a5670 src/pyams_file/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/interfaces/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,255 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from z3c.form.interfaces import IFileWidget as IBaseFileWidget +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.lifecycleevent.interfaces import IObjectModifiedEvent +from zope.schema.interfaces import IBytes + +# import packages +from zope.interface import implementer, Interface, Attribute +from zope.lifecycleevent import ObjectModifiedEvent +from zope.schema import Bytes, BytesLine, Int, Text, TextLine, Choice + +from pyams_file import _ + + +# +# Main file objects interfaces +# + +class IFileModifiedEvent(IObjectModifiedEvent): + """Modified file event interface""" + + +@implementer(IFileModifiedEvent) +class FileModifiedEvent(ObjectModifiedEvent): + """Modified file event""" + + +class IFile(IAttributeAnnotatable): + """File object interface""" + + content_type = BytesLine(title="Content type", + description="The content type identifies the type of content data", + required=False, + default=b'', + missing_value=b'') + + data = Bytes(title="Content data", + description="Actual file content", + required=False, + default=b'', + missing_value=b'') + + def get_size(self): + """Returns the byte-size of object's data""" + + def get_blob(self, mode='r'): + """Get Blob file associated with this object""" + + +class IMediaFile(IFile): + """Multimedia file""" + + +class IImage(IMediaFile): + """Image object interface""" + + def get_image_size(self): + """Returns an (x, y) tuple describing image dimensions""" + + def resize(self, width, height): + """Resize image to given dimensions""" + + def crop(self, x1, y1, x2, y2): + """Crop image to given coordinates""" + + +class IVideo(IMediaFile): + """Video object interface""" + + +class IAudio(IMediaFile): + """Audio file interface""" + + +class IFileInfo(Interface): + """File extended information""" + + title = TextLine(title=_("Title"), + required=False) + + description = Text(title=_("Description"), + required=False) + + filename = TextLine(title=_("Save file as..."), + description=_("Name under which the file will be saved"), + required=False) + + language = Choice(title=_("Language"), + description=_("File's content language"), + vocabulary="PyAMS base languages", + required=False) + + +class IFileFieldContainer(IAttributeAnnotatable): + """Marker interface for contents holding file properties""" + + +# +# Schema fields interfaces +# + +class DELETED_FILE(object): + def __repr__(self): + return '' +DELETED_FILE = DELETED_FILE() + + +class IFileField(IBytes): + """File object field interface""" + + schema = Attribute("Required value schema") + + +class IMediaField(IFileField): + """Media file object field interface""" + + +class IImageField(IMediaField): + """Image file object field interface""" + + +class IThumbnailImageField(IImageField): + """Image object field with cthumb interface""" + + +class IVideoField(IMediaField): + """Video file field interface""" + + +class IAudioField(IMediaField): + """Audio file field interface""" + + +# +# Widgets interfaces +# + +class IFileWidget(IBaseFileWidget): + """File field widget""" + + +class IImageWidget(IFileWidget): + """Image field widget""" + + +class IThumnailImageWidget(IImageWidget): + """Image field widget with thumbnail selection""" + + +# +# Thumbnails interfaces +# + +class IThumbnailGeometry(Interface): + """Image thumbnail geometry interface""" + + x1 = Int(title="Thumbnail position X1", + required=True, + min=0) + + y1 = Int(title="Thumbnail position Y1", + required=True, + min=0) + + x2 = Int(title="Thumbnail position X2", + required=True, + min=0) + + y2 = Int(title="Thumbnail position Y2", + required=True, + min=0) + + +class IThumbnailer(Interface): + """Interface of adapter used to generate image thumbnails""" + + def get_default_geometry(self): + """Get default thumbnail geometry""" + + def create_thumbnail(self, target_size, format=None): + """Create thumbnail of the given source object + + Source can be any file which can provide thumbnails (image, video, + PDF file...) + target_size is the size of the created thumbnail, as an (width, height) tuple. + + If the requested image is of a resolution higher than that of the original file, + the resulting image resolution will be that of the original file. + + If format (JPEG, PNG...) is given, this will be the format of the generated + thumbnail; otherwise the selected format + """ + + +class IThumbnail(Interface): + """Image thumbnail interface + + Displays are images thumbnails generated 'on the fly' and stored into image + annotations for future use + """ + + def get_image_size(self): + """Get original image size""" + + def get_thumbnail_size(self, thumbnail_name, forced=False): + """Get real size of the genrated thumbnail + + If forced is True, the generated thumbnail can be larger than the original + source + """ + + def get_thumbnail_geometry(self, thumbnail_name): + """Get geometry of a given thumbnail""" + + def set_thumbnail_geometry(self, thumbnail_name, geometry): + """Set geometry for given thumbnail""" + + def clear_geometries(self): + """Remove all stored geometries from object annotations""" + + def get_thumbnail_name(self, thumbnail_name, with_size=None): + """Get matching name for the given thumbnail name or size""" + + def get_thumbnail(self, thumbnail_name, format=None): + """Get requested thumbnail + + Display can be specified as: + - a name matching a custom thumbnailer utility + - a width, as wXXX where XXX is the requested image width + - a height, as hYYY, where YYY is the requested image height + - a size, as XXXxYYY + """ + + def delete_thumbnail(self, thumbnail_name): + """Remove selected thumbnail from object annotations""" + + def clear_thumbnails(self): + """Remove all thumbnails from object annotations""" diff -r 000000000000 -r 63811b2a5670 src/pyams_file/interfaces/archive.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/interfaces/archive.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,34 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages +from zope.interface import Interface + + +class IArchiveExtractor(Interface): + """Archive contents extractor""" + + def initialize(self, data, mode='r'): + """Initialize extractor for given data""" + + def get_contents(self): + """Get list of archive contents + + Each result item is a tuple containing data and file name + """ diff -r 000000000000 -r 63811b2a5670 src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo Binary file src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.mo has changed diff -r 000000000000 -r 63811b2a5670 src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,163 @@ +# +# French translations for PACKAGE package +# This file is distributed under the same license as the PACKAGE package. +# Thierry Florac , 2015. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-02-10 17:59+0100\n" +"PO-Revision-Date: 2015-02-06 21:39+0100\n" +"Last-Translator: Thierry Florac \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Lingua 3.8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: src/pyams_file/widget/templates/image-display.pt:13 +#: src/pyams_file/widget/templates/image-input.pt:41 +#: src/pyams_file/widget/templates/file-input.pt:35 +#: src/pyams_file/widget/templates/file-display.pt:7 +msgid "Current value:" +msgstr "Contenu actuel :" + +#: src/pyams_file/widget/templates/image-display.pt:22 +#: src/pyams_file/widget/templates/image-input.pt:50 +#: src/pyams_file/widget/templates/file-input.pt:41 +#: src/pyams_file/widget/templates/file-display.pt:13 +msgid "${size} Kb" +msgstr "${size} Ko" + +#: src/pyams_file/widget/templates/image-display.pt:27 +#: src/pyams_file/widget/templates/image-input.pt:57 +#: src/pyams_file/widget/templates/file-input.pt:48 +#: src/pyams_file/widget/templates/file-display.pt:16 +msgid "Download" +msgstr "Télécharger" + +#: src/pyams_file/widget/templates/image-input.pt:9 +#: src/pyams_file/widget/templates/file-input.pt:9 +msgid "Browse..." +msgstr "Parcourir..." + +#: src/pyams_file/widget/templates/image-input.pt:12 +#: src/pyams_file/widget/templates/file-input.pt:12 +msgid "Please select a file..." +msgstr "Veuillez sélectionner un fichier..." + +#: src/pyams_file/widget/templates/image-input.pt:24 +#: src/pyams_file/widget/templates/file-input.pt:24 +msgid "Delete content" +msgstr "Supprimer ce contenu" + +#: src/pyams_file/zmi/file.py:41 +msgid "Properties..." +msgstr "Propriétés..." + +#: src/pyams_file/zmi/file.py:52 +msgid "Update file properties" +msgstr "Mise à jour des propriétés" + +#: src/pyams_file/zmi/image.py:56 +msgid "Resize image..." +msgstr "Redimensionner l'image..." + +#: src/pyams_file/zmi/image.py:107 src/pyams_file/zmi/image.py:67 +msgid "Resize image" +msgstr "Redimensionner l'image" + +#: src/pyams_file/zmi/image.py:141 +msgid "" +"You can use this form to change image dimensions.\n" +"\n" +"This will generate a new image only if requested size is smaller than the " +"original one." +msgstr "" +"Vous pouvez utiliser ce formulaire pour changer la taille de l'image.\n" +"\n" +"Une nouvelle image ne sera générée que si les dimensions indiquées sont inférieures " +"à la taille du fichier actuel." + +#: src/pyams_file/zmi/image.py:152 +msgid "Crop image..." +msgstr "Recadrer l'image..." + +#: src/pyams_file/zmi/image.py:170 +msgid "Crop image" +msgstr "Recadrer l'image" + +#: src/pyams_file/zmi/image.py:231 +msgid "Select square thumbnail..." +msgstr "Vignette carrée..." + +#: src/pyams_file/zmi/image.py:242 +msgid "Select square thumbnail" +msgstr "Sélection de l'emprise d'une vignette carrée" + +#: src/pyams_file/zmi/image.py:290 +msgid "Select panoramic thumbnail..." +msgstr "Vignette panoramique..." + +#: src/pyams_file/zmi/image.py:306 +msgid "Select panoramic thumbnail" +msgstr "Sélection de l'emprise d'une vignette panoramique" + +#: src/pyams_file/zmi/image.py:66 src/pyams_file/zmi/image.py:162 +#: src/pyams_file/zmi/image.py:222 +msgid "Close" +msgstr "Fermer" + +#: src/pyams_file/zmi/image.py:73 +msgid "New image width" +msgstr "Largeur de l'image" + +#: src/pyams_file/zmi/image.py:75 +msgid "New image height" +msgstr "Hauteur de l'image" + +#: src/pyams_file/zmi/image.py:77 +msgid "Keep aspect ratio" +msgstr "Ne pas déformer l'image" + +#: src/pyams_file/zmi/image.py:78 +msgid "" +"Check to keep original aspect ratio; image will be resized as large as " +"possible within given limits" +msgstr "" +"Sélectionnez 'oui' pour conserver le rapport hauteur/largeur de l'image. " +"L'image sera redimensionnée (sans jamais être agrandie !) pour être aussi " +"grande que possible en fonction des contraintes indiquées." + +#: src/pyams_file/zmi/image.py:163 +msgid "Crop" +msgstr "Recadrer l'image" + +#: src/pyams_file/zmi/image.py:223 +msgid "Select thumbnail" +msgstr "Sélectionner cette vignette" + +#: src/pyams_file/interfaces/__init__.py:95 +msgid "Title" +msgstr "Titre" + +#: src/pyams_file/interfaces/__init__.py:98 +msgid "Description" +msgstr "Description" + +#: src/pyams_file/interfaces/__init__.py:101 +msgid "Save file as..." +msgstr "Enregistrer sous..." + +#: src/pyams_file/interfaces/__init__.py:102 +msgid "Name under which the file will be saved" +msgstr "Nom proposé automatiquement lors de l'enregistrement du fichier" + +#: src/pyams_file/interfaces/__init__.py:105 +msgid "Language" +msgstr "Langue" + +#: src/pyams_file/interfaces/__init__.py:106 +msgid "File's content language" +msgstr "Langue du contenu du fichier" diff -r 000000000000 -r 63811b2a5670 src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po~ --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/locales/fr/LC_MESSAGES/pyams_file.po~ Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,130 @@ +# +# French translations for PACKAGE package +# This file is distributed under the same license as the PACKAGE package. +# Thierry Florac , 2015. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-02-10 11:50+0100\n" +"PO-Revision-Date: 2015-02-06 21:39+0100\n" +"Last-Translator: Thierry Florac \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Lingua 3.8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: src/pyams_file/widget/templates/image-display.pt:13 +#: src/pyams_file/widget/templates/image-input.pt:41 +#: src/pyams_file/widget/templates/file-input.pt:35 +#: src/pyams_file/widget/templates/file-display.pt:7 +msgid "Current value:" +msgstr "Contenu actuel :" + +#: src/pyams_file/widget/templates/image-display.pt:22 +#: src/pyams_file/widget/templates/image-input.pt:50 +#: src/pyams_file/widget/templates/file-input.pt:41 +#: src/pyams_file/widget/templates/file-display.pt:13 +msgid "${size} Kb" +msgstr "${size} Ko" + +#: src/pyams_file/widget/templates/image-display.pt:27 +#: src/pyams_file/widget/templates/image-input.pt:57 +#: src/pyams_file/widget/templates/file-input.pt:48 +#: src/pyams_file/widget/templates/file-display.pt:16 +msgid "Download" +msgstr "Télécharger" + +#: src/pyams_file/widget/templates/image-input.pt:9 +#: src/pyams_file/widget/templates/file-input.pt:9 +msgid "Browse..." +msgstr "Parcourir..." + +#: src/pyams_file/widget/templates/image-input.pt:12 +#: src/pyams_file/widget/templates/file-input.pt:12 +msgid "Please select a file..." +msgstr "Veuillez sélectionner un fichier..." + +#: src/pyams_file/widget/templates/image-input.pt:24 +#: src/pyams_file/widget/templates/file-input.pt:24 +msgid "Delete content" +msgstr "Supprimer ce contenu" + +#: src/pyams_file/zmi/file.py:41 +msgid "Properties..." +msgstr "Propriétés..." + +#: src/pyams_file/zmi/file.py:52 +msgid "Update file properties" +msgstr "Mise à jour des propriétés" + +#: src/pyams_file/zmi/image.py:53 +msgid "Resize image..." +msgstr "Redimensionner l'image..." + +#: src/pyams_file/zmi/image.py:104 src/pyams_file/zmi/image.py:64 +msgid "Resize image" +msgstr "Redimensionner l'image" + +#: src/pyams_file/zmi/image.py:138 +msgid "Crop image..." +msgstr "Recadrer l'image..." + +#: src/pyams_file/zmi/image.py:156 +msgid "Crop image" +msgstr "Recadrer l'image" + +#: src/pyams_file/zmi/image.py:63 src/pyams_file/zmi/image.py:148 +msgid "Close" +msgstr "Fermer" + +#: src/pyams_file/zmi/image.py:70 +msgid "New image width" +msgstr "Largeur de l'image" + +#: src/pyams_file/zmi/image.py:72 +msgid "New image height" +msgstr "Hauteur de l'image" + +#: src/pyams_file/zmi/image.py:74 +msgid "Keep aspect ratio" +msgstr "Ne pas déformer l'image" + +#: src/pyams_file/zmi/image.py:75 +msgid "" +"Check to keep original aspect ratio; image will be resized as large as " +"possible within given limits" +msgstr "" +"Sélectionnez 'oui' pour conserver le rapport hauteur/largeur de l'image. " +"L'image sera redimensionnée pour être aussi grande que possible en fonction " +"des dimensions indiquées." + +#: src/pyams_file/zmi/image.py:149 +msgid "Crop" +msgstr "Recadrer l'image" + +#: src/pyams_file/interfaces/__init__.py:95 +msgid "Title" +msgstr "Titre" + +#: src/pyams_file/interfaces/__init__.py:98 +msgid "Description" +msgstr "Description" + +#: src/pyams_file/interfaces/__init__.py:101 +msgid "Save file as..." +msgstr "Enregistrer sous..." + +#: src/pyams_file/interfaces/__init__.py:102 +msgid "Name under which the file will be saved" +msgstr "Nom proposé automatiquement lors de l'enregistrement du fichier" + +#: src/pyams_file/interfaces/__init__.py:105 +msgid "Language" +msgstr "Langue" + +#: src/pyams_file/interfaces/__init__.py:106 +msgid "File's content language" +msgstr "Langue du contenu du fichier" diff -r 000000000000 -r 63811b2a5670 src/pyams_file/locales/pyams_file.pot --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/locales/pyams_file.pot Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,155 @@ +# +# SOME DESCRIPTIVE TITLE +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2015. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-02-10 17:59+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Lingua 3.8\n" + +#: ./src/pyams_file/widget/templates/image-display.pt:13 +#: ./src/pyams_file/widget/templates/image-input.pt:41 +#: ./src/pyams_file/widget/templates/file-input.pt:35 +#: ./src/pyams_file/widget/templates/file-display.pt:7 +msgid "Current value:" +msgstr "" + +#: ./src/pyams_file/widget/templates/image-display.pt:22 +#: ./src/pyams_file/widget/templates/image-input.pt:50 +#: ./src/pyams_file/widget/templates/file-input.pt:41 +#: ./src/pyams_file/widget/templates/file-display.pt:13 +msgid "${size} Kb" +msgstr "" + +#: ./src/pyams_file/widget/templates/image-display.pt:27 +#: ./src/pyams_file/widget/templates/image-input.pt:57 +#: ./src/pyams_file/widget/templates/file-input.pt:48 +#: ./src/pyams_file/widget/templates/file-display.pt:16 +msgid "Download" +msgstr "" + +#: ./src/pyams_file/widget/templates/image-input.pt:9 +#: ./src/pyams_file/widget/templates/file-input.pt:9 +msgid "Browse..." +msgstr "" + +#: ./src/pyams_file/widget/templates/image-input.pt:12 +#: ./src/pyams_file/widget/templates/file-input.pt:12 +msgid "Please select a file..." +msgstr "" + +#: ./src/pyams_file/widget/templates/image-input.pt:24 +#: ./src/pyams_file/widget/templates/file-input.pt:24 +msgid "Delete content" +msgstr "" + +#: ./src/pyams_file/zmi/file.py:41 +msgid "Properties..." +msgstr "" + +#: ./src/pyams_file/zmi/file.py:52 +msgid "Update file properties" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:56 +msgid "Resize image..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:107 ./src/pyams_file/zmi/image.py:67 +msgid "Resize image" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:141 +msgid "" +"You can use this form to change image dimensions.\n" +"\n" +"This will generate a new image only if requested size is smaller than the original one." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:152 +msgid "Crop image..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:170 +msgid "Crop image" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:231 +msgid "Select square thumbnail..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:242 +msgid "Select square thumbnail" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:290 +msgid "Select panoramic thumbnail..." +msgstr "" + +#: ./src/pyams_file/zmi/image.py:306 +msgid "Select panoramic thumbnail" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:66 ./src/pyams_file/zmi/image.py:162 +#: ./src/pyams_file/zmi/image.py:222 +msgid "Close" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:73 +msgid "New image width" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:75 +msgid "New image height" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:77 +msgid "Keep aspect ratio" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:78 +msgid "" +"Check to keep original aspect ratio; image will be resized as large as " +"possible within given limits" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:163 +msgid "Crop" +msgstr "" + +#: ./src/pyams_file/zmi/image.py:223 +msgid "Select thumbnail" +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:95 +msgid "Title" +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:98 +msgid "Description" +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:101 +msgid "Save file as..." +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:102 +msgid "Name under which the file will be saved" +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:105 +msgid "Language" +msgstr "" + +#: ./src/pyams_file/interfaces/__init__.py:106 +msgid "File's content language" +msgstr "" diff -r 000000000000 -r 63811b2a5670 src/pyams_file/property.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/property.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,110 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_file.interfaces import IFile, IFileInfo, IFileFieldContainer, DELETED_FILE +from z3c.form.interfaces import NOT_CHANGED +from zope.annotation.interfaces import IAnnotations +from zope.schema.interfaces import IField + +# import packages +from pyams_file.file import FileFactory +from pyams_utils.request import check_request +from zope.interface import alsoProvides +from zope.lifecycleevent import ObjectCreatedEvent, ObjectRemovedEvent, ObjectAddedEvent +from zope.location.location import locate + + +FILE_CONTAINER_ATTRIBUTES = 'pyams_file.file.attributes' + +_marker = object() + + +class FileProperty(object): + """Property class used to handle files""" + + def __init__(self, field, name=None, klass=None, **args): + if not IField.providedBy(field): + raise ValueError("Provided field must implement IField interface...") + if name is None: + name = field.__name__ + self.__field = field + self.__name = name + self.__klass = klass + self.__args = args + + def __get__(self, instance, klass): + if instance is None: + return self + value = instance.__dict__.get(self.__name, _marker) + if value is _marker: + field = self.__field.bind(instance) + value = getattr(field, 'default', _marker) + if value is _marker: + raise AttributeError(self.__name) + return value + + def __set__(self, instance, value): + if value is NOT_CHANGED: + return + registry = check_request().registry + if (value is not None) and (value is not DELETED_FILE): + filename = None + # file upload data converter returns a tuple containing + # filename and buffered IO stream extracted from FieldStorage... + if isinstance(value, tuple): + filename, value = value + # initialize file through factory + if not IFile.providedBy(value): + factory = self.__klass or FileFactory + file = factory(value, **self.__args) + registry.notify(ObjectCreatedEvent(file)) + if not file.get_size(): + value.seek(0) # because factory may read until end of file... + file.data = value + value = file + if filename is not None: + info = IFileInfo(value) + if info is not None: + info.filename = filename + field = self.__field.bind(instance) + field.validate(value) + if field.readonly and instance.__dict__.has_key(self.__name): + raise ValueError(self.__name, "Field is readonly") + old_value = instance.__dict__.get(self.__name, _marker) + if old_value != value: + # check for previous value + if (old_value is not _marker) and (old_value is not None): + registry.notify(ObjectRemovedEvent(old_value)) + if value is DELETED_FILE: + if self.__name in instance.__dict__: + del instance.__dict__[self.__name] + else: + # set name of new value + name = '++attr++{0}'.format(self.__name) + if value is not None: + locate(value, instance, name) + instance.__dict__[self.__name] = value + # store file attributes of instance + if not IFileFieldContainer.providedBy(instance): + alsoProvides(instance, IFileFieldContainer) + annotations = IAnnotations(instance) + attributes = annotations.get(FILE_CONTAINER_ATTRIBUTES) + if attributes is None: + attributes = annotations[FILE_CONTAINER_ATTRIBUTES] = set() + attributes.add(self.__name) + registry.notify(ObjectAddedEvent(value, instance, name)) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/schema.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,78 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + +# import standard library + +# import interfaces +from pyams_file.interfaces import IFile, IFileField, IMediaFile, IMediaField, IImage, IImageField, IThumbnailImageField, \ + IVideo, IVideoField, IAudio, IAudioField, DELETED_FILE +from zope.schema.interfaces import WrongType + +# import packages +from zope.interface import implementer +from zope.schema import Bytes + + +@implementer(IFileField) +class FileField(Bytes): + """Custom field used to handle file-like properties""" + + schema = IFile + + def _validate(self, value): + if value is DELETED_FILE: + return + elif isinstance(value, tuple): + try: + filename, stream = value + if not isinstance(filename, str): + raise WrongType(filename, str, '{0}.filename' % self.__name__) + if not hasattr(stream, 'read'): + raise WrongType(stream, '', self.__name__) + except ValueError: + raise WrongType(value, tuple, self.__name__) + elif not self.schema.providedBy(value): + raise WrongType(value, self.schema, self.__name__) + + +@implementer(IMediaField) +class MediaField(FileField): + """Custom field used to store media-like properties""" + + schema = IMediaFile + + +@implementer(IImageField) +class ImageField(MediaField): + """Custom field used to handle image properties""" + + schema = IImage + + +@implementer(IThumbnailImageField) +class CthumbImageField(ImageField): + """Custom field used to handle images properties with square selection""" + + +@implementer(IVideoField) +class VideoField(MediaField): + """Custom field used to store video file""" + + schema = IVideo + +@implementer(IAudioField) +class AudioField(MediaField): + """Custom field used to store audio file""" + + schema = IAudio diff -r 000000000000 -r 63811b2a5670 src/pyams_file/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/tests/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/tests/test_utilsdocs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/tests/test_utilsdocs.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,59 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +Generic Test case for pyams_file doctest +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.dirname(__file__) + +def doc_suite(test_dir, setUp=None, tearDown=None, globs=None): + """Returns a test suite, based on doctests found in /doctest.""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + doctest_dir = os.path.join(package_dir, 'doctests') + + # filtering files on extension + docs = [os.path.join(doctest_dir, doc) for doc in + os.listdir(doctest_dir) if doc.endswith('.txt')] + + for test in docs: + suite.append(doctest.DocFileSuite(test, optionflags=flags, + globs=globs, setUp=setUp, + tearDown=tearDown, + module_relative=False)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/tests/test_utilsdocstrings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/tests/test_utilsdocstrings.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,62 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +Generic Test case for pyams_file doc strings +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.abspath(os.path.dirname(__file__)) + +def doc_suite(test_dir, globs=None): + """Returns a test suite, based on doc tests strings found in /*.py""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + # filtering files on extension + docs = [doc for doc in + os.listdir(package_dir) if doc.endswith('.py')] + docs = [doc for doc in docs if not doc.startswith('__')] + + for test in docs: + fd = open(os.path.join(package_dir, test)) + content = fd.read() + fd.close() + if '>>> ' not in content: + continue + test = test.replace('.py', '') + location = 'pyams_file.%s' % test + suite.append(doctest.DocTestSuite(location, optionflags=flags, + globs=globs)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff -r 000000000000 -r 63811b2a5670 src/pyams_file/thumbnail.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/thumbnail.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,218 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import re +import transaction + +# import interfaces +from pyams_file.interfaces import IImage, IThumbnail, IThumbnailer, IFileModifiedEvent +from pyams_utils.interfaces.tales import ITALESExtension +from zope.annotation.interfaces import IAnnotations +from zope.traversing.interfaces import ITraversable + +# import packages +from persistent.dict import PersistentDict +from pyams_file.file import FileFactory +from pyams_utils.adapter import ContextAdapter, ContextRequestViewAdapter, adapter_config +from pyams_utils.request import check_request +from pyramid.events import subscriber +from zope.component import adapter +from zope.interface import implementer, Interface +from zope.lifecycleevent import ObjectAddedEvent, ObjectRemovedEvent, ObjectCreatedEvent +from zope.location import locate + + +THUMBNAIL_ANNOTATIONS_KEY = 'pyams_file.image.thumbnails' +THUMBNAIL_GEOMETRY_KEY = 'pyams_file.image.geometry' + +THUMB_WIDTH = re.compile('^w([0-9]+)$') +THUMB_HEIGHT = re.compile('^h([0-9]+)$') +THUMB_SIZE = re.compile('^([0-9]+)x([0-9]+)$') + + +@adapter_config(context=IImage, provides=IThumbnail) +class ImageThumbnailAdapter(object): + """Image thumbnails adapter""" + + def __init__(self, image): + self.image = image + annotations = IAnnotations(image) + thumbnails = annotations.get(THUMBNAIL_ANNOTATIONS_KEY) + if thumbnails is None: + thumbnails = annotations[THUMBNAIL_ANNOTATIONS_KEY] = PersistentDict() + self.thumbnails = thumbnails + + def get_image_size(self): + return self.image.get_image_size() + + def get_thumbnail_size(self, thumbnail_name, forced=False): + width, height = self.get_image_size() + match = THUMB_WIDTH.match(thumbnail_name) + if match: + w = int(match.groups()[0]) + w_ratio = 1. * width / w + h_ratio = 0. + else: + match = THUMB_HEIGHT.match(thumbnail_name) + if match: + h = int(match.group()[0]) + w_ratio = 0. + h_ratio = 1. * height / h + else: + match = THUMB_SIZE.match(thumbnail_name) + if match: + groups = match.groups() + w = int(groups[0]) + h = int(groups[1]) + w_ratio = 1. * width / w + h_ratio = 1. * height / h + if match: + ratio = max(w_ratio, h_ratio) + if not forced: + ratio = max(ratio, 1.) + return int(width / ratio), int(height / ratio) + else: + return None + + def get_thumbnail_geometry(self, thumbnail_name): + annotations = IAnnotations(self.image) + geometries = annotations.get(THUMBNAIL_GEOMETRY_KEY, {}) + # get default geometry for custom thumbnails + if ':' in thumbnail_name: + thumbnailer_name, options = thumbnail_name.split(':', 1) + else: + thumbnailer_name = thumbnail_name + options = None + if thumbnailer_name in geometries: + return geometries[thumbnailer_name] + registry = check_request().registry + thumbnailer = registry.queryAdapter(self.image, IThumbnailer, name=thumbnailer_name) + if thumbnailer is not None: + return thumbnailer.get_default_geometry(options) + + def set_thumbnail_geometry(self, thumbnail_name, geometry): + annotations = IAnnotations(self.image) + geometries = annotations.get(THUMBNAIL_GEOMETRY_KEY) + if geometries is None: + geometries = annotations[THUMBNAIL_GEOMETRY_KEY] = PersistentDict() + geometries[thumbnail_name] = geometry + for current_thumbnail_name in self.thumbnails.copy(): + if current_thumbnail_name.startswith(thumbnail_name): + self.delete_thumbnail(current_thumbnail_name) + + def clear_geometries(self): + annotations = IAnnotations(self.image) + geometries = annotations.get(THUMBNAIL_GEOMETRY_KEY) + if geometries is not None: + for geometry_name in geometries.copy(): + del geometries[geometry_name] + + def get_thumbnail_name(self, thumbnail_name, with_size=False): + size = self.get_thumbnail_size(thumbnail_name) + if size is not None: + if with_size: + return '{0}x{1}'.format(*size), size + else: + return '{0}x{1}'.format(*size) + else: + return None, None + + def get_thumbnail(self, thumbnail_name, format=None): + # check for existing thumbnail + if thumbnail_name in self.thumbnails: + return self.thumbnails[thumbnail_name] + # check for matching one + name, size = self.get_thumbnail_name(thumbnail_name, with_size=True) + if name: + if name in self.thumbnails: + return self.thumbnails[name] + # check for original image + if size == self.get_image_size(): + return self.image + # wee will look for default image thumbnailer + thumbnailer_name = '' + options = name + else: + if ':' in thumbnail_name: + thumbnailer_name, options = thumbnail_name.split(':', 1) + else: + thumbnailer_name = thumbnail_name + options = name = thumbnail_name + # generate and store thumbnail + registry = check_request().registry + thumbnailer = registry.queryAdapter(self.image, IThumbnailer, name=thumbnailer_name) + if thumbnailer is not None: + thumbnail_image = thumbnailer.create_thumbnail(options, format) + if thumbnail_image is not None: + if isinstance(thumbnail_image, tuple): + thumbnail_image, format = thumbnail_image + else: + format = 'jpeg' + thumbnail_image = FileFactory(thumbnail_image) + registry.notify(ObjectCreatedEvent(thumbnail_image)) + self.thumbnails[name] = thumbnail_image + thumbnail_size = thumbnail_image.get_image_size() + locate(thumbnail_image, self.image, + '++thumb++{0}{1}{2}x{3}.{4}'.format(thumbnailer_name, + ':' if thumbnailer_name else '', + thumbnail_size[0], + thumbnail_size[1], + format)) + registry.notify(ObjectAddedEvent(thumbnail_image)) + return thumbnail_image + + def delete_thumbnail(self, thumbnail_name): + if thumbnail_name in self.thumbnails: + thumbnail_image = self.thumbnails[thumbnail_name] + registry = check_request().registry + registry.notify(ObjectRemovedEvent(thumbnail_image)) + del self.thumbnails[thumbnail_name] + + def clear_thumbnails(self): + [self.delete_thumbnail(thumbnail_name) for thumbnail_name in self.thumbnails.copy()] + + +@subscriber(IFileModifiedEvent, context_selector=IImage) +def handle_modified_file(event): + thumbnail = IThumbnail(event.object) + thumbnail.clear_geometries() + thumbnail.clear_thumbnails() + + +@adapter_config(name='thumb', context=IImage, provides=ITraversable) +class ThumbnailTraverser(ContextAdapter): + """++thumb++ namespace traverser""" + + def traverse(self, name, furtherpath=None): + if '.' in name: + thumbnail_name, format = name.rsplit('.', 1) + else: + thumbnail_name = name + format = None + thumbnails = IThumbnail(self.context) + result = thumbnails.get_thumbnail(thumbnail_name, format) + transaction.commit() + return result + + +@adapter_config(name='thumbnails', context=(Interface, Interface, Interface), provides=ITALESExtension) +class ThumbnailsExtension(ContextRequestViewAdapter): + """extension:thumbnails(image) TALES extension""" + + def render(self, context=None): + if context is None: + context = self.context + return IThumbnail(context) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/views/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/views/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,20 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages diff -r 000000000000 -r 63811b2a5670 src/pyams_file/views/file.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/views/file.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,61 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from http.client import NOT_MODIFIED + +# import interfaces +from pyams_file.interfaces import IFile, IImage +from zope.dublincore.interfaces import IZopeDublinCore + +# import packages +from pyramid.response import Response +from pyramid.view import view_config + + +@view_config(context=IFile) +def FileView(request): + """Default file view""" + context = request.context + + # check for last modification date + modified = IZopeDublinCore(context).modified + if_modified_since = request.if_modified_since + if if_modified_since and (int(modified.timestamp()) <= int(if_modified_since.timestamp())): + return Response(status=NOT_MODIFIED) + + headers = {'Content-Disposition': 'attachment; filename="{0}"'.format(context.filename)} + response = Response(content_type=context.content_type.decode('utf-8'), + headers=headers) + response.last_modified = modified + response.body_file = context.get_blob(mode='c') + return response + + +@view_config(context=IImage) +def ImageView(request): + """Default image view""" + context = request.context + + # check for last modification date + modified = IZopeDublinCore(context).modified + if_modified_since = request.if_modified_since + if if_modified_since and (int(modified.timestamp()) <= int(if_modified_since.timestamp())): + return Response(status=NOT_MODIFIED) + + response = Response(content_type=context.content_type.decode('utf-8')) + response.last_modified = modified + response.body_file = context.get_blob(mode='c') + return response diff -r 000000000000 -r 63811b2a5670 src/pyams_file/widget/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/widget/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,98 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from cgi import FieldStorage + +# import interfaces +from pyams_file.interfaces import IFileField, IFileWidget, IImageField, IImageWidget, \ + IThumnailImageWidget, IThumbnailImageField, DELETED_FILE +from pyams_form.interfaces.form import IFormLayer +from z3c.form.interfaces import IFieldWidget, NOT_CHANGED, IDataConverter + +# import packages +from pyams_utils.adapter import adapter_config +from z3c.form.browser.file import FileWidget as BaseFileWidget +from z3c.form.converter import BaseDataConverter +from z3c.form.util import toBytes +from z3c.form.widget import FieldWidget +from zope.interface import implementer_only + + +@adapter_config(context=(IFileField, IFileWidget), provides=IDataConverter) +class FileUploadDataConverter(BaseDataConverter): + """File upload data converter""" + + def toWidgetValue(self, value): + return value + + def toFieldValue(self, value): + deleted_field_name = '{0}__deleted'.format(self.widget.name) + deleted = self.widget.request.params.get(deleted_field_name) + if deleted: + return DELETED_FILE + if value is None or value == '': + return NOT_CHANGED + elif isinstance(value, FieldStorage): + return value.filename, value.file + else: + return toBytes(value) + + +@implementer_only(IFileWidget) +class FileWidget(BaseFileWidget): + """File widget""" + + label_css_class = 'input input-file' + + @property + def current_value(self): + if self.form.ignoreContext: + return None + return self.field.get(self.context) + + @property + def deletable(self): + if self.required: + return False + elif self.value is NOT_CHANGED: + return self.current_value is not None + else: + return self.value is not None + + +@adapter_config(context=(IFileField, IFormLayer), provides=IFieldWidget) +def FileFieldWidget(field, request): + return FieldWidget(field, FileWidget(request)) + + +@implementer_only(IImageWidget) +class ImageWidget(FileWidget): + """Image widget""" + + +@adapter_config(context=(IImageField, IFormLayer), provides=IFieldWidget) +def ImageFieldWidget(field, request): + return FieldWidget(field, ImageWidget(request)) + + +@implementer_only(IThumnailImageWidget) +class ThumbnailImageWidget(ImageWidget): + """Image widget with thumbnail images selection""" + + +@adapter_config(context=(IThumbnailImageField, IFormLayer), provides=IFieldWidget) +def ThumbnailImageFieldWidget(field, request): + return FieldWidget(field, ThumbnailImageWidget(request)) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/widget/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/widget/configure.zcml Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/widget/templates/file-display.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/widget/templates/file-display.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,14 @@ + +
+
+ Current value: + +  –  + Kb + Download +
+
+
diff -r 000000000000 -r 63811b2a5670 src/pyams_file/widget/templates/file-input.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/widget/templates/file-input.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,47 @@ + +
+
+ + Browse... +
+
+
+ +
+
+
+ Current value: + +  –  + Kb +
+
+ + Download + + + + + +
+
+
+
diff -r 000000000000 -r 63811b2a5670 src/pyams_file/widget/templates/image-display.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/widget/templates/image-display.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,30 @@ + +
+ + + +
+ Current value: + +  –  + +  –  + Kb +
+ + Download + +
+
+
diff -r 000000000000 -r 63811b2a5670 src/pyams_file/widget/templates/image-input.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/widget/templates/image-input.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,60 @@ + +
+
+ + Browse... +
+
+
+ +
+
+ + + +
+ Current value: + +  –  + +  –  + Kb +
+
+ + Download + + + + + +
+
+
+
diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/__init__.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,20 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/configure.zcml Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,6 @@ + + + + + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/file.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/file.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,79 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_file.interfaces import IFile, IFileInfo +from pyams_skin.interfaces.viewlet import IContextActions +from pyams_skin.layer import IPyAMSLayer + +# import packages +from pyams_form.form import AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogEditForm +from pyramid.view import view_config +from z3c.form import field +from zope.interface import Interface + +from pyams_file import _ + + +@viewlet_config(name='file.properties.action', context=IFile, layer=IPyAMSLayer, view=Interface, + manager=IContextActions, permission='view', weight=1) +class FileRenameAction(ToolbarMenuItem): + """File rename action""" + + label = _("Properties...") + label_css_class = 'fa fa-fw fa-edit' + + url = 'properties.html' + modal_target = True + + +@pagelet_config(name='properties.html', context=IFile, layer=IPyAMSLayer, permission='view') +class FilePropertiesEditForm(AdminDialogEditForm): + """File properties edit form""" + + legend = _("Update file properties") + icon_css_class = 'fa fa-fw fa-edit' + + fields = field.Fields(IFileInfo) + ajax_handler = 'properties.json' + + @property + def title(self): + return self.context.title or self.context.filename + + def updateWidgets(self, prefix=None): + super(FilePropertiesEditForm, self).updateWidgets() + self.widgets['description'].label_css_class = 'textarea' + + +@view_config(name='properties.json', context=IFile, request_type=IPyAMSLayer, + permission='manage', renderer='json', xhr=True) +class FilePropertiesAJAXEditForm(AJAXEditForm, FilePropertiesEditForm): + """File properties edit form, AJAX renderer""" + + def get_ajax_output(self, changes): + info_changes = changes.get(IFileInfo, ()) + if ('title' in info_changes) or ('filename' in info_changes): + return {'status': 'reload', + 'smallbox': self.request.localizer.translate(self.successMessage), + 'smallbox_status': 'success'} + else: + return super(FilePropertiesAJAXEditForm, self).get_ajax_output(changes) diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/image.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/image.py Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,366 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_file.interfaces import IImage, IThumnailImageWidget, IThumbnail +from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager +from pyams_skin.interfaces import IContentHelp +from pyams_skin.interfaces.viewlet import IContextActions +from pyams_skin.layer import IPyAMSLayer +from pyams_zmi.layer import IAdminLayer + +# import packages +from pyams_file.image import ThumbnailGeometrry +from pyams_form.form import AJAXEditForm +from pyams_form.schema import CloseButton +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.help import ContentHelp +from pyams_skin.viewlet.toolbar import ToolbarMenuItem, ToolbarMenuDivider +from pyams_template.template import template_config +from pyams_utils.adapter import adapter_config +from pyams_viewlet.viewlet import viewlet_config, Viewlet +from pyams_zmi.form import AdminDialogEditForm +from pyramid.view import view_config +from z3c.form import field, button +from zope.interface import implementer, Interface +from zope.schema import Int, Bool +from zope.schema.fieldproperty import FieldProperty + +from pyams_file import _ + + +@viewlet_config(name='image.resize.divider', context=IImage, layer=IAdminLayer, view=Interface, + manager=IContextActions, permission='manage', weight=19) +class ImageDividerAction(ToolbarMenuDivider): + """Image divider action""" + + +# +# Image resize +# + +@viewlet_config(name='image.resize.action', context=IImage, layer=IAdminLayer, view=Interface, + manager=IContextActions, permission='manage', weight=20) +class ImageResizeAction(ToolbarMenuItem): + """Image resize action""" + + label = _("Resize image...") + label_css_class = 'fa fa-fw fa-compress' + + url = 'resize.html' + modal_target = True + + +class IResizeFormButtons(Interface): + """Image resize form buttons""" + + close = CloseButton(name='close', title=_("Close")) + resize = button.Button(name='resize', title=_("Resize image")) + + +class IImageResizeInfo(Interface): + """Image resize interface""" + + width = Int(title=_("New image width")) + + height = Int(title=_("New image height")) + + keep_ratio = Bool(title=_("Keep aspect ratio"), + description=_("Check to keep original aspect ratio; image will be resized as large as " + "possible within given limits"), + required=True, + default=True) + + +@implementer(IImageResizeInfo) +class ImageResizeInfo(object): + """Image resize info""" + + width = FieldProperty(IImageResizeInfo['width']) + height = FieldProperty(IImageResizeInfo['height']) + keep_ratio = FieldProperty(IImageResizeInfo['keep_ratio']) + + +@adapter_config(context=IImage, provides=IImageResizeInfo) +def ImageResizeInfoFactory(image): + size = image.get_image_size() + info = ImageResizeInfo() + info.width = size[0] + info.height = size[1] + return info + + +@pagelet_config(name='resize.html', context=IImage, layer=IPyAMSLayer, permission='manage') +class ImageResizeForm(AdminDialogEditForm): + """Image resize form""" + + legend = _("Resize image") + icon_css_class = 'fa fa-fw fa-compress' + + fields = field.Fields(IImageResizeInfo) + buttons = button.Buttons(IResizeFormButtons) + ajax_handler = 'resize.json' + + @property + def title(self): + return self.context.title or self.context.filename + + def updateActions(self): + super(ImageResizeForm, self).updateActions() + if 'resize' in self.actions: + self.actions['resize'].addClass('btn-primary') + + +@view_config(name='resize.json', context=IImage, request_type=IPyAMSLayer, + permission='manage', renderer='json', xhr=True) +class ImageResizeAJAXForm(AJAXEditForm, ImageResizeForm): + """Image resize form, AJAX renderer""" + + def update_content(self, content, data): + self.context.resize(data.get('width'), data.get('height'), data.get('keep_ratio')) + + def get_ajax_output(self, changes): + return {'status': 'reload', + 'smallbox': self.request.localizer.translate(self.successMessage), + 'smallbox_status': 'success'} + + +@adapter_config(context=(IImage, IAdminLayer, ImageResizeForm), provides=IContentHelp) +class ImageResizeFormHelpAdapter(ContentHelp): + """Image resize form help adapter""" + + message = _("""You can use this form to change image dimensions. + +This will generate a new image only if requested size is smaller than the original one.""") + message_format = 'rest' + + +# +# Image crop +# + +@viewlet_config(name='image.crop.action', context=IImage, layer=IAdminLayer, view=Interface, + manager=IContextActions, permission='manage', weight=21) +class ImageCropAction(ToolbarMenuItem): + """Image crop action""" + + label = _("Crop image...") + label_css_class = 'fa fa-fw fa-crop' + + url = 'crop.html' + modal_target = True + + +class ICropFormButtons(Interface): + """Image crop form buttons""" + + close = CloseButton(name='close', title=_("Close")) + crop = button.Button(name='crop', title=_("Crop")) + + +@pagelet_config(name='crop.html', context=IImage, layer=IPyAMSLayer, permission='manage') +class ImageCropForm(AdminDialogEditForm): + """Image crop form""" + + legend = _("Crop image") + icon_css_class = 'fa fa-fw fa-crop' + dialog_class = 'modal-large' + + fields = field.Fields(Interface) + buttons = button.Buttons(ICropFormButtons) + ajax_handler = 'crop.json' + + @property + def title(self): + return self.context.title or self.context.filename + + def updateActions(self): + super(ImageCropForm, self).updateActions() + if 'crop' in self.actions: + self.actions['crop'].addClass('btn-primary') + + +@view_config(name='crop.json', context=IImage, request_type=IPyAMSLayer, + permission='manage', renderer='json', xhr=True) +class ImageCropAJAXForm(AJAXEditForm, ImageCropForm): + """Image crop form, AJAX renderer""" + + def update_content(self, content, data): + image_size = self.context.get_image_size() + x1 = int(self.request.params.get('selection.x1', 0)) + y1 = int(self.request.params.get('selection.y1', 0)) + x2 = int(self.request.params.get('selection.x2', image_size[0])) + y2 = int(self.request.params.get('selection.y2', image_size[1])) + self.context.crop(x1, y1, x2, y2) + + def get_ajax_output(self, changes): + return {'status': 'reload', + 'smallbox': self.request.localizer.translate(self.successMessage), + 'smallbox_status': 'success'} + + +@viewlet_config(name='crop.widgets-prefix', context=IImage, layer=IAdminLayer, view=ImageCropForm, + manager=IWidgetsPrefixViewletsManager) +@template_config(template='templates/image-crop.pt') +class ImageCropViewletsPrefix(Viewlet): + """Image crop viewlets prefix""" + + +# +# Image square thumbnail selection +# + +@viewlet_config(name='image.thumb.divider', context=IImage, layer=IAdminLayer, view=IThumnailImageWidget, + manager=IContextActions, permission='manage', weight=30) +class ImageThumbnailsDividerAction(ToolbarMenuDivider): + """Image divider action""" + + +class IThumbnailFormButtons(Interface): + """Image crop form buttons""" + + close = CloseButton(name='close', title=_("Close")) + crop = button.Button(name='crop', title=_("Select thumbnail")) + + +@viewlet_config(name='image.thumb.square.action', context=IImage, layer=IAdminLayer, view=IThumnailImageWidget, + manager=IContextActions, permission='manage', weight=31) +class ImageSquareThumbnailAction(ToolbarMenuItem): + """Square thumbnail image selection""" + + label = _("Select square thumbnail...") + label_css_class = 'fa fa-fw fa-instagram' + + url = 'square-thumbnail.html' + modal_target = True + + +@pagelet_config(name='square-thumbnail.html', context=IImage, layer=IPyAMSLayer, permission='manage') +class ImageSquareThumbnailEditForm(AdminDialogEditForm): + """Image square thumbnail edit form""" + + legend = _("Select square thumbnail") + icon_css_class = 'fa fa-fw fa-instagram' + dialog_class = 'modal-large' + + fields = field.Fields(Interface) + buttons = button.Buttons(IThumbnailFormButtons) + ajax_handler = 'square-thumbnail.json' + + @property + def title(self): + return self.context.title or self.context.filename + + def updateActions(self): + super(ImageSquareThumbnailEditForm, self).updateActions() + if 'crop' in self.actions: + self.actions['crop'].addClass('btn-primary') + + +@view_config(name='square-thumbnail.json', context=IImage, request_type=IPyAMSLayer, + permission='manage', renderer='json', xhr=True) +class ImageSquareThumbnailAJAXEditForm(AJAXEditForm, ImageSquareThumbnailEditForm): + """Image square thumbnail edit form, AJAX renderer""" + + def update_content(self, content, data): + geometry = ThumbnailGeometrry() + geometry.x1 = int(self.request.params.get('selection.x1')) + geometry.y1 = int(self.request.params.get('selection.y1')) + geometry.x2 = int(self.request.params.get('selection.x2')) + geometry.y2 = int(self.request.params.get('selection.y2')) + IThumbnail(self.context).set_thumbnail_geometry('square', geometry) + + def get_ajax_output(self, changes): + return {'status': 'success', + 'smallbox': self.request.localizer.translate(self.successMessage), + 'smallbox_status': 'success'} + + +@viewlet_config(name='square-thumbnail.widgets-prefix', context=IImage, layer=IAdminLayer, + view=ImageSquareThumbnailEditForm, manager=IWidgetsPrefixViewletsManager) +@template_config(template='templates/image-square-thumbnail.pt') +class ImageSquareThumbnailViewletsPrefix(Viewlet): + """Image square thumbnail viewlets prefix""" + + +# +# Image panoramic thumbnail selection +# + +@viewlet_config(name='image.thumb.pano.action', context=IImage, layer=IAdminLayer, view=IThumnailImageWidget, + manager=IContextActions, permission='manage', weight=32) +class ImagePanoThumbnailAction(ToolbarMenuItem): + """Panoramic thumbnail image selection""" + + label = _("Select panoramic thumbnail...") + label_css_class = 'fa fa-fw fa-youtube-play' + + url = 'pano-thumbnail.html' + modal_target = True + + def updateActions(self): + super(ImageSquareThumbnailEditForm, self).updateActions() + if 'crop' in self.actions: + self.actions['crop'].addClass('btn-primary') + + +@pagelet_config(name='pano-thumbnail.html', context=IImage, layer=IPyAMSLayer, permission='manage') +class ImagePanoThumbnailEditForm(AdminDialogEditForm): + """Image panoramic thumbnail edit form""" + + legend = _("Select panoramic thumbnail") + icon_css_class = 'fa fa-fw fa-youtube-play' + dialog_class = 'modal-large' + + fields = field.Fields(Interface) + buttons = button.Buttons(IThumbnailFormButtons) + ajax_handler = 'pano-thumbnail.json' + + @property + def title(self): + return self.context.title or self.context.filename + + def updateActions(self): + super(ImagePanoThumbnailEditForm, self).updateActions() + if 'crop' in self.actions: + self.actions['crop'].addClass('btn-primary') + + +@view_config(name='pano-thumbnail.json', context=IImage, request_type=IPyAMSLayer, + permission='manage', renderer='json', xhr=True) +class ImagePanoThumbnailAJAXEditForm(AJAXEditForm, ImagePanoThumbnailEditForm): + """Image panoramic thumbnail edit form, AJAX renderer""" + + def update_content(self, content, data): + geometry = ThumbnailGeometrry() + geometry.x1 = int(self.request.params.get('selection.x1')) + geometry.y1 = int(self.request.params.get('selection.y1')) + geometry.x2 = int(self.request.params.get('selection.x2')) + geometry.y2 = int(self.request.params.get('selection.y2')) + IThumbnail(self.context).set_thumbnail_geometry('pano', geometry) + + def get_ajax_output(self, changes): + return {'status': 'success', + 'smallbox': self.request.localizer.translate(self.successMessage), + 'smallbox_status': 'success'} + + +@viewlet_config(name='pano-thumbnail.widgets-prefix', context=IImage, layer=IAdminLayer, + view=ImagePanoThumbnailEditForm, manager=IWidgetsPrefixViewletsManager) +@template_config(template='templates/image-pano-thumbnail.pt') +class ImagePanoThumbnailViewletsPrefix(Viewlet): + """Image panoramic thumbnail viewlets prefix""" diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/templates/image-crop.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/templates/image-crop.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,17 @@ + + + + + + + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/templates/image-pano-thumbnail.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/templates/image-pano-thumbnail.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,23 @@ + + + + + + + diff -r 000000000000 -r 63811b2a5670 src/pyams_file/zmi/templates/image-square-thumbnail.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_file/zmi/templates/image-square-thumbnail.pt Thu Feb 19 10:56:21 2015 +0100 @@ -0,0 +1,23 @@ + + + + + + +