Commit 4b448f8a authored by Jean-Paul Smets's avatar Jean-Paul Smets

Early refactoring of document related classes.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@11807 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 35f0ca7a
...@@ -31,27 +31,158 @@ from AccessControl import ClassSecurityInfo ...@@ -31,27 +31,158 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.WebDAVSupport import TextContent from Products.ERP5Type.WebDAVSupport import TextContent
from DateTime import DateTime
class Document(XMLObject, TextContent): def makeSortedTuple(kw):
items = kw.items()
items.sort()
return tuple(items)
class ConversionCacheMixin:
"""
This class provides a generic API to store in the ZODB
various converted versions of a file or of a string.
TODO:
* Implement ZODB BLOB
"""
# time of generation of various formats
_cached_time = {}
# generated files (cache)
_cached_data = {}
# mime types for cached formats XXX to be refactored
_cached_mime = {}
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
security.declareProtected(Permissions.ModifyPortalContent, 'clearConversionCache')
def clearConversionCache(self):
"""
Clear cache (invoked by interaction workflow upon file upload
needed here to overwrite class attribute with instance attrs
"""
self._cached_time = {}
self._cached_data = {}
self._cached_mime = {}
security.declareProtected(Permissions.View, 'hasConversion')
def hasConversion(self, **format):
"""
Checks whether we have a version in this format
"""
return self._cached_data.has_key(makeSortedTuple(format))
def getCacheTime(self, **format):
"""
Checks when if ever was the file produced
"""
return self._cached_time.get(makeSortedTuple(format), 0)
def updateConversion(self, **format):
self._cached_time[makeSortedTuple(format)] = DateTime()
def setConversion(self, data, mime=None, **format):
tformat = makeSortedTuple(format)
if mime is not None:
self._cached_mime[tformat] = mime
if data is not None:
self._cached_data[tformat] = data
self.updateConversion(format = format)
self._p_changed = 1
def getConversion(self, **format):
'''
we could be much cooler here - pass testing and updating methods to this function
so that it does it all by itself; this'd eliminate the need for cacheSet public method
'''
tformat = makeSortedTuple(format)
return self._cached_mime.get(tformat, ''), self._cached_data.get(tformat, '')
security.declareProtected(Permissions.View, 'getConversionCacheInfo')
def getConversionCacheInfo(self):
"""
Get cache details as string (for debugging)
""" """
A Document can contain text that can be formatted using s = 'CACHE INFO:<br/><table><tr><td>format</td><td>size</td><td>time</td><td>is changed</td></tr>'
*Structured Text* or *HTML*. Text can be automatically translated #self.log('getCacheInfo',self.cached_time)
through the use of 'message catalogs'. #self.log('getCacheInfo',self.cached_data)
for f in self._cached_time.keys():
Document inherits from XMLObject and can t = self._cached_time[f]
be synchronized accross multiple sites. data = self._cached_data.get(f)
if data:
Version Management: the notion of version depends on the if isinstance(data, str):
type of application. For example, in the case (1) of Transformation ln = len(data)
(BOM), all versions are considered as equal and may be kept else:
indefinitely for both archive and usage purpose. In the case (2) ln = 0
of Person data, the new version replaces the previous one while data is not None:
in place and is not needed for archive. In the case (3) of ln += len(data.data)
a web page, the new version replaces the previous one, data = data.next
the previous one being kept in place for archive. else:
ln = 'no data!!!'
s += '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (f, str(ln), str(t), '-')
s += '</table>'
return s
class Document(XMLObject):
"""
Document is an abstract class with all methods
related to document management in ERP5. This includes
searchable text, explicit relations, implicit relations,
metadata, versions, languages, etc.
There currently two types of Document subclasses:
* File for binary file based documents. File
has subclasses such as Image, OOoDocument,
PDFDocument, etc. to implement specific conversion
methods
* TextDocument for text based documents. TextDocument
has subclasses such as Wiki to implement specific
methods
Document classes which implement conversion should use
the CachingMixin class so that converted values are
stored.
The Document class behaviour can be extended through scripts.
* Document_discoverMetadata (DMS_ingestFile)
finds all metadata or uses the metadata which was
provided as parameter. Document_discoverMetadata should
be overloaded if necessary for some classes
(ex. TextDocument_discoverMetadata, Image_discoverMetadata)
and should be called through a single API discoverMetadata()
Consider using _getTypeBasedMethod for implementation
* Document_ingestFile (Document_uploadFile)
is called for http based ingestion and itself calls
Document_discoverMetadata. Many parameters may be
passed to Document_ingest through an
online form.
* Document_ingestEmail is called for email based
ingestion and itself calls Document_ingestFile.
Document_ingestEmail is in charge of parsing email
to extract metadata before calling Document_ingestFile.
* PUT is called for DAV/FTP based ingestion directly from the class.
It itself calls Document_discoverMetadata.
Custom scripts for automatic classification:
* Document_findWikiPredecessorList finds a list of documents
which are referencing us.
Should this be merged with WebSite_getDocumentValue ? XXX
* Document_findWikiSuccessor tries to find a document matching with
a given regexp.
Should this be merged with WebSite_getDocumentValue ? XXX
Subcontent: documents may include subcontent (files, images, etc.) Subcontent: documents may include subcontent (files, images, etc.)
so that publication of rich content can be path independent so that publication of rich content can be path independent.
""" """
meta_type = 'ERP5 Document' meta_type = 'ERP5 Document'
...@@ -59,6 +190,7 @@ class Document(XMLObject, TextContent): ...@@ -59,6 +190,7 @@ class Document(XMLObject, TextContent):
add_permission = Permissions.AddPortalContent add_permission = Permissions.AddPortalContent
isPortalContent = 1 isPortalContent = 1
isRADContent = 1 isRADContent = 1
isDocument = 1
# Declarative security # Declarative security
security = ClassSecurityInfo() security = ClassSecurityInfo()
...@@ -76,19 +208,102 @@ class Document(XMLObject, TextContent): ...@@ -76,19 +208,102 @@ class Document(XMLObject, TextContent):
# Declarative interfaces # Declarative interfaces
__implements__ = () __implements__ = ()
# Patch searchable_property_list = ('title', 'description', 'id', 'reference',
PUT = TextContent.PUT 'version', 'short_title', 'keywords',
'subject', 'source_reference', 'source_project_title')
# What is keywords ?
# XXX-JPS This is a plural
# XXX-JPS subject_list would be better than subject in this case
# and the getSearchableText should be able to process lists
# Same for source_reference_list, source_project_title_list
### Content indexing methods ### Content indexing methods
security.declareProtected(Permissions.View, 'getSearchableText') security.declareProtected(Permissions.View, 'getSearchableText')
def getSearchableText(self, md=None): def getSearchableText(self, md=None):
"""\ """
Used by the catalog for basic full text indexing Used by the catalog for basic full text indexing.
We should try to do some kind of file conversion here
""" XXX-JPS - This method is nice. It should probably be moved to Base class
searchable_text = "%s %s %s %s" % (self.getTitle(), self.getDescription(), searchable_property_list could become a standard class attribute.
self.getId(), self.getTextContent())
return searchable_text TODO (future): Make this property a per portal type property.
"""
# Compatibility with CMF Catalog / CPS sites searchable_text = ' '.join(map(lambda x: self.getProperty(x) or ' ',self.searchable_property_list))
SearchableText = getSearchableText return searchable_text
# Compatibility with CMF Catalog
SearchableText = getSearchableText # XXX-JPS - Here wa have a security issue - ask seb what to do
security.declareProtected(Permissions.ModifyPortalContent, 'setPropertyListFromFilename')
def setPropertyListFromFilename(self, fname):
"""
XXX-JPS missing description
"""
rx_src = self.portal_preferences.getPreferredDocumentFilenameRegexp()
if rx_src:
rx_parse = re.compile()
if rx_parse is None:
self.setReference(fname) # XXX-JPS please use _setReference to prevent reindexing all the time
return
m = rx_parse.match(fname)
if m is None:
self.setReference(fname) # XXX-JPS please use _setReference to prevent reindexing all the time
return
for k,v in m.groupdict().items():
self.setProperty(k,v) # XXX-JPS please use _setProperty to prevent reindexing all the time
# XXX-JPS finally call self.reindexObject()
else:
# If no regexp defined, we use the file name as reference
# this is the failover behaviour
self.setReference(fname)
security.declareProtected(Permissions.View, 'getWikiSuccessorReferenceList')
def getWikiSuccessorReferenceList(self):
"""
find references in text_content, return matches
with this we can then find objects
"""
if self.getTextContent() is None:
return []
rx_search = re.compile(self.portal_preferences.getPreferredDocumentReferenceRegexp()) # XXX-JPS Safe ? Better error required ?
try:
res = rx_search.finditer(self.getTextContent())
except AttributeError:
return []
res = [(r.group(),r.groupdict()) for r in res]
return res
security.declareProtected(Permissions.View, 'getWikiSuccessorValueList')
def getWikiSuccessorValueList(self):
"""
XXX-JPS Put a description then add notes (notes only is not enough)
getWikiSuccessorValueList - the way to find objects is on
implementation level
"""
# XXX results should be cached as volatile attributes
# XXX-JPS - Please use TransactionCache in ERP5Type for this
# TransactionCache does all the work for you
lst = []
for ref in self.getWikiSuccessorReferenceList():
r = ref[1]
res = self.Document_findWikiSuccessor(**r)
if len(res)>0:
lst.append(res[0].getObject())
return lst
security.declareProtected(Permissions.View, 'getWikiPredecessorValueList')
def getWikiPredecessorValueList(self):
"""
XXX-JPS Put a description then add notes (notes only is not enough)
it is mostly implementation level - depends on what parameters we use to identify
document, and on how a doc must reference me to be my predecessor (reference only,
or with a language, etc
"""
# XXX results should be cached as volatile attributes
lst = self.Document_findWikiPredecessorList()
lst = [r.getObject() for r in lst]
di = dict.fromkeys(lst) # make it unique
ref = self.getReference()
return [o for o in di.keys() if o.getReference() != ref] # every object has its own reference in SearchableText
...@@ -30,86 +30,165 @@ from AccessControl import ClassSecurityInfo ...@@ -30,86 +30,165 @@ from AccessControl import ClassSecurityInfo
from Products.CMFCore.WorkflowCore import WorkflowMethod from Products.CMFCore.WorkflowCore import WorkflowMethod
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.Cache import CachingMethod
from Products.ERP5.Document.Document import Document
from Products.ERP5Type.Base import Base from Products.ERP5Type.Base import Base
from Products.CMFDefault.File import File as CMFFile from Products.CMFDefault.File import File as CMFFile
from zLOG import LOG from zLOG import LOG
class File(XMLObject, CMFFile): import mimetypes, re
from DateTime import DateTime
mimetypes.init()
rs=[]
rs.append(re.compile('<HEAD>.*</HEAD>',re.DOTALL|re.MULTILINE|re.IGNORECASE))
rs.append(re.compile('<!DOCTYPE[^>]*>'))
rs.append(re.compile('<.?(HTML|BODY)[^>]*>',re.DOTALL|re.MULTILINE|re.IGNORECASE))
def stripHtml(txt): # XXX-JPS to be moved to TextDocument
for r in rs:
txt=r.sub('',txt)
return txt
class File(Document, CMFFile):
"""
A File can contain raw data which can be uploaded and downloaded.
It is the root class of Image, OOoDocument (ERP5OOo product),
etc. The main purpose of the File class is to handle efficiently
large files. It uses Pdata from OFS.File for this purpose.
File inherits from XMLObject and can be synchronized
accross multiple sites.
Subcontent: File can only contain role information.
TODO:
* make sure ZODB BLOBS are supported to prevent
feeding the ZODB cache with unnecessary large data
"""
meta_type = 'ERP5 File'
portal_type = 'File'
add_permission = Permissions.AddPortalContent
isPortalContent = 1
isRADContent = 1
__dav_collection__=0
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Default global values
content_type = '' # Required for WebDAV support (default value)
# Default Properties
property_sheets = ( PropertySheet.Base
, PropertySheet.CategoryCore
, PropertySheet.DublinCore
, PropertySheet.Version
, PropertySheet.Reference
, PropertySheet.Document
, PropertySheet.Data
)
# Declarative interfaces
#__implements__ = ( , )
### Special edit method
security.declarePrivate( '_edit' )
def _edit(self, **kw):
"""\
This is used to edit files
""" """
A File can contain text that can be formatted using if kw.has_key('file'):
*Structured Text* or *HTML*. Text can be automatically translated file = kw.get('file')
through the use of 'message catalogs'. precondition = kw.get('precondition')
if self._isNotEmpty(file):
CMFFile._edit(self, precondition=precondition, file=file)
del kw['file']
Base._edit(self, **kw)
security.declareProtected( Permissions.ModifyPortalContent, 'edit' )
edit = WorkflowMethod( _edit )
# Copy support needs to be implemented by ExtFile
################################
# Special management methods #
################################
def manage_afterClone(self, item):
Base.manage_afterClone(self, item)
CMFFile.manage_afterClone(self, item)
def manage_afterAdd(self, item, container):
Base.manage_afterAdd(self, item, container)
CMFFile.manage_afterAdd(self, item, container)
def manage_beforeDelete(self, item, container):
CMFFile.manage_beforeDelete(self, item, container)
def get_size(self):
"""
has to be overwritten here, otherwise WebDAV fails
"""
try:
return len(self.data)
except (AttributeError, TypeError):
return 0
File can only contain role information. getcontentlength = get_size
File inherits from XMLObject and can security.declareProtected(Permissions.View, 'hasFile')
be synchronized accross multiple sites. def hasFile(self):
""" """
Checks whether we have an initial file
"""
_marker = []
if getattr(self,'data', _marker) is not _marker: # XXX-JPS - use propertysheet accessors
return getattr(self,'data') is not None
return False
meta_type = 'ERP5 File' security.declarePrivate('_unpackData')
portal_type = 'File' def _unpackData(self,data):
add_permission = Permissions.AddPortalContent """
isPortalContent = 1 Unpack Pdata into string
isRADContent = 1 """
if isinstance(data, str):
# Declarative security return data
security = ClassSecurityInfo() else:
security.declareObjectProtected(Permissions.AccessContentsInformation) data_list = []
while data is not None:
# Default global values data_list.append(data.data)
content_type = '' # Required for WebDAV support (default value) data=data.next
return ''.join(data_list)
# Declarative properties
property_sheets = ( PropertySheet.Base security.declareProtected(Permissions.ModifyPortalContent, 'guessMimeType')
, PropertySheet.CategoryCore def guessMimeType(self, fname=''):
, PropertySheet.DublinCore """
, PropertySheet.Data get mime type from file name
) """
if fname == '': fname = self.getOriginalFilename()
# Declarative interfaces if fname:
#__implements__ = ( , ) content_type,enc = mimetypes.guess_type(fname)
if content_type is not None:
### Special edit method self.content_type = content_type
security.declarePrivate( '_edit' ) return content_type
def _edit(self, **kw):
"""\ security.declareProtected(Permissions.ModifyPortalContent,'PUT')
This is used to edit files def PUT(self,REQUEST,RESPONSE):
""" CMFFile.PUT(self,REQUEST,RESPONSE)
if kw.has_key('file'): self.DMS_ingestFile(fname=self.getId()) # XXX-JPS we should call here Document_discoverMetadata
file = kw.get('file') # with the filename as parameter
precondition = kw.get('precondition')
if self._isNotEmpty(file): # DAV Support
CMFFile._edit(self, precondition=precondition, file=file) index_html = CMFFile.index_html # XXX-JPS - Here we have a security issue - ask seb what to do
del kw['file'] PUT = CMFFile.PUT # XXX-JPS - Here we have a security issue - ask seb what to do
Base._edit(self, **kw) security.declareProtected('FTP access', 'manage_FTPget', 'manage_FTPstat', 'manage_FTPlist')
manage_FTPget = CMFFile.manage_FTPget # XXX-JPS - Here we have a security issue - ask seb what to do
security.declareProtected( Permissions.ModifyPortalContent, 'edit' ) manage_FTPlist = CMFFile.manage_FTPlist # XXX-JPS - Here we have a security issue - ask seb what to do
edit = WorkflowMethod( _edit ) manage_FTPstat = CMFFile.manage_FTPstat # XXX-JPS - Here we have a security issue - ask seb what to do
# Copy support needs to be implemented by ExtFile # vim: syntax=python shiftwidth=2
################################
# Special management methods #
################################
def manage_afterClone(self, item):
Base.manage_afterClone(self, item)
CMFFile.manage_afterClone(self, item)
def manage_afterAdd(self, item, container):
Base.manage_afterAdd(self, item, container)
CMFFile.manage_afterAdd(self, item, container)
def manage_beforeDelete(self, item, container):
CMFFile.manage_beforeDelete(self, item, container)
# DAV Support
index_html = CMFFile.index_html
PUT = CMFFile.PUT
security.declareProtected('FTP access', 'manage_FTPget', 'manage_FTPstat', 'manage_FTPlist')
manage_FTPget = CMFFile.manage_FTPget
manage_FTPlist = CMFFile.manage_FTPlist
manage_FTPstat = CMFFile.manage_FTPstat
...@@ -28,119 +28,340 @@ ...@@ -28,119 +28,340 @@
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.CMFCore.WorkflowCore import WorkflowMethod
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.Base import Base from Products.ERP5.Document.File import File
from Products.Photo.Photo import Photo from Products.ERP5.Document.Document import ConversionCacheMixin
from OFS.Image import Image as OFSImage
from OFS.Image import getImageInfo
from OFS.content_types import guess_content_type
import string, time, sys
from cStringIO import StringIO
from zLOG import LOG
class Image (Base, Photo): # XXX This should be move to preferences
""" defaultdisplays = {'thumbnail' : (128,128),
An Image can contain text that can be formatted using 'xsmall' : (200,200),
*Structured Text* or *HTML*. Text can be automatically translated 'small' : (320,320),
through the use of 'message catalogs'. 'medium' : (480,480),
'large' : (768,768),
'xlarge' : (1024,1024)
}
Image can only contain role information. default_formats = ['jpg', 'jpeg', 'png', 'gif', 'pnm', 'ppm']
Image inherits from XMLObject and can class Image(File, OFSImage, ConversionCacheMixin):
be synchronized accross multiple sites.
""" """
An Image is a File which contains image data. It supports
various conversions of format, size, resolution through
imagemagick. imagemagick was preferred due to its ability
to support PDF files (incl. Adobe Illustrator) which make
it very useful in the context of a graphic design shop.
Image inherits from XMLObject and can be synchronized
accross multiple sites.
Subcontent: Image can only contain role information.
TODO:
* extend Image to support more image file formats,
including Xara Xtreme (http://www.xaraxtreme.org/)
* include upgrade methods so that previous images
in ERP5 get upgraded automatically to new class
"""
# CMF Type Definition
meta_type = 'ERP5 Image' meta_type = 'ERP5 Image'
portal_type = 'Image' portal_type = 'Image'
add_permission = Permissions.AddPortalContent
isPortalContent = 1 isPortalContent = 1
isRADContent = 1 isRADContent = 1
# Default attribute values
width = 0
height = 0
# Declarative security # Declarative security
security = ClassSecurityInfo() security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation) security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative properties # Default Properties
property_sheets = ( PropertySheet.Base property_sheets = ( PropertySheet.Base
, PropertySheet.CategoryCore , PropertySheet.CategoryCore
, PropertySheet.DublinCore , PropertySheet.DublinCore
, PropertySheet.Version
, PropertySheet.Reference
, PropertySheet.Document
, PropertySheet.Data
) )
def __init__( self, id, title='', file='', store='Image' #
, engine='ImageMagick', quality=75, pregen=0, timeout=0): # Original photo attributes
Photo.__init__(self, id=id, title=title, file=file, store=store #
, engine=engine, quality=quality, pregen=pregen, timeout=timeout)
Base.__init__(self, id=id) def _update_image_info(self):
self._data = ''
self.store = store
self._checkOriginal()
### Special edit method
def _checkOriginal(self):
if not hasattr(self, '_original'):
if self.store == 'Image' : from Products.Photo.PhotoImage import PhotoImage
elif self.store == 'ExtImage': from Products.Photo.ExtPhotoImage import PhotoImage
self._original = PhotoImage(self.id, self.title, path=self.absolute_url(1))
security.declarePrivate('_setFile')
def _setFile(self, file):
""" """
This is used to set files This method tries to determine the content type of an image and
its geometry. It uses currently OFS.Image for this purpose.
However, this method is known to be too simplistic.
TODO:
- use image magick or PIL
"""
content_type, width, height = getImageInfo(self.data)
self.height = height
self.width = width
self._setContentType(content_type)
security.declareProtected(Permissions.AccessContentsInformation, 'getWidth')
def getWidth(self):
"""
Tries to get the width from the image data.
""" """
Photo.manage_editPhoto(self, file=file) if self.get_size() and not self.width: self._update_image_info()
self.manage_purgeDisplays() return self.width
security.declarePrivate('_edit') security.declareProtected(Permissions.AccessContentsInformation, 'getHeight')
def _edit(self, **kw): def getHeight(self):
""" """
This is used to edit files Tries to get the height from the image data.
""" """
self._checkOriginal() if self.get_size() and not self.height: self._update_image_info()
if kw.has_key('file'): return self.height
file = kw.get('file')
precondition = kw.get('precondition') security.declareProtected(Permissions.AccessContentsInformation, 'getContentType')
self._setFile(file) def getContentType(self, format=''):
del kw['file'] """Original photo content_type."""
Base._edit(self, **kw) if format == '':
return self._baseGetContentType()
else:
return guess_content_type('myfile.' + format)[0]
#
# Photo display methods
#
security.declareProtected('View', 'tag')
def tag(self, display=None, height=None, width=None, cookie=0,
alt=None, css_class=None, format='', quality=75,
resolution=None, **kw):
"""Return HTML img tag."""
# Get cookie if display is not specified.
if display is None:
display = self.REQUEST.cookies.get('display', None)
# display may be set from a cookie.
if display is not None and defaultdisplays.has_key(display):
if not self.hasConversion(display=display, format=format,
quality=quality, resolution=resolution):
# Generate photo on-the-fly
self._makeDisplayPhoto(display, 1, format=format, quality=quality, resolution=resolution)
mime, image = self.getConversion(display=display, format=format,
quality=quality ,resolution=resolution)
width, height = (image.width, image.height)
# Set cookie for chosen size
if cookie:
self.REQUEST.RESPONSE.setCookie('display', display, path="/")
else:
# TODO: Add support for on-the-fly resize?
height = self.getHeight()
width = self.getWidth()
if display:
result = '<img src="%s?display=%s"' % (self.absolute_url(), display)
else:
result = '<img src="%s"' % (self.absolute_url())
if alt is None:
alt = getattr(self, 'title', '')
if alt == '':
alt = self.getId()
result = '%s alt="%s"' % (result, html_quote(alt))
if height:
result = '%s height="%s"' % (result, height)
if width:
result = '%s width="%s"' % (result, width)
if not 'border' in map(string.lower, kw.keys()):
result = '%s border="0"' % (result)
if css_class is not None:
result = '%s class="%s"' % (result, css_class)
for key in kw.keys():
value = kw.get(key)
result = '%s %s="%s"' % (result, key, value)
result = '%s />' % (result)
return result
def __str__(self):
return self.tag()
security.declareProtected('Access contents information', 'displayIds')
def displayIds(self, exclude=('thumbnail',)):
"""Return list of display Ids."""
ids = defaultdisplays.keys()
# Exclude specified displays
if exclude:
for id in exclude:
if id in ids:
ids.remove(id)
# Sort by desired photo surface area
ids.sort(lambda x,y,d=self._displays: cmp(d[x][0]*d[x][1], d[y][0]*d[y][1]))
return ids
security.declareProtected('Access contents information', 'displayLinks')
def displayLinks(self, exclude=('thumbnail',)):
"""Return list of HTML <a> tags for displays."""
links = []
for display in self.displayIds(exclude):
links.append('<a href="%s?display=%s">%s</a>' % (self.REQUEST['URL'], display, display))
return links
security.declareProtected('Access contents information', 'displayMap')
def displayMap(self, exclude=None, format='', quality=75, resolution=None):
"""Return list of displays with size info."""
displays = []
for id in self.displayIds(exclude):
if self._isGenerated(id, format=format, quality=quality,resolution=resolution):
photo_width = self._photos[(id,format)].width
photo_height = self._photos[(id,format)].height
bytes = self._photos[(id,format)]._size()
age = self._photos[(id,format)]._age()
else:
(photo_width, photo_height, bytes, age) = (None, None, None, None)
displays.append({'id': id,
'width': defaultdisplays[id][0],
'height': defaultdisplays[id][1],
'photo_width': photo_width,
'photo_height': photo_height,
'bytes': bytes,
'age': age
})
return displays
security.declareProtected('View', 'index_html') security.declareProtected('View', 'index_html')
index_html = Photo.index_html def index_html(self, REQUEST, RESPONSE, display=None, format='', quality=75, resolution=None):
"""Return the image data."""
security.declareProtected(Permissions.AccessContentsInformation, 'content_type') # display may be set from a cookie (?)
content_type = Photo.content_type if (display is not None or resolution is not None or quality != 75) and defaultdisplays.has_key(display):
if not self.hasConversion(display=display, format=format,
quality=quality,resolution=resolution):
# Generate photo on-the-fly
self._makeDisplayPhoto(display, 1, format=format, quality=quality,resolution=resolution)
# Return resized image
mime, image = self.getConversion(display=display, format=format,
quality=quality ,resolution=resolution)
return image.index_html(REQUEST, RESPONSE)
# Copy support needs to be implemented by ExtFile # Return original image
################################ return OFSImage.index_html(self, REQUEST, RESPONSE)
# Special management methods #
################################
def manage_afterClone(self, item):
Base.manage_afterClone(self, item)
self._checkOriginal()
Photo.manage_afterClone(self, item)
def manage_afterAdd(self, item, container): #
Photo.manage_afterAdd(self, item, container) # Photo processing
#
def manage_beforeDelete(self, item, container): def _resize(self, display, width, height, quality=75, format='', resolution=None):
Photo.manage_beforeDelete(self, item, container) """Resize and resample photo."""
newimg = StringIO()
# Some ERPish if sys.platform == 'win32':
def getWidth(self): from win32pipe import popen2
""" from tempfile import mktemp
Alias for width newimg_path = mktemp(suffix=format)
""" if resolution is None:
return self.width() imgin, imgout = popen2('convert -quality %s -geometry %sx%s - %s'
% (quality, width, height, newimg_path), 'b')
else:
imgin, imgout = popen2('convert -density %sx%s -quality %s -geometry %sx%s - %s'
% (resolution, resolution, quality, width, height, newimg_path), 'b')
def getHeight(self): else:
""" from popen2 import popen2
Alias for width import tempfile
""" tempdir = tempfile.tempdir
return self.height() tempfile.tempdir = '/tmp'
newimg_path = tempfile.mktemp(suffix='.' + format)
# Aliases for uniform update of data tempfile.tempdir = tempdir
def manage_upload(self, file='', REQUEST=None): if resolution is None:
self.manage_file_upload(self, file=file, REQUEST=None) imgout, imgin = popen2('convert -quality %s -geometry %sx%s - %s'
% (quality, width, height, newimg_path))
# DAV Support else:
PUT = Photo.PUT LOG('Resolution',0,str(resolution))
security.declareProtected('FTP access', 'manage_FTPget', 'manage_FTPstat', 'manage_FTPlist') imgout, imgin = popen2('convert -density %sx%s -quality %s -geometry %sx%s - %s'
manage_FTPget = Photo.manage_FTPget % (resolution, resolution, quality, width, height, newimg_path))
manage_FTPlist = Photo.manage_FTPlist
manage_FTPstat = Photo.manage_FTPstat imgin.write(str(self.getData()))
imgin.close()
imgout.read()
imgout.close()
newimg_file = open(newimg_path, 'r')
newimg.write(newimg_file.read())
newimg_file.close()
newimg.seek(0)
return newimg
def _getDisplayData(self, display, format='', quality=75, resolution=None):
"""Return raw photo data for given display."""
(width, height) = defaultdisplays[display]
if width == 0 and height == 0:
width = self.getWidth()
height = self.getHeight()
(width, height) = self._getAspectRatioSize(width, height)
return self._resize(display, width, height, quality, format=format, resolution=resolution)
def _getDisplayPhoto(self, display, format='', quality=75, resolution=None):
"""Return photo object for given display."""
try:
base, ext = string.split(self.id, '.')
id = base+'_'+display+'.'+ext
except ValueError:
id = self.id+'_'+display
image = OFSImage(id, self.getTitle(), self._getDisplayData(display, format=format,
quality=quality,resolution=resolution))
return image
def _makeDisplayPhoto(self, display, force=0, format='', quality=75, resolution=None):
"""Create given display."""
if not self.hasConversion(display=display, format=format, quality=quality,resolution=resolution) or force:
image = self._getDisplayPhoto(display, format=format, quality=quality, resolution=resolution)
self.setConversion(image, mime=image.content_type,
display=display, format=format,
quality=quality ,resolution=resolution)
def _getAspectRatioSize(self, width, height):
"""Return proportional dimensions within desired size."""
img_width, img_height = (self.getWidth(), self.getHeight())
if height > img_height * width / img_width:
height = img_height * width / img_width
else:
width = img_width * height / img_height
return (width, height)
def _validImage(self):
"""At least see if it *might* be valid."""
return self.getWidth() and self.getHeight() and self.getData() and self.getContentType()
#
# FTP/WebDAV support
#
#if hasattr(self, '_original'):
## Updating existing Photo
#self._original.manage_upload(file, self.content_type())
#if self._validImage():
#self._makeDisplayPhotos()
# Maybe needed
#def manage_afterClone(self, item):
# Maybe needed
#def manage_afterAdd(self, item, container):
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment