######################################################################
#
# JTrackerIssue	A support issue
#
# This software is governed by a license. See 
# LICENSE.txt for the terms of this license.
#
######################################################################
__version__='$Revision: 1.29 $'[11:-2]

# General Python imports
import os, string, random

# Zope imports
from Acquisition import aq_inner, aq_parent, aq_base
from AccessControl.Permissions import access_contents_information, view
from OFS.SimpleItem import SimpleItem
from OFS.Image import File
from OFS.PropertyManager import PropertyManager
from BTrees.OOBTree import OOBTree
from DateTime import DateTime
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass, package_home, PersistentMapping
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from AccessControl.SecurityManagement import getSecurityManager

# JTracker package imports
from Permissions import ManageJTracker, SubmitJTrackerIssues
from Permissions import SupportJTrackerIssues
from utils import REPLY_TEMPLATE

_wwwdir = os.path.join(package_home(globals()), 'www')
addJTrackerIssueForm = PageTemplateFile('addJTrackerIssue.pt', _wwwdir)


def _generateIssueId(container):
    """ Generate an ID for JTracker issues """
    if hasattr(container, '_count'):
        # This is a BTreeFolder
        ob_count = container._count()
    else:
        # Dealing with a normal folder
        ob_count = len(container._objects)

    if ob_count == 0:
        return 'issue_00001'

    # Starting point is the highest ID possible given the
    # number of objects in the JTracker
    good_id = None
    found_ob = None

    while good_id is None:
        try_id = 'issue_%s' % string.zfill(str(ob_count), 5)

        if ( container._getOb(try_id, None) is None and
             container._getOb('issue_%i' % ob_count, None) is None ):

            if found_ob is not None:
                # First non-used ID after hitting an object, this is the one
                good_id = try_id
            else:
                if try_id == 'issue_00001':
                    # Need to special-case the first ID
                    good_id = try_id
                else:
                    # Go down
                    ob_count -= 1
        else:
            # Go up
            found_ob = 1
            ob_count += 1

    return good_id

def _encodeSecret():
    """ Generate a secret token """
    token = ''
    pool = '%s%s%s' % ( string.uppercase[:26]
                      , string.digits
                      , string.lowercase[:26]
                      )

    for i in range(5):
        token = '%s%s' % (token, random.choice(pool))

    return token


class JTrackerIssue(PropertyManager, SimpleItem):
    """ A supprt issue """
    security = ClassSecurityInfo()
    meta_type = 'JTracker Issue'

    _views = { 'index_html' :   { 'view_label' : 'Default View'
                                , 'view_id'    : 'index_html'
                                }
             , 'addReplyForm' : { 'view_label' : 'Add Reply Form'
                                , 'view_id'    : 'addReplyForm'
                                }
             }

    _properties = ( { 'id'   : 'title'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'description'
                    , 'type' : 'text'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'component'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'request_type'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'requester_name'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'requester_email'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'request_date'
                    , 'type' : 'date'
                    , 'mode' : 'r'
                    }
                  , { 'id'   : 'stage'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'supporter'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'subscribers'
                    , 'type' : 'lines'
                    , 'mode' : 'w'
                    }
                  )

    security.declareProtected(ManageJTracker, 'manage_replies')
    manage_replies = PageTemplateFile('editJTrackerIssueReplies.pt', _wwwdir)

    index_html = PageTemplateFile('viewJTrackerIssue.pt', _wwwdir)

    security.declareProtected(SubmitJTrackerIssues, 'addReplyForm')
    addReplyForm = PageTemplateFile('addJTrackerIssueReplyForm.pt', _wwwdir)

    manage_main = PropertyManager.manage_propertiesForm
    manage_main._setName('manage_main')

    manage_options = ( PropertyManager.manage_options
                     + ( {'label' : 'Replies', 'action' : 'manage_replies'}
                       , {'label' : 'View', 'action' : ''}
                       )
                     + SimpleItem.manage_options
                     )


    def __init__( self
                , id
                , title=''
                , description=''
                , component=''
                , request_type=''
                , requester_name=''
                , requester_email=''
                , subscribers=()
                ):
        """ Instantiate a new JTrackerIssue object """
        self.id = id
        self.title = title
        self._replies = OOBTree()
        self._files = OOBTree()
        self.description = description
        self.component = component
        self.request_type = request_type
        self.requester_email = requester_email
        self.request_date = DateTime()
        self.stage = 'Pending'
        self.supporter = ''
        self.subscribers = list(subscribers)
        self._secret = _encodeSecret()
        
        user = getSecurityManager().getUser()
        anon_user = 'Anonymous User'
        if ( ( user.getUserName() == anon_user and
               requester_name == anon_user ) or
             not requester_name ):
            self.requester_name = 'Anonymous Coward'
        else:
            self.requester_name = requester_name


    security.declarePrivate('indexObject')
    def indexObject(self):
        """ Handle indexing """
        cat = getattr(self, 'catalog')
        url = '/'.join(self.getPhysicalPath())
        cat.catalog_object(self, url)


    security.declarePrivate('unindexObject')
    def unindexObject(self):
        """ Handle unindexing """
        cat = getattr(self, 'catalog')
        url = '/'.join(self.getPhysicalPath())
        cat.uncatalog_object(url)


    security.declareProtected(SubmitJTrackerIssues, 'manage_afterAdd')
    def manage_afterAdd(self, item, container):
        """ What to do after an issue has been added """
        jt = self.getTracker()
        jt_email = jt.getProperty('admin_email')
        jt_name = jt.getProperty('admin_name')
        
        if jt_email:
            jtadmin_email = '"%s" <%s>' % (jt_name, jt_email)
            subscribers = list(self.getProperty('subscribers', []))

            if jtadmin_email not in subscribers:
                subscribers.append(jtadmin_email)
                self._updateProperty('subscribers', filter(None, subscribers))

        self.indexObject()


    security.declareProtected(ManageJTracker, 'manage_beforeDelete')
    def manage_beforeDelete(self, item, container):
        """ Cleanup before an issue gets deleted """
        self.unindexObject()


    security.declarePublic('SearchableText')
    def SearchableText(self):
        """ Catalog text search helper """
        s_text = '%s %s' % (self.title, self.description)

        for reply in self.getReplies():
            s_text = '%s %s' % (s_text, reply.get('description', ''))

        return s_text


    security.declarePublic('getTracker')
    def getTracker(self):
        """ return the tracker this issue is situated in """
        return aq_parent(aq_inner(self))


    security.declarePublic('date')
    def date(self):
        """ Catalog helper to return request date representation """
        return self.request_date.Date()


    security.declareProtected(ManageJTracker, 'manage_editProperties')
    def manage_editProperties(self, REQUEST):
        """ Overridden to make sure recataloging is done """
        for prop in self._propertyMap():
            name=prop['id']
            if 'w' in prop.get('mode', 'wd'):
                value=REQUEST.get(name, '')
                self._updateProperty(name, value)

        self.indexObject()

        msg = 'Saved changes.'
        return self.manage_propertiesForm(manage_tabs_message=msg)


    security.declarePrivate('_generateReplyId')
    def _generateReplyId(self):
        """ Generate a suitable reply_id """
        num_replies = self.followups()

        return 'entry_%i' % (num_replies + 1)


    security.declarePrivate('_storeFile')
    def _storeFile(self, reply_id, file, respect_limit=1):
        """ Store a file """
        if respect_limit:
            tracker = self.getTracker()
            filesize_limit = int(tracker.getProperty('filesize_limit'))

            if filesize_limit == 0:
                del file
                return None

            file.seek(0,2)
            kb_size = file.tell()/1024

            if kb_size > filesize_limit:
                del file
                return None
        
        fn = file.filename
        if fn.find('\\') != -1: # Windoze browsers are dumb, they send full paths
            path_comps = fn.split('\\')
            fn = path_comps[-1]

        file_ob = File(fn, fn, file)

        self._files[fn] = { 'reply_id' : reply_id
                          , 'file_id'  : fn
                          , 'file'     : file_ob
                          }

        return fn


    security.declareProtected(SubmitJTrackerIssues, 'getFile')
    def getFile(self, file_id):
        """ Return file in the right context """
        file_info = self._files.get(file_id, None)

        if file_info is not None:
            file = file_info['file'].__of__(self)
        else:
            file = None

        return file


    security.declareProtected(SubmitJTrackerIssues, 'addReply')
    def addReply( self
                , reply_type=''
                , description=''
                , requester_name=''
                , requester_email=''
                , file=None
                , send_email=1
                , request_date=None
                , unrestricted=0
                , REQUEST=None
                ):
        """ Add a followup/reply to a JTracker issue """
        error = ''
        missing_input = []

        if request_date is None:
            request_date = DateTime()

        for key, val in { reply_type : 'Reply Type'
                        , description : 'Reply body'
                        , requester_email : 'Email address'
                        }.items():
            if not key:
                missing_input.append(val)

        user = getSecurityManager().getUser()
        anon_user = 'Anonymous User'
        if ( ( user.getUserName() == anon_user and
               requester_name == anon_user ) or
             not requester_name ):
            requester_name = 'Anonymous Coward'

        if missing_input:
            error = 'Missing input: %s' % ', '.join(missing_input)

        if requester_email.find('@') == -1 or requester_email.find('.') == -1:
            error = 'Invalid email address'

        if unrestricted == 0 and reply_type not in self.getReplyTypes():
            error = 'You are not authorized to %s this issue' % reply_type

        reply_id = self._generateReplyId()

        if file is not None and getattr(file, 'filename', '') != '':
            upload_result = self._storeFile(reply_id, file)

            if upload_result is None:
                jtracker = self.getTracker()
                limit = int(jtracker.getProperty('filesize_limit'))
                error = 'File size too large, limit is %i KB' % limit
        else:
            upload_result = None

        if error:
            if REQUEST is not None:
                form = self.__bobo_traverse__(REQUEST, 'addReplyForm')
                return form(error_msg=error)
            else:
                raise ValueError, error

        new_reply = { 'reply_id' : reply_id
                    , 'reply_type' : reply_type
                    , 'description' : description
                    , 'requester_name' : requester_name
                    , 'requester_email' : requester_email
                    , 'reply_date' : request_date
                    , 'file_name' : upload_result
                    , 'visible' : 1
                    }
        self._replies[reply_id] = new_reply

        if reply_type == 'Initial Request':
            self.stage = 'Pending'
        elif reply_type == 'Accept':
            self.stage = 'Accepted'
        elif reply_type == 'Resolve':
            self.stage = 'Resolved'
        elif reply_type == 'Reject':
            self.stage = 'Rejected'
        elif reply_type == 'Defer':
            self.stage = 'Deferred'

        if not self._is_subscribed(requester_email):
            req_addr = '"%s" <%s>' % (requester_name, requester_email)
            subscribers = list(self.getProperty('subscribers', []))
            subscribers.append(req_addr)
            self._updateProperty('subscribers', filter(None, subscribers))

        self.indexObject()

        if send_email:
            jtracker = self.getTracker()
            jtracker_url = jtracker.absolute_url()

            msg = REPLY_TEMPLATE % { 'requester_name' : requester_name
                                   , 'requester_email' : requester_email
                                   , 'component' : self.component
                                   , 'request_type' : reply_type
                                   , 'request_url' : self.absolute_url()
                                   , 'title' : self.title
                                   , 'description' : description
                                   , 'jtracker_title' : jtracker.title
                                   , 'jtracker_url' : jtracker_url
                                   }
            subject = '%s followup: "%s" (%s)' % ( self.component
                                                 , self.title
                                                 , self.getId()
                                                 )

            subscribers = filter(None, self.getProperty('subscribers') or ())
            for subscriber in subscribers:
                jtracker.sendMail(subscriber, subject, msg)

        if REQUEST is not None:
            REQUEST.RESPONSE.redirect(self.absolute_url())


    security.declareProtected(ManageJTracker, 'deleteReply')
    def deleteReply(self, reply_id, REQUEST=None):
        """ Remove a reply entirely """
        if reply_id in self._replies.keys():
            reply = self._replies[reply_id]
            file_name = reply.get('file_name', '')

            if file_name and file_name in self._files.keys():
                del self._files[file_name]
                
            del self._replies[reply_id]

            msg = 'Reply "%s" deleted.' % reply_id
        else:
            msg = 'No such reply "%s".' % reply_id

        self.indexObject()

        if REQUEST is not None:
            return self.manage_replies(manage_tabs_message=msg)


    security.declareProtected(ManageJTracker, 'deleteFile')
    def deleteFile(self, reply_id, REQUEST=None):
        """ Remove a file that is attached to an entry """
        if reply_id in self._replies.keys():
            reply = self._replies[reply_id]
            file_name = reply.get('file_name', '')

            if file_name and file_name in self._files.keys():
                reply['file_name'] = ''
                self._replies[reply_id] = reply
                del self._files[file_name]

            msg = 'File attachment on reply "%s" deleted.' % reply_id
        else:
            msg = 'No such reply "%s".' % reply_id

        if REQUEST is not None:
            return self.manage_replies(manage_tabs_message=msg)


    security.declareProtected(ManageJTracker, 'addFile')
    def addFile(self, reply_id, file, REQUEST=None):
        """ Add a file to an entry """
        if reply_id in self._replies.keys():
            reply = self._replies[reply_id]
            file_name = reply.get('file_name', '')

            if file_name:
                msg = 'Reply "%s" already has a file. Delete it first.' % reply_id
            else:
                file_id = self._storeFile(reply_id, file, respect_limit=0)
                reply['file_name'] = file_id
                self._replies[reply_id] = reply

                msg = 'Added file to reply "%s"' % reply_id

        if REQUEST is not None:
            return self.manage_replies(manage_tabs_message=msg)


    security.declareProtected(ManageJTracker, 'editReply')
    def editReply(self, reply_id, description, REQUEST=None):
        """ Edit a reply """
        if reply_id in self._replies.keys():
            reply = self._replies.get(reply_id)
            reply['description'] = description
            self._replies[reply_id] = reply

            msg = 'Reply "%s" edited.' % reply_id
        else:
            msg = 'No such reply "%s".' % reply_id

        self.indexObject()

        if REQUEST is not None:
            return self.manage_replies(manage_tabs_message=msg)


    security.declareProtected(SubmitJTrackerIssues, 'getReplyTypes')
    def getReplyTypes(self, unrestricted=0):
        """ Return the possible types of reply depending on permission """
        reply_types = ['Comment']
        privileged_reply_types = ['Accept', 'Resolve', 'Reject', 'Defer']

        if unrestricted == 0:
            user = getSecurityManager().getUser()

            if not self._replies and user.has_permission(SubmitJTrackerIssues, self):
                reply_types.append('Initial Request')

            if user.has_permission(SupportJTrackerIssues, self):
                reply_types.extend(privileged_reply_types)

        else:
            reply_types.extend(privileged_reply_types)

        return tuple(reply_types)


    security.declarePublic('getReplies')
    def getReplies(self):
        """ Return all replies """
        replies = []
        reply_ids = list(self._replies.keys())
        reply_numbers = [int(x.split('_')[1]) for x in reply_ids]
        reply_numbers.sort()
        reply_numbers.reverse()

        for number in reply_numbers:
            replies.append(self._replies.get('entry_%i' % number))

        return tuple(replies)


    security.declarePublic('getReply')
    def getReply(self, reply_id):
        """ return a reply by its ID """
        return self._replies.get(reply_id, None)


    security.declarePublic('manage_subscribe')
    def manage_subscribe(self, email='', REQUEST=None):
        """ Subscribe to this issue """
        if not email:
            msg = 'No email address specified'
        elif email.find('@') == -1 or email.find('.') == -1:
            msg = 'Invalid email address'
        else:
            email = email.strip()

            if not self._is_subscribed(email):
                subscribers = list(self.getProperty('subscribers', []))
                subscribers.append(email)
                self._updateProperty('subscribers', filter(None, subscribers))
                msg = 'Added subscriber "%s"' % email
            else:
                msg = '"%s" is already subscribed' % email

        if REQUEST is not None:
            return self.index_html(err_msg=msg, REQUEST=REQUEST)

    security.declarePublic('followups')
    def followups(self):
        """ How many followups have been posted? """
        return len(self._replies)


    def _is_subscribed(self, email):
        """ Check to see if the given email address is already subscribed """
        already_subscribed = 0
        subscribers = self.getProperty('subscribers', [])

        for subscriber in subscribers:
            if subscriber.find(email) != -1:
                already_subscribed = 1
                break

        return already_subscribed


    def __bobo_traverse__(self, REQUEST, name):
        """traverse trickery"""
        if name in self._views.keys():
            view_data = self._views.get(name)
            default_view = getattr(self, view_data.get('view_id'))
            vm = getattr(self, 'views_manager', None)

            if vm is None:
                return default_view

            view_ob = vm.getViewObject(self, view_data.get('view_label'))

            if view_ob is None:
                return default_view
            else:
                return view_ob

        try:
            return getattr(self, name)
        except AttributeError:
            pass

        if name in self._files.keys():
            file_ob = self._files.get(name)['file']
            return file_ob.__of__(self)

        method=REQUEST.get('REQUEST_METHOD', 'GET')
        if not method in ('GET', 'POST'):
            return NullResource(self, name, REQUEST).__of__(self)

        REQUEST.RESPONSE.notFoundError(name)


def manage_addJTrackerIssue( self
                           , title=''
                           , description=''
                           , component=''
                           , request_type=''
                           , requester_name=''
                           , requester_email=''
                           , file=None
                           , subscribers=()
                           , REQUEST=None
                           ):
    """ Add a new issue """
    id = _generateIssueId(self.this())
    jti = JTrackerIssue( id
                       , title
                       , description
                       , component
                       , request_type
                       , requester_name
                       , requester_email
                       , subscribers
                       )
    self._setObject(id, jti)

    jti = getattr(self, id)
    jti.addReply( reply_type='Initial Request'
                , description=description
                , requester_name=requester_name
                , requester_email=requester_email
                , file=file
                , send_email=0
                )

    if REQUEST is not None:
        ret_url = '%s/manage_main' % REQUEST['URL1']
        msg = 'Issue+Added'
        REQUEST.RESPONSE.redirect('%s?manage_tabs_message=%s' % (ret_url, msg))
    else:
        return id


InitializeClass(JTrackerIssue)
