[bin] job.py: if the specified user does not exist in the DB, try with a user named 'admin'; [gen] Calendar: allow to insert additional, custom (XHTML or textual) info in any cell of the calendar, via new attribute Calendar.additionalInfo; [pod] some code refactoring; xhtml2odt: allow, when converting tables, to take into account attributes 'width' of tds; bugfix when converting 'href' attrs of 'a' tags.

This commit is contained in:
Gaetan Delannay 2012-10-26 13:09:44 +02:00
parent ba148c51aa
commit caca61516f
10 changed files with 133 additions and 60 deletions

View file

@ -56,6 +56,9 @@ else:
# Log as Zope admin # Log as Zope admin
from AccessControl.SecurityManagement import newSecurityManager from AccessControl.SecurityManagement import newSecurityManager
user = app.acl_users.getUserById(zopeUser) user = app.acl_users.getUserById(zopeUser)
if not user:
# Try with user "admin"
user = app.acl_users.getUserById('admin')
if not hasattr(user, 'aq_base'): if not hasattr(user, 'aq_base'):
user = user.__of__(app.acl_users) user = user.__of__(app.acl_users)
newSecurityManager(None, user) newSecurityManager(None, user)

View file

@ -17,8 +17,8 @@ class Calendar(Type):
specificWritePermission=False, width=None, height=300, specificWritePermission=False, width=None, height=300,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
mapping=None, label=None, maxEventLength=50, mapping=None, label=None, maxEventLength=50,
otherCalendars=None, startDate=None, endDate=None, otherCalendars=None, additionalInfo=None, startDate=None,
defaultDate=None): endDate=None, defaultDate=None):
Type.__init__(self, validator, (0,1), None, default, False, False, Type.__init__(self, validator, (0,1), None, default, False, False,
show, page, group, layouts, move, False, False, show, page, group, layouts, move, False, False,
specificReadPermission, specificWritePermission, specificReadPermission, specificWritePermission,
@ -52,6 +52,12 @@ class Calendar(Type):
# leading "#" when relevant) into which events of the calendar must # leading "#" when relevant) into which events of the calendar must
# appear. # appear.
self.otherCalendars = otherCalendars self.otherCalendars = otherCalendars
# One may want to add custom information in the calendar. When a method
# is given in p_additionalInfo, for every cell of the month view, this
# method will be called with a single arg (the cell's date). The
# method's result (a string that can hold text or a chunk of XHTML) will
# be inserted in the cell.
self.additionalInfo = additionalInfo
# One may limit event encoding and viewing to a limited period of time, # One may limit event encoding and viewing to a limited period of time,
# via p_startDate and p_endDate. Those parameters, if given, must hold # via p_startDate and p_endDate. Those parameters, if given, must hold
# methods accepting no arg and returning a Zope DateTime instance. # methods accepting no arg and returning a Zope DateTime instance.
@ -131,6 +137,13 @@ class Calendar(Type):
res[i][1] = res[i][0].getField(res[i][1]) res[i][1] = res[i][0].getField(res[i][1])
return res return res
def getAdditionalInfoAt(self, obj, date):
'''If the user has specified a method in self.additionalInfo, we call
it for displaying this additional info in the calendar, at some
p_date.'''
if not self.additionalInfo: return
return self.additionalInfo(obj.appy(), date)
def getEventTypes(self, obj): def getEventTypes(self, obj):
'''Returns the (dynamic or static) event types as defined in '''Returns the (dynamic or static) event types as defined in
self.eventTypes.''' self.eventTypes.'''
@ -158,6 +171,13 @@ class Calendar(Type):
res = days[day] res = days[day]
return res return res
def getEventTypeAt(self, obj, date):
'''Returns the event type of the first event defined at p_day, or None
if unspecified.'''
events = self.getEventsAt(obj, date, asDict=False)
if not events: return
return events[0].eventType
def hasEventsAt(self, obj, date, otherEvents): def hasEventsAt(self, obj, date, otherEvents):
'''Returns True if, at p_date, an event is found of the same type as '''Returns True if, at p_date, an event is found of the same type as
p_otherEvents.''' p_otherEvents.'''
@ -202,11 +222,20 @@ class Calendar(Type):
else: else:
return DateTime() # Now return DateTime() # Now
def createEvent(self, obj, date, handleEventSpan=True): def createEvent(self, obj, date, eventType=None, eventSpan=None,
'''Create a new event in the calendar, at some p_date (day). If handleEventSpan=True):
p_handleEventSpan is True, we will use rq["eventSpan"] and also '''Create a new event in the calendar, at some p_date (day).
If p_eventType is given, it is used; else, rq['eventType'] is used.
If p_handleEventSpan is True, we will use p_eventSpan (or
rq["eventSpan"] if p_eventSpan is not given) and also
create the same event for successive days.''' create the same event for successive days.'''
obj = obj.o # Ensure p_obj is not a wrapper.
rq = obj.REQUEST rq = obj.REQUEST
# Get values from parameters
if not eventType: eventType = rq['eventType']
if handleEventSpan and not eventSpan:
eventSpan = rq.get('eventSpan', None)
# Split the p_date into separate parts
year, month, day = date.year(), date.month(), date.day() year, month, day = date.year(), date.month(), date.day()
# Check that the "preferences" dict exists or not. # Check that the "preferences" dict exists or not.
if not hasattr(obj.aq_base, self.name): if not hasattr(obj.aq_base, self.name):
@ -230,11 +259,11 @@ class Calendar(Type):
daysDict[day] = events = PersistentList() daysDict[day] = events = PersistentList()
# Create and store the event, excepted if an event already exists. # Create and store the event, excepted if an event already exists.
if not events: if not events:
event = Object(eventType=rq['eventType']) event = Object(eventType=eventType)
events.append(event) events.append(event)
# Span the event on the successive days if required # Span the event on the successive days if required
if handleEventSpan and rq['eventSpan']: if handleEventSpan and eventSpan:
nbOfDays = min(int(rq['eventSpan']), self.maxEventLength) nbOfDays = min(int(eventSpan), self.maxEventLength)
for i in range(nbOfDays): for i in range(nbOfDays):
date = date + 1 date = date + 1
self.createEvent(obj, date, handleEventSpan=False) self.createEvent(obj, date, handleEventSpan=False)
@ -243,6 +272,7 @@ class Calendar(Type):
'''Deletes an event. It actually deletes all events at rq['day']. '''Deletes an event. It actually deletes all events at rq['day'].
If p_handleEventSpan is True, we will use rq["deleteNext"] to If p_handleEventSpan is True, we will use rq["deleteNext"] to
delete successive events, too.''' delete successive events, too.'''
obj = obj.o # Ensure p_obj is not a wrapper.
rq = obj.REQUEST rq = obj.REQUEST
if not self.getEventsAt(obj, date): return if not self.getEventsAt(obj, date): return
daysDict = getattr(obj, self.name)[date.year()][date.month()] daysDict = getattr(obj, self.name)[date.year()][date.month()]

View file

@ -104,6 +104,9 @@
tal:attributes="style python: 'color: %s;; font-style: italic' % event['color']"></div> tal:attributes="style python: 'color: %s;; font-style: italic' % event['color']"></div>
</tal:e> </tal:e>
</tal:others> </tal:others>
<tal:comment replace="nothing">Additional info</tal:comment>
<tal:info define="info python: contextObj.callField(fieldName,'getAdditionalInfoAt', contextObj, date)"
condition="info" replace="structure info"/>
</tal:day> </tal:day>
</td> </td>
</tal:td> </tal:td>

View file

@ -19,6 +19,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import time import time
from appy.shared.utils import Traceback from appy.shared.utils import Traceback
from appy.shared.xml_parser import escapeXhtml
# Some POD-specific constants -------------------------------------------------- # Some POD-specific constants --------------------------------------------------
XHTML_HEADINGS = ('h1', 'h2', 'h3', 'h4', 'h5', 'h6') XHTML_HEADINGS = ('h1', 'h2', 'h3', 'h4', 'h5', 'h6')
@ -27,8 +28,6 @@ XHTML_PARAGRAPH_TAGS = XHTML_HEADINGS + XHTML_LISTS + ('p',)
XHTML_PARAGRAPH_TAGS_NO_LISTS = XHTML_HEADINGS + ('p',) XHTML_PARAGRAPH_TAGS_NO_LISTS = XHTML_HEADINGS + ('p',)
XHTML_INNER_TAGS = ('b', 'i', 'u', 'em') XHTML_INNER_TAGS = ('b', 'i', 'u', 'em')
XHTML_UNSTYLABLE_TAGS = XHTML_LISTS + ('li', 'a') XHTML_UNSTYLABLE_TAGS = XHTML_LISTS + ('li', 'a')
XML_SPECIAL_CHARS = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class PodError(Exception): class PodError(Exception):
@ -83,17 +82,6 @@ class PodError(Exception):
buffer.write('</%s>' % withinElement.OD.elem) buffer.write('</%s>' % withinElement.OD.elem)
dump = staticmethod(dump) dump = staticmethod(dump)
def convertToXhtml(s): # XXX To remove, present for backward compatibility only.
'''Produces the XHTML-friendly version of p_s.''' convertToXhtml = escapeXhtml
res = ''
for c in s:
if XML_SPECIAL_CHARS.has_key(c):
res += XML_SPECIAL_CHARS[c]
elif c == '\n':
res += '<br/>'
elif c == '\r':
pass
else:
res += c
return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -20,11 +20,11 @@
import re import re
from xml.sax.saxutils import quoteattr from xml.sax.saxutils import quoteattr
from appy.pod import PodError, XML_SPECIAL_CHARS from appy.shared.xml_parser import xmlPrologue, escapeXml
from appy.pod import PodError
from appy.pod.elements import * from appy.pod.elements import *
from appy.pod.actions import IfAction, ElseAction, ForAction, VariableAction, \ from appy.pod.actions import IfAction, ElseAction, ForAction, VariableAction, \
NullAction NullAction
from appy.shared import xmlPrologue
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class ParsingError(Exception): pass class ParsingError(Exception): pass
@ -157,11 +157,7 @@ class Buffer:
def dumpContent(self, content): def dumpContent(self, content):
'''Dumps string p_content into the buffer.''' '''Dumps string p_content into the buffer.'''
for c in content: self.write(escapeXml(content))
if XML_SPECIAL_CHARS.has_key(c):
self.write(XML_SPECIAL_CHARS[c])
else:
self.write(c)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class FileBuffer(Buffer): class FileBuffer(Buffer):

View file

@ -10,8 +10,9 @@ class OdtTable:
tns = 'table:' tns = 'table:'
txns = 'text:' txns = 'text:'
def __init__(self, name, paraStyle, cellStyle, nbOfCols, def __init__(self, name, paraStyle='podTablePara', cellStyle='podTableCell',
paraHeaderStyle=None, cellHeaderStyle=None, html=False): nbOfCols=1, paraHeaderStyle=None, cellHeaderStyle=None,
html=False):
# An ODT table must have a name. In the case of an HTML table, p_name # An ODT table must have a name. In the case of an HTML table, p_name
# represents the CSS class for the whole table. # represents the CSS class for the whole table.
self.name = name self.name = name
@ -24,7 +25,7 @@ class OdtTable:
# The default style of every paragraph within a header cell # The default style of every paragraph within a header cell
self.paraHeaderStyle = paraHeaderStyle or paraStyle self.paraHeaderStyle = paraHeaderStyle or paraStyle
# The default style of every header cell # The default style of every header cell
self.cellHeaderStyle = cellHeaderStyle or cellStyle self.cellHeaderStyle = cellHeaderStyle or 'podTableHeaderCell'
# The buffer where the resulting table will be rendered # The buffer where the resulting table will be rendered
self.res = '' self.res = ''
# Do we need to generate an HTML table instead of an ODT table ? # Do we need to generate an HTML table instead of an ODT table ?

View file

@ -21,9 +21,9 @@ import os, os.path, sys, zipfile, re, shutil
import appy.shared.test import appy.shared.test
from appy.shared.test import TesterError from appy.shared.test import TesterError
from appy.shared.utils import FolderDeleter from appy.shared.utils import FolderDeleter
from appy.shared.xml_parser import escapeXml
from appy.pod.odf_parser import OdfEnvironment, OdfParser from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.pod.renderer import Renderer from appy.pod.renderer import Renderer
from appy.pod import XML_SPECIAL_CHARS
# TesterError-related constants ------------------------------------------------ # TesterError-related constants ------------------------------------------------
TEMPLATE_NOT_FOUND = 'Template file "%s" was not found.' TEMPLATE_NOT_FOUND = 'Template file "%s" was not found.'
@ -70,12 +70,7 @@ class AnnotationsRemover(OdfParser):
self.res += '</%s>' % elem self.res += '</%s>' % elem
def characters(self, content): def characters(self, content):
e = OdfParser.characters(self, content) e = OdfParser.characters(self, content)
if not self.ignore: if not self.ignore: self.res += escapeXml(content)
for c in content:
if XML_SPECIAL_CHARS.has_key(c):
self.res += XML_SPECIAL_CHARS[c]
else:
self.res += c
def getResult(self): def getResult(self):
return self.res return self.res

View file

@ -10,7 +10,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import xml.sax, time, random import xml.sax, time, random
from appy.shared.xml_parser import XmlEnvironment, XmlParser from appy.shared.xml_parser import XmlEnvironment, XmlParser, escapeXml
from appy.pod.odf_parser import OdfEnvironment from appy.pod.odf_parser import OdfEnvironment
from appy.pod.styles_manager import Style from appy.pod.styles_manager import Style
from appy.pod import * from appy.pod import *
@ -235,6 +235,10 @@ class HtmlTable:
# The following list stores, for every column, the size of the biggest # The following list stores, for every column, the size of the biggest
# content of all its cells. # content of all its cells.
self.columnContentSizes = [] self.columnContentSizes = []
# The following list stores, for every column, its width, if specified.
# If widths are found, self.columnContentSizes will not be used:
# self.columnWidths will be used instead.
self.columnWidths = []
def computeColumnStyles(self, renderer): def computeColumnStyles(self, renderer):
'''Once the table has been completely parsed, self.columnContentSizes '''Once the table has been completely parsed, self.columnContentSizes
@ -242,22 +246,34 @@ class HtmlTable:
of every column and create the corresponding style declarations, in of every column and create the corresponding style declarations, in
p_renderer.dynamicStyles.''' p_renderer.dynamicStyles.'''
total = 65000.0 # A number representing the total width of the table total = 65000.0 # A number representing the total width of the table
# Ensure first that self.columnContentSizes is correct # Use (a) self.columnWidths if complete, or
if (len(self.columnContentSizes) != self.nbOfColumns) or \ # (b) self.columnContentSizes if complete, or
(None in self.columnContentSizes): # (c) a fixed width else.
# There was a problem while parsing the table. Set every column if self.columnWidths and (len(self.columnWidths) == self.nbOfColumns) \
# with the same width. and (None not in self.columnWidths):
widths = [int(total/self.nbOfColumns)] * self.nbOfColumns # Use self.columnWidths
toUse = self.columnWidths
# Use self.columnContentSizes if complete
elif (len(self.columnContentSizes) == self.nbOfColumns) and \
(None not in self.columnContentSizes):
# Use self.columnContentSizes
toUse = self.columnContentSizes
else: else:
toUse = None
if toUse:
widths = [] widths = []
# Compute the sum of all column content sizes # Compute the sum of all column content sizes
contentTotal = 0 contentTotal = 0
for size in self.columnContentSizes: contentTotal += size for size in toUse: contentTotal += size
contentTotal = float(contentTotal) contentTotal = float(contentTotal)
for size in self.columnContentSizes: for size in toUse:
width = int((size/contentTotal) * total) width = int((size/contentTotal) * total)
widths.append(width) widths.append(width)
# Compute style declatation corresponding to every column. else:
# There was a problem while parsing the table. Set every column
# with the same width.
widths = [int(total/self.nbOfColumns)] * self.nbOfColumns
# Compute style declaration corresponding to every column.
s = self.styleNs s = self.styleNs
i = 0 i = 0
for width in widths: for width in widths:
@ -321,14 +337,10 @@ class XhtmlEnvironment(XmlEnvironment):
currentElem.addInnerParagraph(self) currentElem.addInnerParagraph(self)
# Dump and reinitialize the current content # Dump and reinitialize the current content
content = self.currentContent.strip('\n\t') content = self.currentContent.strip('\n\t')
# We remove leading and trailing carriage returns, but not
# whitespace because whitespace may be part of the text to dump.
contentSize = len(content) contentSize = len(content)
for c in content: self.dumpString(escapeXml(content))
# We remove leading and trailing carriage returns, but not
# whitespace because whitespace may be part of the text to dump.
if XML_SPECIAL_CHARS.has_key(c):
self.dumpString(XML_SPECIAL_CHARS[c])
else:
self.dumpString(c)
self.currentContent = u'' self.currentContent = u''
# If we are within a table cell, update the total size of cell content. # If we are within a table cell, update the total size of cell content.
if self.currentTables and self.currentTables[-1].inCell: if self.currentTables and self.currentTables[-1].inCell:
@ -444,6 +456,16 @@ class XhtmlEnvironment(XmlEnvironment):
# If we are in the first row of a table, update columns count # If we are in the first row of a table, update columns count
if not table.firstRowParsed: if not table.firstRowParsed:
table.nbOfColumns += colspan table.nbOfColumns += colspan
if attrs.has_key('width') and (colspan == 1):
# Get the width, keep figures only.
width = ''
for c in attrs['width']:
if c.isdigit(): width += c
width = int(width)
# Ensure self.columnWidths is long enough
while (len(table.columnWidths)-1) < table.cellIndex:
table.columnWidths.append(None)
table.columnWidths[table.cellIndex] = width
return currentElem return currentElem
def onElementEnd(self, elem): def onElementEnd(self, elem):
@ -525,7 +547,8 @@ class XhtmlParser(XmlParser):
elif elem == 'a': elif elem == 'a':
e.dumpString('<%s %s:type="simple"' % (odfTag, e.linkNs)) e.dumpString('<%s %s:type="simple"' % (odfTag, e.linkNs))
if attrs.has_key('href'): if attrs.has_key('href'):
e.dumpString(' %s:href="%s"' % (e.linkNs, attrs['href'])) e.dumpString(' %s:href="%s"' % (e.linkNs,
escapeXml(attrs['href'])))
e.dumpString('>') e.dumpString('>')
elif elem in XHTML_LISTS: elif elem in XHTML_LISTS:
prologue = '' prologue = ''

View file

@ -19,7 +19,6 @@ mimeTypesExts = {
'image/pjpeg' : 'jpg', 'image/pjpeg' : 'jpg',
'image/gif' : 'gif' 'image/gif' : 'gif'
} }
xmlPrologue = '<?xml version="1.0" encoding="utf-8" ?>\n'
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class UnmarshalledFile: class UnmarshalledFile:

View file

@ -22,16 +22,19 @@ import xml.sax, difflib, types, cgi
from xml.sax.handler import ContentHandler, ErrorHandler, feature_external_ges from xml.sax.handler import ContentHandler, ErrorHandler, feature_external_ges
from xml.sax.xmlreader import InputSource from xml.sax.xmlreader import InputSource
from xml.sax import SAXParseException from xml.sax import SAXParseException
from appy.shared import UnicodeBuffer, xmlPrologue from appy.shared import UnicodeBuffer
from appy.shared.errors import AppyError from appy.shared.errors import AppyError
from appy.shared.utils import sequenceTypes from appy.shared.utils import sequenceTypes
from appy.shared.css import parseStyleAttribute from appy.shared.css import parseStyleAttribute
# Constants -------------------------------------------------------------------- # Constants --------------------------------------------------------------------
xmlPrologue = '<?xml version="1.0" encoding="utf-8" ?>\n'
CONVERSION_ERROR = '"%s" value "%s" could not be converted by the XML ' \ CONVERSION_ERROR = '"%s" value "%s" could not be converted by the XML ' \
'unmarshaller.' 'unmarshaller.'
CUSTOM_CONVERSION_ERROR = 'Custom converter for "%s" values produced an ' \ CUSTOM_CONVERSION_ERROR = 'Custom converter for "%s" values produced an ' \
'error while converting value "%s". %s' 'error while converting value "%s". %s'
XML_SPECIAL_CHARS = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'}
XML_ENTITIES = {'lt': '<', 'gt': '>', 'amp': '&', 'quot': "'", 'apos': "'"} XML_ENTITIES = {'lt': '<', 'gt': '>', 'amp': '&', 'quot': "'", 'apos': "'"}
HTML_ENTITIES = { HTML_ENTITIES = {
'iexcl': '¡', 'cent': '¢', 'pound': '£', 'curren': '', 'yen': '¥', 'iexcl': '¡', 'cent': '¢', 'pound': '£', 'curren': '', 'yen': '¥',
@ -60,6 +63,38 @@ for k, v in htmlentitydefs.entitydefs.iteritems():
if not HTML_ENTITIES.has_key(k) and not XML_ENTITIES.has_key(k): if not HTML_ENTITIES.has_key(k) and not XML_ENTITIES.has_key(k):
HTML_ENTITIES[k] = '' HTML_ENTITIES[k] = ''
def escapeXml(s):
'''Returns p_s, whose XML special chars have been replaced with escaped XML
entities.'''
if isinstance(s, unicode):
res = u''
else:
res = ''
for c in s:
if XML_SPECIAL_CHARS.has_key(c):
res += XML_SPECIAL_CHARS[c]
else:
res += c
return res
def escapeXhtml(s):
'''Return p_s, whose XHTML special chars and carriage return chars have
been replaced with corresponding XHTML entities.'''
if isinstance(s, unicode):
res = u''
else:
res = ''
for c in s:
if XML_SPECIAL_CHARS.has_key(c):
res += XML_SPECIAL_CHARS[c]
elif c == '\n':
res += '<br/>'
elif c == '\r':
pass
else:
res += c
return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class XmlElement: class XmlElement:
'''Represents an XML tag.''' '''Represents an XML tag.'''