diff --git a/bin/job.py b/bin/job.py
index f33579f..75bd9c9 100644
--- a/bin/job.py
+++ b/bin/job.py
@@ -56,6 +56,9 @@ else:
# Log as Zope admin
from AccessControl.SecurityManagement import newSecurityManager
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'):
user = user.__of__(app.acl_users)
newSecurityManager(None, user)
diff --git a/gen/calendar.py b/gen/calendar.py
index d74a77d..6b18c82 100644
--- a/gen/calendar.py
+++ b/gen/calendar.py
@@ -17,8 +17,8 @@ class Calendar(Type):
specificWritePermission=False, width=None, height=300,
colspan=1, master=None, masterValue=None, focus=False,
mapping=None, label=None, maxEventLength=50,
- otherCalendars=None, startDate=None, endDate=None,
- defaultDate=None):
+ otherCalendars=None, additionalInfo=None, startDate=None,
+ endDate=None, defaultDate=None):
Type.__init__(self, validator, (0,1), None, default, False, False,
show, page, group, layouts, move, False, False,
specificReadPermission, specificWritePermission,
@@ -52,6 +52,12 @@ class Calendar(Type):
# leading "#" when relevant) into which events of the calendar must
# appear.
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,
# via p_startDate and p_endDate. Those parameters, if given, must hold
# 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])
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):
'''Returns the (dynamic or static) event types as defined in
self.eventTypes.'''
@@ -158,6 +171,13 @@ class Calendar(Type):
res = days[day]
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):
'''Returns True if, at p_date, an event is found of the same type as
p_otherEvents.'''
@@ -202,11 +222,20 @@ class Calendar(Type):
else:
return DateTime() # Now
- def createEvent(self, obj, date, handleEventSpan=True):
- '''Create a new event in the calendar, at some p_date (day). If
- p_handleEventSpan is True, we will use rq["eventSpan"] and also
+ def createEvent(self, obj, date, eventType=None, eventSpan=None,
+ handleEventSpan=True):
+ '''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.'''
+ obj = obj.o # Ensure p_obj is not a wrapper.
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()
# Check that the "preferences" dict exists or not.
if not hasattr(obj.aq_base, self.name):
@@ -230,11 +259,11 @@ class Calendar(Type):
daysDict[day] = events = PersistentList()
# Create and store the event, excepted if an event already exists.
if not events:
- event = Object(eventType=rq['eventType'])
+ event = Object(eventType=eventType)
events.append(event)
# Span the event on the successive days if required
- if handleEventSpan and rq['eventSpan']:
- nbOfDays = min(int(rq['eventSpan']), self.maxEventLength)
+ if handleEventSpan and eventSpan:
+ nbOfDays = min(int(eventSpan), self.maxEventLength)
for i in range(nbOfDays):
date = date + 1
self.createEvent(obj, date, handleEventSpan=False)
@@ -243,6 +272,7 @@ class Calendar(Type):
'''Deletes an event. It actually deletes all events at rq['day'].
If p_handleEventSpan is True, we will use rq["deleteNext"] to
delete successive events, too.'''
+ obj = obj.o # Ensure p_obj is not a wrapper.
rq = obj.REQUEST
if not self.getEventsAt(obj, date): return
daysDict = getattr(obj, self.name)[date.year()][date.month()]
diff --git a/gen/ui/widgets/calendar.pt b/gen/ui/widgets/calendar.pt
index 94cda3b..d89c805 100644
--- a/gen/ui/widgets/calendar.pt
+++ b/gen/ui/widgets/calendar.pt
@@ -104,6 +104,9 @@
tal:attributes="style python: 'color: %s;; font-style: italic' % event['color']">
+ Additional info
+
diff --git a/pod/__init__.py b/pod/__init__.py
index 8d00957..2bf79d0 100644
--- a/pod/__init__.py
+++ b/pod/__init__.py
@@ -19,6 +19,7 @@
# ------------------------------------------------------------------------------
import time
from appy.shared.utils import Traceback
+from appy.shared.xml_parser import escapeXhtml
# Some POD-specific constants --------------------------------------------------
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_INNER_TAGS = ('b', 'i', 'u', 'em')
XHTML_UNSTYLABLE_TAGS = XHTML_LISTS + ('li', 'a')
-XML_SPECIAL_CHARS = {'<': '<', '>': '>', '&': '&', '"': '"',
- "'": '''}
# ------------------------------------------------------------------------------
class PodError(Exception):
@@ -83,17 +82,6 @@ class PodError(Exception):
buffer.write('%s>' % withinElement.OD.elem)
dump = staticmethod(dump)
-def convertToXhtml(s):
- '''Produces the XHTML-friendly version of p_s.'''
- res = ''
- for c in s:
- if XML_SPECIAL_CHARS.has_key(c):
- res += XML_SPECIAL_CHARS[c]
- elif c == '\n':
- res += '
'
- elif c == '\r':
- pass
- else:
- res += c
- return res
+# XXX To remove, present for backward compatibility only.
+convertToXhtml = escapeXhtml
# ------------------------------------------------------------------------------
diff --git a/pod/buffers.py b/pod/buffers.py
index c17b212..21c547f 100644
--- a/pod/buffers.py
+++ b/pod/buffers.py
@@ -20,11 +20,11 @@
import re
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.actions import IfAction, ElseAction, ForAction, VariableAction, \
NullAction
-from appy.shared import xmlPrologue
# ------------------------------------------------------------------------------
class ParsingError(Exception): pass
@@ -157,11 +157,7 @@ class Buffer:
def dumpContent(self, content):
'''Dumps string p_content into the buffer.'''
- for c in content:
- if XML_SPECIAL_CHARS.has_key(c):
- self.write(XML_SPECIAL_CHARS[c])
- else:
- self.write(c)
+ self.write(escapeXml(content))
# ------------------------------------------------------------------------------
class FileBuffer(Buffer):
diff --git a/pod/parts.py b/pod/parts.py
index 505fc35..7ba6790 100644
--- a/pod/parts.py
+++ b/pod/parts.py
@@ -10,8 +10,9 @@ class OdtTable:
tns = 'table:'
txns = 'text:'
- def __init__(self, name, paraStyle, cellStyle, nbOfCols,
- paraHeaderStyle=None, cellHeaderStyle=None, html=False):
+ def __init__(self, name, paraStyle='podTablePara', cellStyle='podTableCell',
+ nbOfCols=1, paraHeaderStyle=None, cellHeaderStyle=None,
+ html=False):
# An ODT table must have a name. In the case of an HTML table, p_name
# represents the CSS class for the whole table.
self.name = name
@@ -24,7 +25,7 @@ class OdtTable:
# The default style of every paragraph within a header cell
self.paraHeaderStyle = paraHeaderStyle or paraStyle
# 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
self.res = ''
# Do we need to generate an HTML table instead of an ODT table ?
diff --git a/pod/test/Tester.py b/pod/test/Tester.py
index 4478181..ba15084 100644
--- a/pod/test/Tester.py
+++ b/pod/test/Tester.py
@@ -21,9 +21,9 @@ import os, os.path, sys, zipfile, re, shutil
import appy.shared.test
from appy.shared.test import TesterError
from appy.shared.utils import FolderDeleter
+from appy.shared.xml_parser import escapeXml
from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.pod.renderer import Renderer
-from appy.pod import XML_SPECIAL_CHARS
# TesterError-related constants ------------------------------------------------
TEMPLATE_NOT_FOUND = 'Template file "%s" was not found.'
@@ -70,12 +70,7 @@ class AnnotationsRemover(OdfParser):
self.res += '%s>' % elem
def characters(self, content):
e = OdfParser.characters(self, content)
- if not self.ignore:
- for c in content:
- if XML_SPECIAL_CHARS.has_key(c):
- self.res += XML_SPECIAL_CHARS[c]
- else:
- self.res += c
+ if not self.ignore: self.res += escapeXml(content)
def getResult(self):
return self.res
diff --git a/pod/xhtml2odt.py b/pod/xhtml2odt.py
index 85df287..a03fd61 100644
--- a/pod/xhtml2odt.py
+++ b/pod/xhtml2odt.py
@@ -10,7 +10,7 @@
# ------------------------------------------------------------------------------
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.styles_manager import Style
from appy.pod import *
@@ -235,6 +235,10 @@ class HtmlTable:
# The following list stores, for every column, the size of the biggest
# content of all its cells.
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):
'''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
p_renderer.dynamicStyles.'''
total = 65000.0 # A number representing the total width of the table
- # Ensure first that self.columnContentSizes is correct
- if (len(self.columnContentSizes) != self.nbOfColumns) or \
- (None in self.columnContentSizes):
- # There was a problem while parsing the table. Set every column
- # with the same width.
- widths = [int(total/self.nbOfColumns)] * self.nbOfColumns
+ # Use (a) self.columnWidths if complete, or
+ # (b) self.columnContentSizes if complete, or
+ # (c) a fixed width else.
+ if self.columnWidths and (len(self.columnWidths) == self.nbOfColumns) \
+ and (None not in self.columnWidths):
+ # 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:
+ toUse = None
+ if toUse:
widths = []
# Compute the sum of all column content sizes
contentTotal = 0
- for size in self.columnContentSizes: contentTotal += size
+ for size in toUse: contentTotal += size
contentTotal = float(contentTotal)
- for size in self.columnContentSizes:
+ for size in toUse:
width = int((size/contentTotal) * total)
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
i = 0
for width in widths:
@@ -321,14 +337,10 @@ class XhtmlEnvironment(XmlEnvironment):
currentElem.addInnerParagraph(self)
# Dump and reinitialize the current content
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)
- for c in 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.dumpString(escapeXml(content))
self.currentContent = u''
# If we are within a table cell, update the total size of cell content.
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 not table.firstRowParsed:
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
def onElementEnd(self, elem):
@@ -525,7 +547,8 @@ class XhtmlParser(XmlParser):
elif elem == 'a':
e.dumpString('<%s %s:type="simple"' % (odfTag, e.linkNs))
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('>')
elif elem in XHTML_LISTS:
prologue = ''
diff --git a/shared/__init__.py b/shared/__init__.py
index ec9cd29..816699c 100644
--- a/shared/__init__.py
+++ b/shared/__init__.py
@@ -19,7 +19,6 @@ mimeTypesExts = {
'image/pjpeg' : 'jpg',
'image/gif' : 'gif'
}
-xmlPrologue = '\n'
# ------------------------------------------------------------------------------
class UnmarshalledFile:
diff --git a/shared/xml_parser.py b/shared/xml_parser.py
index 930802d..477c6de 100644
--- a/shared/xml_parser.py
+++ b/shared/xml_parser.py
@@ -22,16 +22,19 @@ import xml.sax, difflib, types, cgi
from xml.sax.handler import ContentHandler, ErrorHandler, feature_external_ges
from xml.sax.xmlreader import InputSource
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.utils import sequenceTypes
from appy.shared.css import parseStyleAttribute
# Constants --------------------------------------------------------------------
+xmlPrologue = '\n'
CONVERSION_ERROR = '"%s" value "%s" could not be converted by the XML ' \
'unmarshaller.'
CUSTOM_CONVERSION_ERROR = 'Custom converter for "%s" values produced an ' \
'error while converting value "%s". %s'
+XML_SPECIAL_CHARS = {'<': '<', '>': '>', '&': '&', '"': '"',
+ "'": '''}
XML_ENTITIES = {'lt': '<', 'gt': '>', 'amp': '&', 'quot': "'", 'apos': "'"}
HTML_ENTITIES = {
'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):
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 += '
'
+ elif c == '\r':
+ pass
+ else:
+ res += c
+ return res
+
# ------------------------------------------------------------------------------
class XmlElement:
'''Represents an XML tag.'''