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('' % 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 += '' % 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.'''