# -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ # Copyright (c) 2007 by PloneGov # # GNU General Public License (GPL) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # # ------------------------------------------------------------------------------ from zope.interface import implements from AccessControl import ClassSecurityInfo from DateTime import DateTime from Globals import InitializeClass from Products.PloneMeeting.MeetingItem import MeetingItem from Products.PloneMeeting.utils import kupuFieldIsEmpty from Products.PloneMeeting.interfaces import IMeetingItemCustom from Products.PloneTask.ToolPloneTask import ToolPloneTask from Products.PloneTask.interfaces import IToolPloneTaskCustom class CustomMeetingItem(MeetingItem): '''Adapter that adapts a meeting item implementing IMeetingItem to the interface IMeetingItemCustom.''' implements(IMeetingItemCustom) security = ClassSecurityInfo() # Types of "notes (au Gouvernement, rectificatives)" that may be found # among an item's annexes. notesIds = ('proposition', 'proposition-corrected') # Items to discuss or not are named 'A' and 'B' for EGW. toDiscussEgw = {False: 'A', True: 'B'} # Some useful groups of meeting states notDecidedMeetingStates = ('created', 'published', 'frozen') decidedMeetingStates = ('closed', 'archived') # The 'accepted' state is managed differently and is not among the previous # tuple def __init__(self, item): self.context = item def getItemReference(self, annexNumber=None): '''Produces the EGW-specific item reference. In some cases, a reference that is specific to an annex (Note au Gouvernement, Note rectificative) must be produced. In this case p_annexNumber is inserted into the reference.''' meeting = self.context.getMeeting() meeting.portal_plonemeeting.enterProfiler('getItemRef') mDate = meeting.getDate() itemNumber = self.context.getItemNumber(relativeTo='meetingConfig') # Compute acronyms acronyms = '' groupIds = self.context.getAssociatedGroups() + \ (self.context.getProposingGroup(),) nbOfAcronyms = len(groupIds) found = 0 tool = self.context.portal_plonemeeting for group in tool.objectValues('MeetingGroup'): if group.id in groupIds: found += 1 acronym = group.getAcronym() if not acronyms: acronyms += acronym else: acronyms += '-%s' % acronym if found == nbOfAcronyms: # Ok we have all acronyms. We avoid continuing to walk into # the remaining groups for nothing. break # Must we include the "Doc." element ? (indicates presence of annexes) hasDoc = '' if self.context.getRawAnnexes(): hasDoc = 'Doc. ' # Manage annex number if annexNumber == None: annexNb = '' else: annexNb = '.%s' % str(annexNumber).zfill(2) res = '%s/%d/%s.%s/%s%d%s/%s' % (meeting.getMeetingConfigVersion(), mDate.year(), str(mDate.day()).zfill(2),str(mDate.month()).zfill(2), hasDoc, itemNumber, annexNb, acronyms) self.context.portal_plonemeeting.leaveProfiler() return res keepWithNextStyles = {'para': 'pmParaKeepWithNext', 'item': 'podItemKeepWithNext'} emptyParagraphs = ('
', '', '
', '
', '
') # The 2 '
' are different (2 different blank # chars) def transformRichTextField(self, fieldName, richContent): '''For XHTML fields "description" and "decision", I need to add a predefined CSS class to the last paragraph in order to tell POD that it must always be on the same page than the next paragraph. This way, we avoid problems such as finding a "signature block" alone on the top of one page.''' res = richContent if fieldName != 'decision': # Transforming the content of "description" fields may cause # problems for the color system, that updates pm_modification_date # if it detects that a change occurs on either the title or the # description of an item (in MeetingItem.processForm and in # Meeting.updateItem the description new ad old content of # description field are compared). return res if kupuFieldIsEmpty(res): # Do nothing, the field is empty. Else, if your remove really # everything from the Kupu field, when using base_edit the next # time, the visual editor will not show up. return res # First, remove empty paragraphs for emptyPara in self.emptyParagraphs: res = res.replace(emptyPara, '') # A paragraph way be a "p" or "li". If it is a "p", I will add style # (if not already done) "pmItemKeepWithNext"; if it is a "li" I will # add style "pmParaKeepWithNext" (if not already done). lastParaIndex = res.rfind('
lastItemIndex: styleKey = 'para' elemLenght = 2 maxIndex = max(lastParaIndex, lastItemIndex) kwnStyle = self.keepWithNextStyles[styleKey] # Does this element already have a "class" attribute? if res.find('class="%s"' % kwnStyle, maxIndex) == -1: # No: I add the style res = res[:maxIndex+elemLenght] + (' class="%s" ' % kwnStyle) + \ res[maxIndex+elemLenght:] return res def onEdit(self, isCreated): '''Every time PloneMeeting detects a change in the title or description of a given item, it changes the pm_modification_date. The PloneMeeting color system uses this information to display (a) newly created items or (b) items whose last changes have not been consulted yet by the authenticated member in a special color. The EGW users want (a) and not (b). It is achieved by forcing the pm_modification_date for an item to be always equal to its creation_date.''' if not isCreated: self.context.pm_modification_date = self.context.creation_date # The methods below are not in IMeetingItem(Custom). They are used in # the EGW-specific POD templates. def modifiedAfterMeetingIsClosed(self): '''Was this item modified since the meeting was closed? In this case it is a "Notification rectificative", not a "Notification (définitive)".''' res = False meeting = self.context.getMeeting() if meeting: lastModifDate = self.context.modification_date # Find last 'confirm' date in workflow history lastCloseDate = None for wfChange in meeting.workflow_history['meeting_workflow']: if wfChange['action'] == 'close': lastCloseDate = wfChange['time'] if not lastCloseDate: lastCloseDate = DateTime() # Now if lastModifDate > lastCloseDate: res = True return res def hasSeveralNotes(self): '''Does this item has, in its annexes, more than one annex of types "Note au Gouvernement" and "Note rectificative"?''' nbOfNotes = 0 for annex in self.context.getAnnexes(): if annex.getMeetingFileType().id in self.notesIds: nbOfNotes += 1 return nbOfNotes > 1 def getNotesInfo(self): '''Returns info (title, annex-specific reference) about the annexes of types "Note au Gouvernement" and "Note rectificative", excepted for the original "Note au Gouvernement".''' res = [] i = 0 for annex in self.context.getAnnexesByType( makeSubLists=False, typesIds=self.notesIds): i += 1 if i > 1: res.append( (annex['Title'], self.getItemReference(i)) ) return res def getEgwNb(self): '''Returns the item number + A/B flag.''' nb = self.context.getItemNumber(relativeTo='meeting') if nb == None: nb = '' else: nb = str(nb) return '%s%s' % (self.toDiscussEgw[self.context.getToDiscuss()], nb) def getDecisionQualifier(self): '''According to meeting state, a decision may be qualified differently.''' if self.context.queryState() == 'confirmed': # In this case we must rely exclusively on the item state if self.modifiedAfterMeetingIsClosed(): res = ' RECTIFICATIVE' else: res = '' else: # In other cases, we must rely on meeting state instead of item # state to qualify the decision. res = ' PROVISOIRE' # The decision is being prepared meeting = self.context.getMeeting() if meeting: meetingState = meeting.queryState() if meetingState in self.notDecidedMeetingStates: res = ' PROVISOIRE' elif meetingState == 'decided': res = ' PROVISOIRE' elif meetingState in self.decidedMeetingStates: if self.modifiedAfterMeetingIsClosed(): res = ' RECTIFICATIVE' else: res = '' return res def decisionIsFinal(self): '''Must I show elements that must only be shown for finalized decisions ?''' res = False meeting = self.context.getMeeting() if meeting and (meeting.queryState() == 'archived'): res = True return res def showSignatureImage(self): '''Must I include the scanned signature of Renaud Witmeur?''' res = False if self.decisionIsFinal() and \ ('Witmeur' in self.context.getMeeting().getSignatures()) and \ (self.context.queryState() != 'refused'): res = True return res def showMeetingSignature(self): '''Must I show the whole "signature" block linked to the meeting?''' res = False if self.context.getMeeting(): if (not self.context.getItemSignatures()) and \ self.context.getMeeting().getSignatures() and \ (self.context.queryState() != 'refused'): res = True return res # ------------------------------------------------------------------------------ class CustomToolPloneTask(ToolPloneTask): '''Adapter that adapts the ToolPloneTask implementing IToolPloneTask to the interface IToolPloneTaskCustom.''' implements(IToolPloneTaskCustom) security = ClassSecurityInfo() def __init__(self, tool): self.context = tool # Specific methods, not part of IToolPloneTask, that are used within the # EGW-specific "Tasks" pod templates. def getPageTitle(self, queryName, endDate): '''Gets the page title.''' if queryName == 'tasksPortletQuery': res = 'Echéancier au ' + \ endDate.strftime(self.context.getDatesFormat()) else: res = self.context.utranslate(queryName, domain='PloneTask') return res def getEndDateMonth(self, task): if task.getEndDate(): res = self.context.utranslate(task.getEndDate().mm(), domain='PloneTask') + ' ' + \ str(task.getEndDate().year()) else: res = 'Tâches sans échéance' return res def getShowableEndDate(self, task): '''Gets a tasks' end date in the right format.''' if task.getEndDate(): res = task.getEndDate().strftime(self.context.getDatesFormat()) else: res = '-' return res def showMonthLine(self, tasks, task): res = False if tasks.index(task) == 0: res = True else: previousTask = tasks[tasks.index(task)-1] if task.getEndDate() and not previousTask.getEndDate(): res = True elif task.getEndDate() and previousTask.getEndDate() and \ (task.getEndDate().month() != previousTask.getEndDate().month()): res = True return res # ------------------------------------------------------------------------------ InitializeClass(CustomMeetingItem) InitializeClass(CustomToolPloneTask) # ------------------------------------------------------------------------------