# IssueTrackerProduct # www.issuetrackerproduct.com # # Peter Bengtsson # License: ZPL # # python import sys # Zope from Globals import InitializeClass, DTMLFile from AccessControl import ClassSecurityInfo from zLOG import LOG, ERROR, INFO, PROBLEM, WARNING from DateTime import DateTime # Is CMF installed? try: from Products.CMFCore.utils import getToolByName as CMF_getToolByName except ImportError: CMF_getToolByName = None # Product from Issue import IssueTrackerIssue from TemplateAdder import addTemplates2Class import Utils from Utils import unicodify, asciify from Constants import * from Permissions import VMS from I18N import _ from Errors import ConfigurationError #---------------------------------------------------------------------------- manage_addIssueTrackerIssueThreadForm = DTMLFile('dtml/NotImplemented', globals()) def manage_addIssueTrackerIssueThread(*args, **kw): """ This is not supported """ raise NotImplementedError, "This method should not be used" #---------------------------------------------------------------------------- class IssueTrackerIssueThread(IssueTrackerIssue): """ Issuethreads class """ meta_type = ISSUETHREAD_METATYPE icon = '%s/issuethread.gif'%ICON_LOCATION _properties=({'id':'title', 'type': 'ustring', 'mode':'w'}, {'id':'comment', 'type': 'utext', 'mode':'w'}, {'id':'threaddate', 'type': 'date', 'mode':'w'}, {'id':'fromname', 'type': 'ustring', 'mode':'w'}, {'id':'email', 'type': 'string', 'mode':'w'}, {'id':'acl_adder', 'type': 'string', 'mode':'w'}, {'id':'display_format','type': 'string', 'mode':'w'}, ) security=ClassSecurityInfo() manage_options = ( {'label':'Properties', 'action':'manage_propertiesForm'}, {'label':'Contents', 'action':'manage_main'}, ) acl_adder = '' # backward compatability def __init__(self, id, title, comment, threaddate, fromname, email, display_format=None, acl_adder='', submission_type=''): """ create thread """ self.id = str(id) self.title = unicodify(title) self.comment = unicodify(comment) if isinstance(threaddate, basestring): threaddate = DateTime(threaddate) self.threaddate = threaddate self.fromname = unicodify(fromname) if isinstance(email, basestring): email = asciify(email, 'ignore') self.email = email if display_format: self.display_format = display_format else: self.display_format = self.default_display_format if acl_adder is None: acl_adder = '' self.acl_adder = acl_adder self.submission_type = submission_type self.email_message_id = None def getTitle(self): """ return title """ return self.title def getThreadDate(self): """ return threaddate """ return self.threaddate def getModifyDate(self): return self.bobobase_modification_time() def getFromname(self, issueusercheck=True): """ return fromname """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # the userfolder (as it was saved) no longer exists return self.fromname if uf.meta_type == ISSUEUSERFOLDER_METATYPE: if uf.data.has_key(name): issueuserobj = uf.data[name] return issueuserobj.getFullname() or self.fromname elif CMF_getToolByName and hasattr(uf, 'portal_membership'): mtool = CMF_getToolByName(self, 'portal_membership') member = mtool.getMemberById(name) if member and member.getProperty('fullname'): return member.getProperty('fullname') return self.fromname def getEmail(self, issueusercheck=True): """ return email """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # the userfolder (as it was saved) no longer exists return self.email if uf.meta_type == ISSUEUSERFOLDER_METATYPE: if uf.data.has_key(name): issueuserobj = uf.data[name] return issueuserobj.getEmail() or self.email elif CMF_getToolByName and hasattr(uf, 'portal_membership'): mtool = CMF_getToolByName(self, 'portal_membership') member = mtool.getMemberById(name) if member and member.getProperty('email'): return member.getProperty('email') return self.email def getACLAdder(self): """ return acl_adder """ return self.acl_adder def _setACLAdder(self, acl_adder): """ set acl_adder """ self.acl_adder = acl_adder def getComment(self): """ return comment """ return self.comment def getCommentPure(self): """ return comment purified. If the comment contains HTML for example, remove it.""" comment = self.getComment() if self.getDisplayFormat() =='html': # textify() coverts "Something" to "Something". Simple. comment = Utils.textify(comment) # a very common thing is that the description contains # these faux double linebreaks and when you run textify() # on '

 

' the result is ' '. Too many of # those result in '     ' which # isn't pure and purifying is what this method aims to do comment = comment.replace('

 

','') return comment def _unicode_comment(self): """ make the comment of this thread a unicode string """ self.comment = unicodify(self.comment) self._prerendered_comment = unicodify(self._prerendered_comment) def _prerender_comment(self): """ Run the methods that pre-renders the comment of the issue. """ comment = self.getComment() display_format = self.display_format formatted = self.ShowDescription(comment+' ', display_format) if self.getSubmissionType()=='email': formatted = Utils.highlight_signature(formatted, 'class="sig"') formatted = self._findIssueLinks(formatted) self._prerendered_comment = formatted def _getFormattedComment(self): """ return the comment formatted """ if getattr(self, '_prerendered_comment', None): formatted = self._prerendered_comment else: comment = self.getComment() display_format = self.display_format formatted = self.ShowDescription(comment+' ', display_format) return formatted def showComment(self): """ combine ShowDescription (which is generic) with this threads display format.""" formatted = self._getFormattedComment() return self.HighlightQ(formatted) def getSubmissionType(self): """ return how it was submitted, empty string if not found """ return getattr(self, 'submission_type', '') def getEmailMessageId(self): """ if the email was submitted via email it will most likely have a message id """ # important to use the aq_base because otherwise we might pick it up # from the parenting issue base = getattr(self, 'aq_base', self) return getattr(base, 'email_message_id', None) def _setEmailMessageId(self, message_id): """ set the email message id """ assert message_id.strip(), "Message_id not valid" self.email_message_id = message_id.strip() def _setEmailOriginal(self, original_email): """ set the original_email attribute """ self.original_email = original_email def hasEmailOriginal(self): """ return if we have a 'original_email' attribute set """ return hasattr(self, 'original_email') def ShowOriginalEmail(self, REQUEST): """ return the original email text """ if REQUEST: REQUEST.RESPONSE.setHeader('Content-Type','text/plain') return self.original_email def index_object(self, idxs=None): """A common method to allow Findables to index themselves.""" path = '/'.join(self.getPhysicalPath()) catalog = self.getCatalog() if idxs is None: # because I don't want to put mutable defaults in # the keyword arguments idxs = ['comment','meta_type','fromname','email','path', 'modifydate', 'filenames'] else: # No matter what, when indexing you must always include 'path' # otherwise you might update indexes without putting the object # brain in the catalog. If that happens the object won't be # findable in the searchResults(path='/some/path') even if it's # findable on other indexes such as comment. if 'path' not in idxs: idxs.append('path') indexes = catalog._catalog.indexes if 'filenames' not in idxs and indexes.has_key('filenames'): idxs.append('filenames') # This test should not be needed in 2009 when hopefully # everyone has updated the catalogs to contain the new index if 'path' in idxs and not indexes.has_key('path'): raise ConfigurationError( "Catalog is missing 'path' index. Update Everything" ) if 'modifydate' in idxs and not indexes.has_key('modifydate'): idxs.remove('modifydate') catalog.catalog_object(self, path, idxs=idxs) def getFromname_idx(self): return self.getFromname() def getComment_idx(self): return self.getComment() def unindex_object(self): """A common method to allow Findables to unindex themselves.""" self.getCatalog().uncatalog_object('/'.join(self.getPhysicalPath())) security.declareProtected(VMS, 'manage_editProperties') def manage_editProperties(self, REQUEST): """ re-prerender the description of the issue after manual change """ result = IssueTrackerIssue.manage_editProperties(self, REQUEST) try: self._prerender_comment() except: if DEBUG: raise else: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Unable to _prerender_comment() in manage_editProperties()", error=sys.exc_info()) return result def manage_afterAdd(self, REQUEST, RESPONSE): """ intercept so that we prerender always """ try: self._prerender_comment() except: if DEBUG: raise else: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Unable to _prerender_comment() after add", error=sys.exc_info()) security.declareProtected(VMS, 'assertAllProperties') def assertAllProperties(self): """ make sure issue has all properties """ props = { # currently nothing } count = 0 for key, default in props.items(): if not self.__dict__.has_key(key): self.__dict__[key] = default count += 1 # check that self.fromname is as good as self.getFromname() attr_fromname = self.getFromname(issueusercheck=False) linked_fromname = self.getFromname(issueusercheck=True) if linked_fromname != attr_fromname: # for sanity, check that the linked fromname is ok if linked_fromname: self.fromname = linked_fromname count += 1 # check that self.email is as good as self.getFromname() attr_email = self.getEmail(issueusercheck=False) linked_email = self.getEmail(issueusercheck=True) if linked_email != attr_email: # for sanity, check that the linked email is ok if linked_email: self.email = linked_email count += 1 return count def showThreadFileattachments(self, only_temporary=0): """ wrap around the showFileattachments() method """ files = [] container = self.getFileattachmentContainer(only_temporary=only_temporary) return self.showFileattachments(container) def filenames(self): """ return all the filenames of this issue splitted """ files = self.objectValues('File') all = [] for file in files: all.extend(Utils.filenameSplitter(file.getId())) return Utils.uniqify([x.lower() for x in all]) InitializeClass(IssueTrackerIssueThread) #----------------------------------------------------------------------------- class IssueTrackerDraftIssueThread(IssueTrackerIssueThread): """ There are used for the 'Save as draft' feature when writing a followup on an issue. The major difference between these and IssueTrackerIssueThread is that these draft objects must not be indexed in the Catalog. """ meta_type = ISSUETHREAD_DRAFT_METATYPE icon = '%s/issuethreaddraft.gif'%ICON_LOCATION security=ClassSecurityInfo() manage_options = ( {'label':'Contents', 'action':'manage_main'}, {'label':'Properties', 'action':'manage_draftthread_properties'}, ) def __init__(self, id, issueid, action, title=None, comment=None, threaddate=None, fromname=None, email=None, display_format=None, acl_adder=None, is_autosave=False): """ create draft thread """ self.id = str(id) self.issueid = issueid self.action = unicodify(action) self.title = unicodify(title) self.comment = unicodify(comment) if isinstance(threaddate, basestring): threaddate = DateTime(threaddate) self.threaddate = threaddate self.fromname = unicodify(fromname) if isinstance(email, basestring): email = asciify(email) self.email = email self.display_format = display_format self.is_autosave = bool(is_autosave) if not acl_adder: # '', 0 or None acl_adder = '' self.acl_adder = acl_adder # legacy support is_autosave = False def getIssueId(self): """ return issueid """ return self.issueid def getIssuePath(self): """ return a relative URL to where the issue is """ rootpath = self._getIssueContainer().absolute_url_path() if rootpath == '/': return '/' + self.getIssueId() else: return rootpath + '/' + self.getIssueId() def getModifyDate(self): return self.bobobase_modification_time() def index_object(self, *args, **kws): """ do NOT index this object """ pass def unindex_object(self, *args, **kws): """ nothing to unindex """ pass def manage_afterAdd(self, REQUEST, RESPONSE): """ the base class defines this to prerender the comment, something we don't want to do. """ pass def ModifyThread(self, title=None, comment=None, display_format=None, fromname=None, email=None, acl_adder=None, is_autosave=False, REQUEST=None): """ since normal threads don't allow changes, we need to add this very custom method to the drafts """ if title is not None: self.title = title if comment is not None: self.comment = comment if display_format is not None: self.display_format = display_format if fromname is not None: self.fromname = fromname if email is not None: self.email = email if acl_adder is not None: self.acl_adder = acl_adder self.is_autosave = bool(is_autosave) def isAutosave(self): """ return if this was saved as an autosave or a plain draft """ return self.is_autosave def shortDescription(self, maxlength=55, html=True): """ return a simplified description where the title is shown and then as much of the description as possible. """ title = self.getTitle() if title is None: title = self.action if not title.strip(): if html: title = "(%s)" % _("No subject") else: title = "(%s)" % _("No subject") desc = self.getCommentPure() shortened = self.lengthLimit(title, maxlength, "|...|") if shortened.endswith('|...|'): # the title was shortened shortened = shortened[:-len('|...|')] if html: return "%s..."%shortened else: return shortened+'...' else: # i.e. title==shortened # put some of the description ontop if len(shortened) + len(desc) > maxlength: desc = self.lengthLimit(desc, maxlength-len(title)) if html: return u"%s, %s"%(shortened, desc) else: return u"%s, %s"%(shortened, desc) def get__dict__keys(self): """ return the names of the keys we might have """ return ('issueid', 'action', 'title', 'comment', 'fromname', 'email', 'display_format', 'acl_adder', 'is_autosave') def get__dict__nicely(self): """ same as get__dict__keys() but we wrap it nicely """ ok = [] for key in self.get__dict__keys(): if self.__dict__.get(key, None) is not None: ok.append({'key':key, 'value':self.__dict__.get(key)}) return ok dtmls = ({'f':'dtml/draftissuethread_properties', 'n':'manage_draftthread_properties'}, ) addTemplates2Class(IssueTrackerDraftIssueThread, dtmls, "dtml") InitializeClass(IssueTrackerDraftIssueThread)