Skip to content

Commit

Permalink
Add zip download to photogallery
Browse files Browse the repository at this point in the history
  • Loading branch information
rodfersou authored and hvelarde committed Jul 3, 2015
1 parent 340fddb commit 8a53bb8
Show file tree
Hide file tree
Showing 27 changed files with 634 additions and 52 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ There's a frood who really knows where his towel is.
1.0a2 (unreleased)
------------------

- Add download functionality to photogallery (closes `#14`_).
[rodfersou, hvelarde]

- Implement lead image to be used in folder and collection views (closes `#17`_).
[hvelarde]

Expand Down Expand Up @@ -37,4 +40,5 @@ There's a frood who really knows where his towel is.
.. _`#4`: https://github.com/simplesconsultoria/sc.photogallery/issues/4
.. _`#7`: https://github.com/simplesconsultoria/sc.photogallery/issues/7
.. _`#10`: https://github.com/simplesconsultoria/sc.photogallery/issues/10
.. _`#14`: https://github.com/simplesconsultoria/sc.photogallery/issues/14
.. _`#17`: https://github.com/simplesconsultoria/sc.photogallery/issues/17
2 changes: 2 additions & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ Share and Enjoy
- Rodrigo Ferreira de Souza
- `Cycle2 slideshow plugin for jQuery`_
- Font Awesome (`icon`_)
- Wolfgang Beyer (`Mandelbrot set images`_ used in tests)

Development sponsored by Simples Consultoria.

.. _`Cycle2 slideshow plugin for jQuery`: http://jquery.malsup.com/cycle2/
.. _`icon`: http://fontawesome.io/icon/picture-o/
.. _`Mandelbrot set images`: https://commons.wikimedia.org/wiki/File:Mandel_zoom_00_mandelbrot_set.jpg
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
'plone.app.referenceablebehavior',
'plone.app.relationfield',
'plone.app.textfield',
'plone.app.upgrade',
'plone.dexterity',
'plone.directives.form',
'plone.memoize',
'plone.app.upgrade',
'Products.CMFPlone >=4.3',
'Products.GenericSetup',
'setuptools',
Expand Down
41 changes: 0 additions & 41 deletions src/sc/photogallery/browser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,42 +1 @@
# -*- coding: utf-8 -*-
from plone import api
from plone.dexterity.browser.view import DefaultView
from plone.memoize.instance import memoizedproperty
from sc.photogallery.utils import PhotoGalleryMixin


class View(DefaultView, PhotoGalleryMixin):

"""Slideshow view for Photo Gallery content type."""

def id(self):
return id(self)

@memoizedproperty
def results(self):
return self.context.listFolderContents()

@memoizedproperty
def is_empty(self):
return len(self.results) == 0

def image(self, obj, scale='large'):
"""Return an image scale if the item has an image field.
:param obj: [required]
:type obj: content type object
:param scale: the scale to be used
:type scale: string
"""
scales = obj.restrictedTraverse('@@images')
return scales.scale('image', scale)

def localized_time(self, obj, long_format=False):
"""Return the object time in a user-friendly way.
:param item: [required]
:type item: content type object
:param long_format: show long date format if True
:type scale: string
"""
return api.portal.get_localized_time(obj.Date(), long_format)
17 changes: 16 additions & 1 deletion src/sc/photogallery/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,30 @@

<browser:resourceDirectory name="sc.photogallery" directory="static" />

<browser:page
name="photogallery-settings"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
class="sc.photogallery.controlpanel.PhotoGallerySettingsControlPanel"
permission="cmf.ManagePortal"
/>

<browser:page
for="sc.photogallery.content.PhotoGallery"
name="view"
class="sc.photogallery.browser.View"
class="sc.photogallery.browser.view.View"
template="templates/view.pt"
permission="zope2.View"
layer="sc.photogallery.interfaces.IBrowserLayer"
/>

<browser:page
for="sc.photogallery.content.PhotoGallery"
name="zip"
class=".zip.ZipView"
permission="zope2.View"
layer="sc.photogallery.interfaces.IBrowserLayer"
/>

<!-- XXX: compatibility with summary_view in collections -->
<browser:page
for="sc.photogallery.content.PhotoGallery"
Expand Down
6 changes: 6 additions & 0 deletions src/sc/photogallery/browser/static/photogallery.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ body.portaltype-photo-gallery .slideshow-carrossel
width: 45%;
}

body.portaltype-photo-gallery .slideshow-download,
.photogallery-tile .slideshow-download
{
float: left;
}

.photogallery-tile .photogallery-url
{
font-weight: bold;
Expand Down
32 changes: 30 additions & 2 deletions src/sc/photogallery/browser/templates/view.pt
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,36 @@
</tal:image>
</div>
</div><!-- cycle-carrossel -->
</div><!-- slideshow-pager -->
</div><!-- slideshow-carrossel -->
</div><!-- slideshow-carrossel -->
<tal:download condition="view/can_download">
<div class="slideshow-download">
<div class="cycle-download cycle-slideshow"
data-cycle-slides="> div"
data-cycle-timeout="0"
data-cycle-fx="fade"
data-cycle-log="false">
<div tal:repeat="item view/results">
<a class="download"
tal:attributes="href string:${item/absolute_url}/download">
<span>Download Image</span>
<span class="size"
tal:content="python:view.img_size(item)">
</span>
</a>
</div>
</div><!-- cycle-download -->
</div><!-- slideshow-download -->
<tal:zip condition="view/can_zipexport">
<a class="download album"
tal:attributes="href view/zip_url">
<span>Download Photogallery</span>
<span class="size"
tal:content="string:.zip - ${view/zip_size}">
</span>
</a>
</tal:zip>
</tal:download>
</div><!-- slideshow-pager -->
</div>
<div class="" tal:condition="is_empty" i18n:translate="">
The photo gallery has no images on it.
Expand Down
110 changes: 110 additions & 0 deletions src/sc/photogallery/browser/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
from plone import api
from plone.api.exc import InvalidParameterError
from plone.dexterity.browser.view import DefaultView
from plone.memoize import forever
from plone.memoize.instance import memoizedproperty
from sc.photogallery.config import HAS_ZIPEXPORT
from sc.photogallery.interfaces import IPhotoGallerySettings
from sc.photogallery.utils import PhotoGalleryMixin
from zope.component import getMultiAdapter

import os

if HAS_ZIPEXPORT:
from ftw.zipexport.generation import ZipGenerator
from ftw.zipexport.interfaces import IZipRepresentation


def last_modified(context):
# HACK: We don't care with recursion
objects = context.objectValues()
# Take all mofied dates in seconds since epoch
modified = [int(obj.modified().strftime('%s'))
for obj in objects]
modified.append(int(context.modified().strftime('%s')))
modified.sort()
# return the most recent modification
return modified[-1]


class View(DefaultView, PhotoGalleryMixin):

"""Slideshow view for Photo Gallery content type."""

def id(self):
return id(self)

@memoizedproperty
def results(self):
return self.context.listFolderContents()

@memoizedproperty
def is_empty(self):
return len(self.results) == 0

def image(self, obj, scale='large'):
"""Return an image scale if the item has an image field.
:param obj: [required]
:type obj: content type object
:param scale: the scale to be used
:type scale: string
"""
scales = obj.restrictedTraverse('@@images')
return scales.scale('image', scale)

def localized_time(self, obj, long_format=False):
"""Return the object time in a user-friendly way.
:param item: [required]
:type item: content type object
:param long_format: show long date format if True
:type scale: string
"""
return api.portal.get_localized_time(obj.Date(), long_format)

def can_download(self):
download = False
try:
record = IPhotoGallerySettings.__identifier__ + '.download'
download = api.portal.get_registry_record(record)
except InvalidParameterError:
pass

return download

def img_size(self, item):
return '{0:.1f} MB'.format(item.size() / float(1024 * 1024))

def can_zipexport(self):
return HAS_ZIPEXPORT

def last_modified(self):
return last_modified(self.context)

def zip_url(self):
last_modified = self.last_modified()
base_url = self.context.absolute_url()
url = '%s/@@zip/%s/%s.zip' % (base_url,
str(last_modified),
self.context.getId())
return url

@forever.memoize
def _zip_size(self, last_modified=None):
if not HAS_ZIPEXPORT:
return '{0:.1f} MB'.format(0)
with ZipGenerator() as generator:
for obj in [self.context, ]:
repre = getMultiAdapter((obj, self.request),
interface=IZipRepresentation)
for path, pointer in repre.get_files():
generator.add_file(path, pointer)
zip_file = generator.generate()
size = os.stat(zip_file.name).st_size
return '{0:.1f} MB'.format(size / float(1024 * 1024))

def zip_size(self):
last_modified = self.last_modified()
return self._zip_size(last_modified)
94 changes: 94 additions & 0 deletions src/sc/photogallery/browser/zip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
from Products.statusmessages.interfaces import IStatusMessage
from sc.photogallery import _
from sc.photogallery.config import HAS_ZIPEXPORT
from sc.photogallery.browser.view import last_modified
from zope.component import getMultiAdapter
from zope.publisher.browser import BrowserPage
from ZPublisher.Iterators import filestream_iterator

import os

if HAS_ZIPEXPORT:
from ftw.zipexport.generation import ZipGenerator
from ftw.zipexport.interfaces import IZipRepresentation


class ZipView(BrowserPage):
'''Export PhotoGallery as zip package'''

def __init__(self, context, request):
self.context = context
self.request = request
self._path = []

@property
def traverse_subpath(self):
return self._path

def publishTraverse(self, request, name):
self._path.append(name)
return self

def _subpath(self):
return getattr(self, 'traverse_subpath', [])

def last_modified(self):
return last_modified(self.context)

@property
def filename(self):
base_url = self.context.absolute_url()
subpath = self._subpath()
last_modified = self.last_modified()
if not ((len(subpath) > 1) and (str(last_modified) == subpath[0])):
url = '%s/@@zip/%s/%s.zip' % (base_url,
str(last_modified),
self.context.getId())
return self.request.response.redirect(url)
filename = subpath[1].encode('utf-8')
return filename

def zip_selected(self, objects):
if not HAS_ZIPEXPORT:
return None
response = self.request.response
with ZipGenerator() as generator:
for obj in objects:
repre = getMultiAdapter((obj, self.request),
interface=IZipRepresentation)
for path, pointer in repre.get_files():
generator.add_file(path, pointer)
# check if zip has files
if generator.is_empty:
messages = IStatusMessage(self.request)
messages.add(
_('statmsg_content_not_supported',
default=u'Zip export is not supported on the selected content.'),
type=u'error'
)
self.request.response.redirect(self.context.absolute_url())
return

zip_file = generator.generate()
response.setHeader('Content-Disposition',
'inline; filename="%s"' % self.filename)
response.setHeader('Content-type',
'application/zip')
response.setHeader('Content-Length',
os.stat(zip_file.name).st_size)

return filestream_iterator(zip_file.name, 'rb')

def __call__(self):
filename = self.filename
if HAS_ZIPEXPORT and filename:
return self.zip_selected([self.context])
else:
messages = IStatusMessage(self.request)
messages.add(
_(u'Not supported operation'),
type=u'error'
)
self.request.response.redirect(self.context.absolute_url())
return
8 changes: 8 additions & 0 deletions src/sc/photogallery/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import pkg_resources

PROJECTNAME = 'sc.photogallery'

# Cycle2 JS resources used by the package
Expand All @@ -7,3 +9,9 @@
'++resource++collective.js.cycle2/jquery.cycle2.carousel.min.js',
'++resource++collective.js.cycle2/jquery.cycle2.swipe.min.js',
)

HAS_ZIPEXPORT = True
try:
pkg_resources.get_distribution('ftw.zipexport')
except pkg_resources.DistributionNotFound:
HAS_ZIPEXPORT = False

0 comments on commit 8a53bb8

Please sign in to comment.