예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
    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')
예제 #4
0
    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"))
예제 #5
0
    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
예제 #6
0
파일: ptc.py 프로젝트: veit/Products.Poi
    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
예제 #7
0
 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')
예제 #8
0
    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
예제 #9
0
    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
예제 #10
0
    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
예제 #11
0
 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
예제 #12
0
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)
예제 #13
0
 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
예제 #14
0
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)
예제 #15
0
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()
예제 #16
0
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()
예제 #17
0
    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
예제 #18
0
 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')
예제 #19
0
 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)