[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
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)

View file

@ -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()]

View file

@ -104,6 +104,9 @@
tal:attributes="style python: 'color: %s;; font-style: italic' % event['color']"></div>
</tal:e>
</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>
</td>
</tal:td>

View file

@ -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 = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'}
# ------------------------------------------------------------------------------
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 += '<br/>'
elif c == '\r':
pass
else:
res += c
return res
# XXX To remove, present for backward compatibility only.
convertToXhtml = escapeXhtml
# ------------------------------------------------------------------------------

View file

@ -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):

View file

@ -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 ?

View file

@ -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

View file

@ -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')
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)
contentSize = len(content)
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 = ''

View file

@ -19,7 +19,6 @@ mimeTypesExts = {
'image/pjpeg' : 'jpg',
'image/gif' : 'gif'
}
xmlPrologue = '<?xml version="1.0" encoding="utf-8" ?>\n'
# ------------------------------------------------------------------------------
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.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 = '<?xml version="1.0" encoding="utf-8" ?>\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 = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'}
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 += '<br/>'
elif c == '\r':
pass
else:
res += c
return res
# ------------------------------------------------------------------------------
class XmlElement:
'''Represents an XML tag.'''