def add_response_for_files(object, event): """If a file/image is added or deleted, add a response.""" if isinstance(event, ObjectAddedEvent): parent = event.newParent # do not add response if attachment is migrating to DX checkid = object.id + '_MIGRATION_' if any(att.id == checkid for att in parent.getFolderContents()): return if parent.portal_type == "Issue": issue = parent new_response = Response("") else: return elif isinstance(event, ObjectModifiedEvent): if object.aq_parent.portal_type == "Issue": issue = object.aq_parent new_response = Response("") else: return elif isinstance(event, ObjectRemovedEvent): if event.oldParent.portal_type == "Issue": issue = event.oldParent new_response = Response("Attachment deleted: " + object.title) else: return if new_response: new_response.attachment = object new_response.mimetype =\ api.portal.get_registry_record('poi.default_issue_mime_type') new_response.type = "file" folder = IResponseContainer(issue) folder.add(new_response)
def testDeleteResponseLeavesStaleDescription(self): found = len(self.catalog.searchResults( portal_type = 'PoiIssue', SearchableText = 'a-response')) >= 1 self.failUnless(found) from Products.Poi.adapters import IResponseContainer container = IResponseContainer(self.issue) container.delete('0') self.failIf('a-response' in self.issue.SearchableText()) found = len(self.catalog.searchResults( portal_type = 'PoiIssue', SearchableText = 'a-response')) >= 1 self.failIf(found, 'OLD ISSUE RAISING ITS HEAD AGAIN: Deleted response causes stale issue SearchableText')
def testDeleteResponseLeavesStaleDescription(self): found = len( self.catalog.searchResults(portal_type='Issue', SearchableText='a-response')) >= 1 self.failUnless(found) from Products.Poi.adapters import IResponseContainer container = IResponseContainer(self.issue) container.delete('0') self.failIf('a-response' in self.issue.SearchableText()) found = len( self.catalog.searchResults(portal_type='Issue', SearchableText='a-response')) >= 1 self.failIf(found, ("OLD ISSUE RAISING ITS HEAD AGAIN: Deleted " "response causes stale issue SearchableText"))
def options(self): mapping = super(NewResponseMail, self).options() context = aq_inner(self.context) folder = IResponseContainer(context) response = folder[self.response_id] responseText = su(response.text) paras = responseText.splitlines() # Indent the response details so they are correctly # interpreted as a literal block after the double colon behind # the 'Response Details' header. This only really matters # when someone interprets this as reStructuredText though. responseDetails = u'\n\n'.join([wrapper.fill(p) for p in paras]) changes = [] for change in response.changes: before = su(change.get('before')) after = su(change.get('after')) name = su(change.get('name')) # Some changes are workflow changes, which can be translated. # Note that workflow changes are in the plone domain. before = translate(before, 'plone', context=self.request) after = translate(after, 'plone', context=self.request) name = translate(name, 'Poi', context=self.request) changes.append(dict(name=name, before=before, after=after)) if response.attachment: attachment_id = getattr(response.attachment, 'filename', u'') else: attachment_id = u'' mapping['response_details'] = responseDetails mapping['changes'] = changes mapping['attachment_id'] = attachment_id return mapping
def createResponse(self, issue, text='Response text', issueTransition='', newSeverity=None, newTargetRelease=None, newResponsibleManager=None, attachment=None): """Create a response to the given tracker, and perform workflow and rename-after-creation initialisation""" from Products.Poi.browser.response import Create request = issue.REQUEST request.form['response'] = text request.form['transition'] = issueTransition if newSeverity is not None: request.form['severity'] = newSeverity if newTargetRelease is not None: request.form['targetRelease'] = newTargetRelease if newResponsibleManager is not None: request.form['responsibleManager'] = newResponsibleManager if attachment is not None: request.form['attachment'] = attachment create_view = Create(issue, request) # A response is created by calling this view: create_view() container = IResponseContainer(issue) id = str(len(container) -1) response = container[id] # In tests we need to fire this event manually: notify(ObjectModifiedEvent(response)) return response
def __init__(self, context, request): self.context = context self.request = request self.folder = IResponseContainer(context) self.mimetype = api.portal.get_registry_record( 'poi.default_issue_mime_type') self.use_wysiwyg = (self.mimetype == 'text/html')
def getLogEntries(self, count=20): context = aq_inner(self.context) issuefolder = context.restrictedTraverse('@@issuefolder') # First we get the most recently modified issues, which means # the most recently added, or the ones with the most recent # responses. issues = [ i.getObject() for i in issuefolder.getFilteredIssues( sort_on='modified', sort_limit=count, sort_order='reverse') ] responses = [] for issue in issues: folder = IResponseContainer(issue) for res in list(folder): if not res: continue item = dict(parent=issue, response=res, date=fixDate(res.date)) responses.append(item) items = responses + issues # sort entries items.sort(key=getEntrySortingKey, reverse=True) results = [] for item in items[:count]: if not hasattr(item, 'portal_type'): # Response date = item.get('date') issue = item.get('parent') response = item.get('response') data = { 'type': 'response', 'author': self.getPrettyName(response.creator), 'date': date, 'timedelta': self.getTimeDelta(date), 'changes': response.changes, 'issue': issue.title_or_id(), 'url': issue.absolute_url(), 'text': response.rendered_text } else: # Issue data = { 'title': item.title_or_id(), 'type': item.portal_type, 'author': self.getPrettyName(item.Creator()), 'date': item.created(), 'url': item.absolute_url(), 'timedelta': self.getTimeDelta(item.created()), 'text': item.getDetails() } results.append(data) return results
def __iter__(self): for item in self.previous: keys = item.keys() pathkey = self.pathkey(*keys)[0] if not pathkey or ('poi_responses' not in keys): yield item; continue path = item[pathkey] obj = self.context.unrestrictedTraverse(path.lstrip('/'), None) if obj is None: # path doesn't exist yield item; continue if isinstance(obj, PoiIssue): container = IResponseContainer(obj) i = 0 for response in item['poi_responses']: attachment_filename = response.pop('attachment_filename') attachment_url = response.pop('attachment_url') to_add = True try: r_obj = container[i] to_add = False except IndexError: r_obj = Response(response.get('text','')) for k, v in response.items(): setattr(r_obj, k, v) if attachment_filename and attachment_url: # import pdb; pdb.set_trace() attachment = self.setAttachment(attachment_filename, self.orig_plone_url + '/'.join(obj.getPhysicalPath()[2:]) + attachment_url) #self.orig_plone_url + '/' + attachment_url) if attachment: r_obj.attachment = attachment if to_add: container.add(r_obj) i += 1 yield item
def options(self): context = aq_inner(self.context) folder = IResponseContainer(context) response = folder[self.response_id] tracker = aq_parent(context) portal_url = getToolByName(context, 'portal_url') portal = portal_url.getPortalObject() portal_membership = getToolByName(portal, 'portal_membership') fromName = su(portal.getProperty('email_from_name', '')) creator = response.creator creatorInfo = portal_membership.getMemberInfo(creator) if creatorInfo and creatorInfo['fullname']: responseAuthor = creatorInfo['fullname'] else: responseAuthor = creator responseAuthor = su(responseAuthor) responseText = su(response.text) paras = responseText.splitlines() # Indent the response details so they are correctly # interpreted as a literal block after the double colon behind # the 'Response Details' header. This only really matters # when someone interprets this as reStructuredText though. responseDetails = u'\n\n'.join([wrapper.fill(p) for p in paras]) changes = [] for change in response.changes: before = su(change.get('before')) after = su(change.get('after')) name = su(change.get('name')) # Some changes are workflow changes, which can be translated. # Note that workflow changes are in the plone domain. before = translate(before, 'plone', context=self.request) after = translate(after, 'plone', context=self.request) name = translate(name, 'Poi', context=self.request) changes.append(dict(name=name, before=before, after=after)) if response.attachment: attachment_id = response.attachment.getId() else: attachment_id = u'' mapping = dict(issue_title=su(context.title_or_id()), tracker_title=su(tracker.title_or_id()), response_author=responseAuthor, response_details=responseDetails, issue_url=su(context.absolute_url()), changes=changes, from_name=fromName, attachment_id=attachment_id) return mapping
def SearchableText(self): """Include in the SearchableText the text of all responses""" text = BaseObject.SearchableText(self) folder = IResponseContainer(self, None) if folder is None: return text # old style: responses = self.contentValues(filter={'portal_type': 'PoiResponse'}) text += ' ' + ' '.join([r.SearchableText() for r in responses]) # new style: text += ' ' + ' '.join([r.text for r in folder if r]) return text
def migrate_response_attachments_to_blobstorage(context): logger.info('Migrating response attachments to blob storage.') catalog = getToolByName(context, 'portal_catalog') already_migrated = 0 migrated = 0 for brain in catalog.unrestrictedSearchResults(portal_type='PoiIssue'): path = brain.getPath() try: issue = brain.getObject() except (AttributeError, ValueError, TypeError): logger.warn('Error getting object from catalog for path %s', path) continue folder = IResponseContainer(issue) for id, response in enumerate(folder): if response is None: # Has been removed. continue attachment = response.attachment if attachment is None: continue if isinstance(attachment, NamedBlobFile): # Already migrated logger.debug('Response %d already migrated, at %s.', id, path) already_migrated += 1 continue content_type = getattr(attachment, 'content_type', '') filename = getattr(attachment, 'filename', '') if not filename and hasattr(attachment, 'getId'): filename = attachment.getId() data = attachment.data # Data can be 'nested' in OFS.Image.Pdata. if base_hasattr(data, 'data'): data = data.data filename = safe_unicode(filename) try: blob = NamedBlobFile(data, contentType=content_type, filename=filename) except ConstraintNotSatisfied: # Found in live data: a filename that includes a newline... logger.info('Trying to normalize filename %s', filename) filename = normalize_filename(filename, context.REQUEST) logger.info('Normalize to %s', filename) blob = NamedBlobFile(data, contentType=content_type, filename=filename) response.attachment = blob logger.debug('Response %d migrated, at %s.', id, path) migrated += 1 logger.info( 'Migrated %d response attachments to blobs. ' '%d already migrated.', migrated, already_migrated)
def SearchableText(self): """Include in the SearchableText the text of all responses""" text = BaseObject.SearchableText(self) folder = IResponseContainer(self, None) if folder is None: # Should Not Happen (TM) return text try: text += ' ' + ' '.join([r.text for r in folder if r]) except UnicodeDecodeError: text = text.decode('utf-8') + ' ' + ' '.join( [r.text.decode('utf-8') for r in folder if r]) return text
def migrate_workflow_changes(context): """Migrate workflow changes from ids to titles. When a response changes the workflow state of an issue, this change is recorded in that response. This used to be done by storing review state ids. Currently this is done by storing review state titles. Friendlier for the end user and translatable to boot. This migration finds responses with review state ids in them and turns them into titles. """ logger.info("Starting migration of workflow changes.") catalog = getToolByName(context, 'portal_catalog') wftool = getToolByName(context, 'portal_workflow') def get_state_title(state_id): # This neatly returns the input when there is no such review # state id, which happens when the 'state_id' is already a # title. return wftool.getTitleForStateOnType(state_id, 'PoiIssue') issue_brains = catalog.searchResults(portal_type='PoiIssue') logger.info("Found %s PoiIssues.", len(issue_brains)) fixed = 0 for brain in issue_brains: try: issue = brain.getObject() except (AttributeError, KeyError): logger.warn( "AttributeError or KeyError getting tracker object at " "%s", brain.getURL()) continue folder = IResponseContainer(issue) made_changes = False for response in folder: for change in response.changes: # def add_change(self, id, name, before, after): if change['id'] != 'review_state': continue before = get_state_title(change['before']) if change['before'] != before: made_changes = True change['before'] = before change['after'] = get_state_title(change['after']) if made_changes: fixed += 1 if fixed % 100 == 0: logger.info("Committing transaction after fixing " "%s PoiIssues; still busy... " % fixed) transaction.commit() logger.info("Migration completed. %s PoiIssues needed fixing.", fixed)
def replace_old_with_new_responses(issue): if not IIssue.providedBy(issue): return responses = issue.contentValues(filter={'portal_type': 'PoiResponse'}) folder = IResponseContainer(issue) try: request = issue.REQUEST except AttributeError: # When called via prefs_install_products_form (Plone 3.3) we # have no REQUEST object here. We will use a dummy then. request = TestRequest() createview = Create(issue, request) path = '/'.join(issue.getPhysicalPath()) logger.debug("Migrating %s responses for issue at %s", len(responses), path) if not responses: return for old_response in responses: field = old_response.getField('response') text = field.getRaw(old_response) new_response = Response(text) new_response.mimetype = field.getContentType(old_response) new_response.creator = old_response.Creator() new_response.date = old_response.CreationDate() new_response.type = createview.determine_response_type(new_response) changes = old_response.getIssueChanges() for change in changes: new_response.add_change(**change) attachment_field = old_response.getField('attachment') attachment = attachment_field.getRaw(old_response) if attachment.get_size() > 0: new_response.attachment = attachment folder.add(new_response) issue._delObject(old_response.getId()) # This seems a good time to reindex the issue for good measure. issue.reindexObject()
def __call__(self): context = aq_inner(self.context) encoding = self.request.get('encoding') issuefolder = context.restrictedTraverse('@@issuefolder') pas_member = context.restrictedTraverse('@@pas_member') issues = issuefolder.getFilteredIssues(self.request) buffer = BytesIO() writer = csv.writer(buffer) header = [ '#', 'Title', 'Target', 'Area', 'Type', 'Severity', 'Assignee', 'Tags', 'State', 'Last modified by', 'Last modified by date/time', 'Version', 'Submitted by', 'Submitted date/time', ] writer.writerow(header) # to get the previous person who changed something # we need to get workflow and revision history mt = getToolByName(self.context, 'portal_membership') for issue in issues: obj = issue.getObject() responsefolder = IResponseContainer(obj) responses = [] for id, response in enumerate(responsefolder): if response is None: # Has been removed. continue responses.append({ 'creator': response.creator, 'date': response.date }) # the responses are in order so we just grab the last one if len(responses) > 0: last_actor = responses[-1]['creator'] last_modified = responses[-1]['date'] else: last_actor = obj.Creator() last_modified = obj.modified() actor_info = mt.getMemberInfo(last_actor) if actor_info and actor_info.get("fullname", None): last_actor = actor_info["fullname"] row = [] row.append(issue.id) row.append(issue.Title) row.append( issue.target_release and issue.target_release.encode('utf-8') or "") row.append(obj.display_area().encode('utf-8')) row.append(obj.display_issue_type().encode('utf-8')) row.append(issue.severity.encode('utf-8')) row.append(issue.assignee and pas_member.info(issue.assignee)['fullname']) row.append(", ".join( sorted((y for y in issue.Subject), key=lambda x: x.lower()))) row.append(obj.getReviewState()['title'].encode('utf-8')) row.append(last_actor) row.append(last_modified.strftime('%Y-%m-%d %H:%M:%S')) row.append(issue.release and issue.release.encode('utf-8') or "") row.append(pas_member.info(issue.Creator)['name_or_id']) row.append( dateutil.parser.parse( issue.CreationDate).strftime('%Y-%m-%d %H:%M:%S')) writer.writerow(row) value = buffer.getvalue() if not encoding: encoding = 'UTF-8' self.request.response.setHeader('Content-type', 'text/csv;charset=' + encoding) self.request.response.setHeader('Content-Disposition', 'attachment; filename=export.csv') return value
def __init__(self, context, request): self.context = context self.request = request self.folder = IResponseContainer(context) self.mimetype = DEFAULT_ISSUE_MIME_TYPE self.use_wysiwyg = (self.mimetype == 'text/html')
def add_response(self, issue, text, mimetype, attachment): new_response = Response(text) new_response.mimetype = mimetype new_response.attachment = attachment folder = IResponseContainer(issue) folder.add(new_response)