[CMF-checkins] CVS: CMF/CMFCollector - Collector.py:1.1 CollectorIssue.py:1.1 CollectorPermissions.py:1.1 INSTALL.txt:1.1 README.txt:1.1 TODO.txt:1.1 VERSION.txt:1.1 __init__.py:1.1 util.py:1.1

Ken Manheimer klm@zope.com
Wed, 10 Oct 2001 15:14:59 -0400


Update of /cvs-repository/CMF/CMFCollector
In directory cvs.zope.org:/tmp/cvs-serv7316

Added Files:
	Collector.py CollectorIssue.py CollectorPermissions.py 
	INSTALL.txt README.txt TODO.txt VERSION.txt __init__.py 
	util.py 
Log Message:
Initial baseline.

  - Content types: Collector, CollectorIssue, (virtual)
    CollectorIssueTranscript, with python and skins.  (Almost all
    skins methods are ZPT or python scripts).

  - External method install script (Extensions/InstallCollector.py).

  - Install instructions (INSTALL.txt)

  - Pending issues (TODO.txt)



=== Added File CMF/CMFCollector/Collector.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation.  All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
##############################################################################

"""Implement the Collector issue-container content type."""

import os, urllib
from DateTime import DateTime
from Globals import InitializeClass, DTMLFile, package_home

from AccessControl import ClassSecurityInfo, ModuleSecurityInfo
from AccessControl import getSecurityManager

from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
from Products.CMFCore.PortalContent import PortalContent
from Products.CMFCore.WorkflowCore import WorkflowAction

from Products.CMFDefault.SkinnedFolder import SkinnedFolder

# Import permission names
from Products.CMFCore import CMFCorePermissions
from CollectorPermissions import *

from CollectorIssue import addCollectorIssue

# Factory type information -- makes Events objects play nicely
# with the Types Tool (portal_types)
factory_type_information = (
    {'id': 'Collector',
#     'content_icon': 'event_icon.gif',
     'meta_type': 'CMF Collector',
     'description': ('A Collector is a facility for tracking bug reports and'
                     ' other issues.'), 
     'product': 'CMFCollector',
     'factory': 'addCollector',
     'allowed_content_types': ('CollectorIssue',), 
     'immediate_view': 'collector_edit_form',
     'actions': ({'id': 'view',
                  'name': 'Browse',
                  'action': 'collector_contents',
                  'permissions': (ViewCollector,)},
                 {'id': 'addissue',
                  'name': 'New Issue',
                  'action': 'collector_add_issue_form',
                  'permissions': (AddCollectorIssue,)},
                 {'id': 'edit',
                  'name': 'Configure',
                  'action': 'collector_edit_form',
                  'permissions': (ManageCollector,)},
                 ),
     },
    )

_dtmldir = os.path.join(package_home(globals()), 'dtml')
addCollectorForm = DTMLFile('addCollectorForm', _dtmldir, Kind='CMF Collector')

class Collector(SkinnedFolder):
    """Collection of IssueBundles."""

    meta_type = 'CMF Collector'
    effective_date = expiration_date = None
    
    DEFAULT_IMPORTANCES = ['medium', 'high', 'low']
    DEFAULT_SEVERITIES = ['normal', 'critical', 'major', 'minor']
    DEFAULT_CLASSIFICATIONS = ['bug', 'bug+solution', 'feature', 'doc',
                               'test'] 
    DEFAULT_VERSIONS = ['current', 'development', 'old', 'unique']
    DEFAULT_OTHER_VERSIONS_SPIEL = (
        "Pertinent other-system details, eg browser, webserver,"
        " database, python, OS, etc.") 

    security = ClassSecurityInfo()

    def __init__(self, id, title='', description='',
                 topics=None, classifications=None,
                 importances=None, severities=None,
                 supporters=None,
                 versions=None, other_versions_spiel=None):

        SkinnedFolder.__init__(self, id, title)

        self.last_issue_id = 0

        self.description = description

        if supporters is None:
            username = str(getSecurityManager().getUser())
            if username: supporters = [username]
            else: supporters = []
        else:
            self._adjust_supporters_roster(supporters)
        self.supporters = supporters

        if topics is None:
            self.topics = ['Zope', 'Collector', 'Database',
                           'Catalog', 'ZServer']
        else: self.topics = topics

        if classifications is None:
            self.classifications = self.DEFAULT_CLASSIFICATIONS
        else: self.classifications = classifications

        if importances is None:
            self.importances = self.DEFAULT_IMPORTANCES
        else: self.importances = importances

        if severities is None:
            self.severities = self.DEFAULT_SEVERITIES
        else: self.severities = severities

        if versions is None:
            self.versions = self.DEFAULT_VERSIONS
        else: self.versions = versions

        if other_versions_spiel is None:
            self.other_versions_spiel = self.DEFAULT_OTHER_VERSIONS_SPIEL
        else: self.other_versions_spiel = other_versions_spiel

        return self

    security.declareProtected(AddCollectorIssue, 'new_issue_id')
    def new_issue_id(self):
        """Return a new issue id, incrementing the internal counter."""
        lastid = self.last_issue_id = self.last_issue_id + 1
        return str(lastid)

    security.declareProtected(AddCollectorIssue, 'add_issue')
    def add_issue(self,
                  title=None,
                  description=None,
                  submitter=None,
                  email=None,
                  security_related=None,
                  kibitzers=None,
                  topic=None,
                  importance=None,
                  classification=None,
                  severity=None,
                  assigned_to=None,
                  reported_version=None,
                  other_version_info=None):
        """Instigate a new collector issue."""
        id = self.new_issue_id()
        submitter_id = str(getSecurityManager().getUser())
        
        addCollectorIssue(self,
                          id,
                          title=title,
                          description=description,
                          submitter_id=submitter_id,
                          submitter_name=submitter,
                          submitter_email=email,
                          kibitzers=kibitzers,
                          topic=topic,
                          classification=classification,
                          security_related=security_related,
                          importance=importance,
                          severity=severity,
                          assigned_to=assigned_to,
                          reported_version=reported_version,
                          other_version_info=other_version_info)
        return id


    security.declareProtected(ManageCollector, 'edit')
    def edit(self, title=None, description=None, supporters=None,
             topics=None, classifications=None,
             importances=None, severities=None,
             versions=None, other_versions_spiel=None):
        changed = 0
        if title is not None:
            if title != self.title:
                self.title = title
                changed = 1
        if description is not None:
            if self.description != description:
                self.description = description
                changed = 1
        if supporters is not None:
            # XXX Vette supporters - they must exist, etc.
            x = filter(None, supporters)
            if self.supporters != x:
                self._adjust_supporters_roster(x)
                self.supporters = x
                changed = 1
        if topics is not None:
            x = filter(None, topics)
            if self.topics != x:
                self.topics = x
                changed = 1
        if classifications is not None:
            x = filter(None, classifications)
            if self.classifications != x:
                self.classifications = x
                changed = 1
        if importances is not None:
            x = filter(None, importances)
            if self.importances != x:
                self.importances = x
                changed = 1
        if versions is not None:
            x = filter(None, versions)
            if self.versions != x:
                self.versions = x
                changed = 1

        if versions is not None:
            x = filter(None, versions)
            if self.versions != x:
                self.versions = x
                changed = 1
        if other_versions_spiel is not None:
            if self.other_versions_spiel != other_versions_spiel:
                self.other_versions_spiel = other_versions_spiel
                changed = 1
        return changed

    def _adjust_supporters_roster(self, new_roster):
        """Adjust supporters local-role assignments to track roster changes.
        Ie, ensure all and only designated supporters have 'Reviewer' local
        role."""

        already = []
        # Remove 'Reviewer' local role from anyone having it not on new_roster:
        for u in self.users_with_local_role('Reviewer'):
            if u in new_roster:
                already.append(u)
            else:
                # Remove the 'Reviewer' local role:
                roles = list(self.get_local_roles_for_userid(u))
                roles.remove('Reviewer')
                if roles:
                    self.manage_setLocalRoles(u, roles)
                else:
                    self.manage_delLocalRoles([u])
        # Add 'Reviewer' local role to anyone on new_roster that lacks it:
        for u in new_roster:
            if u not in already:
                roles = list(self.get_local_roles_for_userid(u))
                roles.append('Reviewer')
                self.manage_setLocalRoles(u, roles)
        
    security.declareProtected(CMFCorePermissions.View, 'length')
    def length(self):
        """Use length protocol."""
        return self.__len__()
        
    def __len__(self):
        """Implement length protocol method."""
        return len(self.objectIds())

    def __repr__(self):
        return ("<%s %s (%d issues) at 0x%s>"
                % (self.__class__.__name__, `self.id`, len(self),
                   hex(id(self))[2:]))

InitializeClass(Collector)
    
# XXX Enable use of pdb.set_trace() in python scripts
ModuleSecurityInfo('pdb').declarePublic('set_trace')

def addCollector(self, id, title=None, description=None,
                 topics=None, classifications=None,
                 importances=None, severities=None,
                 supporters=None,
                 versions=None, other_versions_spiel=None,
                 REQUEST=None):
    """
    Create a collector.
    """
    it = Collector(id, title=title, description=description,
                   topics=topics, classifications=classifications,
                   supporters=supporters, 
                   versions=versions,
                   other_versions_spiel=other_versions_spiel)
    self._setObject(id, it)
    it = self._getOb(id)
    it._setPortalTypeName('Collector')

    it.manage_permission(ManageCollector, roles=['Owner'], acquire=1)
    it.manage_permission(EditCollectorIssue,
                         roles=['Reviewer'],
                         acquire=1)
    it.manage_permission(AddCollectorIssueComment,
                         roles=['Reviewer', 'Owner'],
                         acquire=1)
    it.manage_permission(AddCollectorIssueArtifact,
                         roles=['Reviewer', 'Owner'],
                         acquire=1)
    if REQUEST is not None:
        try:    url=self.DestinationURL()
        except: url=REQUEST['URL1']
        REQUEST.RESPONSE.redirect('%s/manage_main' % url)
    return id
        


=== Added File CMF/CMFCollector/CollectorIssue.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation.  All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
##############################################################################

"""Implement the Collector Issue content type - a bundle containing the
collector transcript and various parts."""

import os, urllib, string, re
from DateTime import DateTime
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo, getSecurityManager
from Acquisition import aq_base

import util                             # Collector utilities.

from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
from Products.CMFCore.PortalContent import PortalContent
from Products.CMFCore.WorkflowCore import WorkflowAction
from Products.CMFCore.utils import getToolByName

from Products.CMFDefault.SkinnedFolder import SkinnedFolder
from Products.CMFDefault.Document import addDocument

# Import permission names
from Products.CMFCore import CMFCorePermissions
from CollectorPermissions import *

DEFAULT_TRANSCRIPT_FORMAT = 'stx'

factory_type_information = (
    {'id': 'Collector Issue',
#XXX     'content_icon': 'event_icon.gif',
     'meta_type': 'CMF Collector Issue',
     'description': ('A Collector Issue represents a bug report or'
                     ' other support request.'),
     'product': 'CMFCollector',
     'factory': None,                   # So not included in 'New' add form
     'allowed_content_types': ('Collector Issue Transcript', 'File', 'Image'), 
     'immediate_view': 'collector_edit_form',
     'actions': ({'id': 'view',
                  'name': 'Transcript',
                  'action': 'collector_issue_contents',
                  'permissions': (ViewCollector,)},
                 {'id': 'followup',
                  'name': 'Followup',
                  'action': 'collector_issue_followup_form',
                  'permissions': (AddCollectorIssueComment,)},
                 {'id': 'artifacts',
                  'name': 'Add Artifacts',
                  'action': 'collector_issue_add_artifact_form',
                  'permissions': (AddCollectorIssueArtifact,)},
                 {'id': 'edit',
                  'name': 'Edit Issue',
                  'action': 'collector_issue_edit_form',
                  'permissions': (EditCollectorIssue,)},
                 {'id': 'browse',
                  'name': 'Browse Collector',
                  'action': 'collector_issue_up',
                  'permissions': (ViewCollector,)},
                 {'id': 'addIssue',
                  'name': 'New Issue',
                  'action': 'collector_issue_add_issue',
                  'permissions': (ViewCollector,)},
                 ),
     },
    )

TRANSCRIPT_NAME = "ISSUE_TRANSCRIPT"

class CollectorIssue(SkinnedFolder, DefaultDublinCoreImpl):
    """An individual support request in the CMF Collector."""

    meta_type = 'CMF Collector Issue'
    effective_date = expiration_date = None
    
    security = ClassSecurityInfo()

    comment_delimiter = "<hr solid id=comment_delim>"

    comment_number = 0

    def __init__(self, 
                 id, container,
                 title='', description='',
                 submitter_id=None, submitter_name=None, submitter_email=None,
                 kibitzers=None,
                 topic=None, classification=None,
                 security_related=0,
                 importance=None, severity=None,
                 assigned_to=None, current_status='pending',
                 resolution=None,
                 reported_version=None, other_version_info=None,
                 creation_date=None, modification_date=None,
                 effective_date=None, expiration_date=None):
        """ """

        SkinnedFolder.__init__(self, id, title)
        # Take care of standard metadata:
        DefaultDublinCoreImpl.__init__(self,
                                       title=title, description=description,
                                       effective_date=effective_date,
                                       expiration_date=expiration_date)
        if modification_date is None:
            modification_date = self.creation_date
        self.modification_date = modification_date

        self._create_transcript(description, container)

        user = getSecurityManager().getUser()
        if submitter_id is None:
            self.submitter_id = str(user)
        self.submitter_id = submitter_id
        if submitter_name is None:
            if hasattr(user, 'full_name'):
                submitter_name = user.full_name
        elif (submitter_name
              and (getattr(user, 'full_name', None) != submitter_name)):
            # XXX We're being cavalier about stashing the full_name.
            user.full_name = submitter_name
        self.submitter_name = submitter_name
        if submitter_email is None and hasattr(user, 'email'):
            submitter_email = user.email
        self.submitter_email = submitter_email

        if kibitzers is None:
            kibitzers = ()
        self.kibitzers = kibitzers

        self.topic = topic
        self.classification = classification
        self.security_related = security_related
        self.importance = importance
        self.severity = severity
        self.assigned_to = assigned_to
        self.current_status = current_status
        self.resolution = resolution
        self.reported_version = reported_version
        self.other_version_info = other_version_info

        self.edited = 0

        return self

    security.declareProtected(EditCollectorIssue, 'edit')
    def edit(self, comment=None,
             text=None,
             status=None,
             submitter_name=None,
             title=None,
             description=None,
             security_related=None,
             topic=None,
             importance=None,
             classification=None,
             severity=None,
             reported_version=None,
             other_version_info=None):
        """Update the explicitly passed fields."""
        if text is not None:
            transcript = self.get_transcript()
            transcript._edit(text_format=DEFAULT_TRANSCRIPT_FORMAT,
                             text=text)
        if comment is not None:
            self.do_action('edit', comment)
        if submitter_name is not None:
            self.submitter_name = submitter_name
        if title is not None:
            self.title = title
        if description is not None:
            self.description = description
        if security_related is not None:
            self.security_related = security_related
        if topic is not None:
            self.topic = topic
        if importance is not None:
            self.importance = importance
        if classification is not None:
            self.classification = classification
        if severity is not None:
            self.severity = severity
        if reported_version is not None:
            self.reported_version = reported_version
        if other_version_info is not None:
            self.other_version_info = other_version_info
        self.edited = 1

    security.declareProtected(CMFCorePermissions.View, 'get_transcript')
    def get_transcript(self):
        return self._getOb(TRANSCRIPT_NAME)

    security.declareProtected(AddCollectorIssueComment, 'do_action')
    def do_action(self, action, comment, attachments=None):
        """Execute an action, adding comment to the transcript."""
        transcript = self.get_transcript()
        self.comment_number = self.comment_number + 1
        entry_leader = "\n\n" + self._entry_header(action) + "\n\n"
        transcript._edit('stx',
                         transcript.EditableBody()
                         + entry_leader
                         + util.process_comment(comment))

    security.declareProtected(AddCollectorIssueArtifact, 'add_artifact')
    def add_artifact(self, id, type, description, file):
        """Add new artifact, and note in transcript."""
        self.invokeFactory(type, id)
        it = self._getOb(id)
        it.description = description
        it.manage_upload(file)
        transcript = self.get_transcript()
        entry_leader = ("\n\n"
                        + self._entry_header("New Artifact '%s'" % id)
                        + "\n\n")
        transcript._edit('stx',
                         transcript.EditableBody()
                         + entry_leader
                         + util.process_comment(description))

    def _create_transcript(self, description, container,
                           text_format=DEFAULT_TRANSCRIPT_FORMAT):
        """Create events and comments transcript, with initial entry."""

        addDocument(self, TRANSCRIPT_NAME, description=description)
        it = self.get_transcript()
        it._setPortalTypeName('Collector Issue Transcript')
        text = "%s\n\n %s " % (self._entry_header('Request', prefix="== "),
                               description)
        it._edit(text_format=text_format, text=text)
        it.title = self.title

    def _entry_header(self, type, prefix="<hr> == ", suffix=" =="):
        """Return text for the header of a new transcript entry."""
        # Ideally this would be a skin method (probly python script), but i
        # don't know how to call it from the product, sigh.
        t = string.capitalize(type)
        if self.comment_number:
            lead = t + " - Entry #" + str(self.comment_number)
        else:
            lead = t

        user = getSecurityManager().getUser()
        return ("%s%s by %s on %s%s" %
                (prefix, lead, str(user), DateTime().aCommon(), suffix))

    security.declareProtected(CMFCorePermissions.View, 'cited_text')
    def cited_text(self):
        """Quote text for use in literal citations."""
        return util.cited_text(self.get_transcript().text)

    #################################################
    # Dublin Core and search provisions

    # The transcript indexes itself, we just need to index the salient
    # attribute-style issue data/metadata...

    security.declareProtected(CMFCorePermissions.ModifyPortalContent,
                              'indexObject')
    def indexObject(self):
        catalog = getToolByName(self, 'portal_catalog', None)
        if catalog is not None:
            catalog.indexObject(self)

    security.declareProtected(CMFCorePermissions.ModifyPortalContent,
                              'unindexObject')
    def unindexObject(self):
        catalog = getToolByName(self, 'portal_catalog', None)
        if catalog is not None:
            catalog.unindexObject(self)

    security.declareProtected(CMFCorePermissions.ModifyPortalContent,
                              'reindexObject')
    def reindexObject(self):
        catalog = getToolByName(self, 'portal_catalog', None)
        if catalog is not None:
            catalog.reindexObject(self)

    def manage_afterAdd(self, item, container):
        """Add self to the workflow and catalog."""
        # Are we being added (or moved)?
        if aq_base(container) is not aq_base(self):
            wf = getToolByName(self, 'portal_workflow', None)
            if wf is not None:
                wf.notifyCreated(self)
            self.indexObject()

    def manage_beforeDelete(self, item, container):
        """Remove self from the catalog."""
        # Are we going away?
        if aq_base(container) is not aq_base(self):
            self.unindexObject()
            # Now let our "aspects" know we are going away.
            for it, subitem in self.objectItems():
                si_m_bD = getattr(subitem, 'manage_beforeDelete', None)
                if si_m_bD is not None:
                    si_m_bD(item, container)

    def SearchableText(self):
        """Consolidate all text and structured fields for catalog search."""
        # Make this a composite of the text and structured fields.
        return (self.title + ' '
                + self.description + ' '
                + self.topic + ' '
                + self.classification + ' '
                + self.importance + ' '
                + self.severity + ' '
                + self.current_status + ' '
                + self.resolution + ' '
                + self.reported_version + ' '
                + self.other_version_info + ' '
                + ((self.security_related and 'security_related') or ''))

    def Subject(self):
        """The structured attrs, combined w/field names for targeted search."""
        return ('topic:' + self.topic,
                'classification:' + self.classification,
                'security_related:' + ((self.security_related and '1') or '0'),
                'importance:' + self.importance,
                'severity:' + self.severity,
                'assigned_to:' + (self.assigned_to or ''),
                'current_status:' + (self.current_status or ''),
                'resolution:' + (self.resolution or ''),
                'reported_version:' + self.reported_version)

    def __repr__(self):
        return ("<%s %s \"%s\" at 0x%s>"
                % (self.__class__.__name__,
                   self.id, self.title,
                   hex(id(self))[2:]))

InitializeClass(CollectorIssue)
    

def addCollectorIssue(self,
                      id,
                      title='',
                      description='',
                      submitter_id=None,
                      submitter_name=None,
                      submitter_email=None,
                      kibitzers=None,
                      topic=None,
                      classification=None,
                      security_related=0,
                      importance=None,
                      severity=None,
                      assigned_to=None,
                      reported_version=None,
                      other_version_info=None,
                      REQUEST=None):
    """
    Create a new issue in the collector.
    """

    it = CollectorIssue(id=id,
                        container=self,
                        title=title,
                        description=description,
                        submitter_id=submitter_id,
                        submitter_name=submitter_name,
                        submitter_email=submitter_email,
                        kibitzers=kibitzers,
                        topic=topic,
                        classification=classification,
                        security_related=security_related,
                        importance=importance,
                        severity=severity,
                        assigned_to=assigned_to,
                        reported_version=reported_version,
                        other_version_info=other_version_info)
    it._setPortalTypeName('Collector Issue')
    self._setObject(id, it)
    return id


=== Added File CMF/CMFCollector/CollectorPermissions.py ===
from Products.CMFCore import CMFCorePermissions
from Products.CMFCore.CMFCorePermissions import setDefaultRoles

# Gathering Event Related Permissions into one place
ViewCollector = CMFCorePermissions.View
AddCollector = 'Add portal collector'
ManageCollector = 'Add portal collector'
AddCollectorIssue = 'Add collector issue'
AddCollectorIssueComment = 'Add collector issue comment'
AddCollectorIssueArtifact = 'Add collector issue artifact'
EditCollectorIssue = 'Edit collector issue'
SupportIssue = 'Support collector issue'

# Set up default roles for permissions
setDefaultRoles(AddCollector, CMFCorePermissions.AddPortalContent)
setDefaultRoles(ManageCollector,
                ('Manager', 'Owner'))
setDefaultRoles(AddCollectorIssue,
                ('Anonymous', 'Manager', 'Reviewer', 'Owner'))
setDefaultRoles(AddCollectorIssueComment,
                ('Manager', 'Reviewer', 'Owner'))
setDefaultRoles(AddCollectorIssueArtifact,
                ('Manager', 'Reviewer', 'Owner'))
setDefaultRoles(EditCollectorIssue,
                ('Manager', 'Reviewer'))
setDefaultRoles(SupportIssue,
                ('Manager', 'Reviewer'))


=== Added File CMF/CMFCollector/INSTALL.txt ===
Installing CMFCollector

  The CMFCollector is an issue collector for Zope.

  Prerequisites:

   - Zope, 2.4 or better (with page templates, Python 2.x)

   - The CMF ( http://cmf.zope.org ), recent (as of 10/10/2001) CVS
     checkout, or (not yet released) 1.2 or better.

   - Zope Page templates (ZPT, http://www.zope.org/Wikis/DevSite/Projects/ZPT )

   - CMFDecor provisions for ZPT, including use of the ZPT skin as the
     default skin (settable from the portal_skins 'Properties' tab).

  To install CMFCollector, uncompress the CMFCollector product into
  your zope/Products directory or link it there, e.g.::

    ln -s /path/to/installation /path/to/zope/Products

  In the root of your CMFSite installation (within the ZMI):

      1.  Add an external method to the root of the CMF Site.

      2.  Use the following configuration values for the external
          method:

          o id: install_collector

          o title (optional): Install Collector Content Types

          o module name: CMFCollector.InstallCollector

          o function name: install_collector

      3. Go to the management screen for the newly added external
         method and click the 'Try it' tab.  

  The install function will execute and give information about the
  steps it took to register and install the CMF Events into the CMF
  Site instance.


=== Added File CMF/CMFCollector/README.txt ===
CMFCollector README

  The CMFCollector is starting out as a rudimentary issue tracker, to
  replace the equally rudimentary, ancient collector we've been using
  on zope.org for a long time.  It is being implemented as CMF content
  to enable evolution to a more comprehensive solution with time.

  See INSTALL.txt for instructions about installing in your CMF site.


=== Added File CMF/CMFCollector/TODO.txt ===
To-do:

 o 10/10/2001 klm: Basic content types implemented.  Working on
   workflow (prototyping with through-the-web DCWorkflow, but not yet
   packaging that), searching, email, and assignment, but checking in
   the base pieces.

 o 09/24/2001 klm: Implementation.  (Designs at
   http://dev.zope.org/Wikis/DevSite/Projects/CollectorReplacement/FrontPage .)


=== Added File CMF/CMFCollector/VERSION.txt ===
CMFCollector Product 0.1


=== Added File CMF/CMFCollector/__init__.py ===
# Copyright (c) 2001 Zope Corporation.  All Rights Reserved.

# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.

from Products.CMFDefault import Portal
import Collector, CollectorIssue
import Products.CMFCore

from Products.CMFCore import utils, CMFCorePermissions
from Products.CMFCore.DirectoryView import registerDirectory
import CollectorPermissions

import sys
this_module = sys.modules[ __name__ ]

factory_type_information = (
    (Collector.factory_type_information
     + CollectorIssue.factory_type_information

     + ({'id': 'Collector Issue Transcript',
         #     'content_icon': 'event_icon.gif',
         'meta_type': 'Document',
         'description': ('A transcript of issue activity, including comments,'
                         ' state changes, and so forth.'), 
         'product': 'CMFDefault',
         'factory': None,               # So not included in 'New' add form
         'allowed_content_types': None,
         'immediate_view': 'collector_transcript_view',
         'actions': ({'id': 'view',
                      'name': 'View',
                      'action': '../',
                      'permissions': (CMFCorePermissions.View,)},
                     {'id': 'addcomment',
                      'name': 'Add Comment',
                      'action': 'collector_transcript_comment_form',
                      'permissions':
                      (CollectorPermissions.AddCollectorIssueComment,)},
                     {'id': 'edittranscript',
                      'name': 'Edit Transcript',
                      'action': 'collector_transcript_edit_form',
                      'permissions':
                      (CollectorPermissions.EditCollectorIssue,)},
                     ),
         },
        )
     )
    )

contentClasses = (Collector.Collector, CollectorIssue.CollectorIssue)
contentConstructors = (Collector.addCollector,
                       CollectorIssue.addCollectorIssue)
z_bases = utils.initializeBasesPhase1(contentClasses, this_module)
# This is used by a script (external method) that can be run
# to set up collector in an existing CMF Site instance.
collector_globals = globals()

# Make the skins available as DirectoryViews
registerDirectory('skins', globals())
registerDirectory('skins/collector', globals())

def initialize(context):
    utils.initializeBasesPhase2(z_bases, context)
    context.registerHelp(directory='help')
    context.registerHelpTitle('CMF Collector Help')

    context.registerClass(Collector.Collector,
                          constructors = (Collector.addCollector,),
                          permission = CMFCorePermissions.AddPortalContent)

    context.registerClass(CollectorIssue.CollectorIssue,
                          constructors = (CollectorIssue.addCollectorIssue,),
                          permission = CollectorPermissions.AddCollectorIssue)


=== Added File CMF/CMFCollector/util.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation.  All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
##############################################################################

"""Sundry collector utilities."""

import string, re

preexp = re.compile(r'<pre>')
unpreexp = re.compile(r'</pre>')
citedexp = re.compile(r'^\s*>')
# Match group 1 is citation prefix, group 2 is leading whitespace:
cite_prefixexp = re.compile('([\s>]*>)?([\s]*)')

def cited_text(text, rfind=string.rfind, strip=string.strip):
    """Quote text for use in literal citations.

    We prepend '>' to each line, splitting long lines (propagating
    existing citation and leading whitespace) when necessary."""

    got = []
    for line in string.split(text, '\n'):
        pref = '> '
        if len(line) < 79:
            got.append(pref + line)
            continue
        m = cite_prefixexp.match(line)
        if m is None:
            pref = '> %s'
        else:
            if m.group(1):
                pref = pref + m.group(1)
                line = line[m.end(1)+1:]
                if m.end(1) > 60:
                    # Too deep quoting - collapse it:
                    pref = '> >> '
                    lencut = 0
            pref = pref + '%s'
            leading_space = m.group(2)
            if leading_space:
                pref = pref + leading_space
                line = line[len(leading_space):]
        lenpref = len(pref)
        continuation_padding = ''
        lastcurlen = 0
        while 1:
            curlen = len(line) + lenpref
            if curlen < 79 or (lastcurlen and lastcurlen <= curlen):
                # Small enough - we're done - or not shrinking - bail out
                if line: got.append((pref % continuation_padding) + line)
                break
            else:
                lastcurlen = curlen
            splitpoint = max(rfind(line[:78-lenpref], ' '),
                             rfind(line[:78-lenpref], '\t'))
            if not splitpoint or splitpoint == -1:
                if strip(line):
                    got.append((pref % continuation_padding) +
                               line)
                line = ''
            else:
                if strip(line[:splitpoint]):
                    got.append((pref % continuation_padding) +
                               line[:splitpoint])
                line = line[splitpoint+1:]
            if not continuation_padding:
                # Continuation lines are indented more than intial - just
                # enough to line up past, eg, simple bullets.
                continuation_padding = '  '
    return string.join(got, '\n')

def process_comment(comment, strip=string.strip):
    """Return formatted comment, escaping cited text."""
    # Process the comment:
    # - Strip leading whitespace,
    # - indent every line so it's contained as part of the prefix
    #   definition list, and
    # - cause all cited text to be preformatted.

    inpre = incited = atcited = 0
    presearch = preexp.search
    presplit = preexp.split
    unpresearch = unpreexp.search
    unpresplit = unpreexp.split
    citedsearch = citedexp.search
    got = []
    for i in string.split(string.strip(comment), '\n') + ['']:
        atcited = citedsearch(i)
        if not atcited:
            if incited:
                # Departing cited section.
                incited = 0
                if inpre:
                    # Close <pre> that we prepended.
                    got.append(' </pre>')
                    inpre = 0

            # Check line for toggling of inpre.
            # XXX We don't deal well with way imbalanced pres on a
            # single line.  Feh, we're working too hard, already.
            if not inpre:
                x = presplit(i)
                if len(x) > 1 and not unprexpsearch(x[-1]):
                    # The line has a <pre> without subsequent </pre>
                    inpre = 1
            else:                   # in <pre>
                x = unpresplit(i)
                if len(x) > 1 and not prexpsearch(x[-1]):
                    # The line has a </pre> without subsequent <pre>
                    inpre = 0

        else:
            # Quote the minimal set of chars, to reduce raw text
            # ugliness. Do the '&' *before* any others that include '&'s!
            if '&' in i and ';' in i: i = string.replace(i, '&', '&amp;')
            if '<' in i: i = string.replace(i, '<', '&lt;')
            if not incited:
                incited = 1
                if not inpre:
                    got.append(' <pre>')
                    inpre = 1
        got.append(' ' + i)
    return string.join(got, '\n')