IssueTrackerProduct/0000755000175000017500000000000011012074374014660 5ustar peterbepeterbeIssueTrackerProduct/tests/0000755000175000017500000000000011012074374016022 5ustar peterbepeterbeIssueTrackerProduct/tests/email-in-4.email0000644000175000017500000000031711012074371020665 0ustar peterbepeterbeDate: Fri, 8 Feb 2008 18:03:56 +0000 From: Mr Exception To: peter@example.com Cc: mail@example.com Sender: special@peterbe.com Subject: An email CCed in to someone This is the body IssueTrackerProduct/tests/framework.py0000644000175000017500000000701111012074371020365 0ustar peterbepeterbe############################################################################## # # Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.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. # ############################################################################## """ZopeTestCase framework COPY THIS FILE TO YOUR 'tests' DIRECTORY. This version of framework.py will use the SOFTWARE_HOME environment variable to locate Zope and the Testing package. If the tests are run in an INSTANCE_HOME installation of Zope, Products.__path__ and sys.path with be adjusted to include the instance's Products and lib/python directories respectively. If you explicitly set INSTANCE_HOME prior to running the tests, auto-detection is disabled and the specified path will be used instead. If the 'tests' directory contains a custom_zodb.py file, INSTANCE_HOME will be adjusted to use it. If you set the ZEO_INSTANCE_HOME environment variable a ZEO setup is assumed, and you can attach to a running ZEO server (via the instance's custom_zodb.py). The following code should be at the top of every test module: import os, sys if __name__ == '__main__': execfile(os.path.join(sys.path[0], 'framework.py')) ...and the following at the bottom: if __name__ == '__main__': framework() $Id: framework.py,v 1.2 2006/09/15 17:31:34 peterbe Exp $ """ __version__ = '0.2.4' # Save start state # __SOFTWARE_HOME = os.environ.get('SOFTWARE_HOME', '') __INSTANCE_HOME = os.environ.get('INSTANCE_HOME', '') if __SOFTWARE_HOME.endswith(os.sep): __SOFTWARE_HOME = os.path.dirname(__SOFTWARE_HOME) if __INSTANCE_HOME.endswith(os.sep): __INSTANCE_HOME = os.path.dirname(__INSTANCE_HOME) # Find and import the Testing package # if not sys.modules.has_key('Testing'): p0 = sys.path[0] if p0 and __name__ == '__main__': os.chdir(p0) p0 = '' s = __SOFTWARE_HOME p = d = s and s or os.getcwd() while d: if os.path.isdir(os.path.join(p, 'Testing')): zope_home = os.path.dirname(os.path.dirname(p)) sys.path[:1] = [p0, p, zope_home] break p, d = s and ('','') or os.path.split(p) else: print 'Unable to locate Testing package.', print 'You might need to set SOFTWARE_HOME.' sys.exit(1) import Testing, unittest execfile(os.path.join(os.path.dirname(Testing.__file__), 'common.py')) # Include ZopeTestCase support # if 1: # Create a new scope p = os.path.join(os.path.dirname(Testing.__file__), 'ZopeTestCase') if not os.path.isdir(p): print 'Unable to locate ZopeTestCase package.', print 'You might need to install ZopeTestCase.' sys.exit(1) ztc_common = 'ztc_common.py' ztc_common_global = os.path.join(p, ztc_common) f = 0 if os.path.exists(ztc_common_global): execfile(ztc_common_global) f = 1 if os.path.exists(ztc_common): execfile(ztc_common) f = 1 if not f: print 'Unable to locate %s.' % ztc_common sys.exit(1) # Debug # print 'SOFTWARE_HOME: %s' % os.environ.get('SOFTWARE_HOME', 'Not set') print 'INSTANCE_HOME: %s' % os.environ.get('INSTANCE_HOME', 'Not set') sys.stdout.flush() IssueTrackerProduct/tests/version.txt0000644000175000017500000000000311012074371020236 0ustar peterbepeterbe0.4IssueTrackerProduct/tests/jp-1.email0000644000175000017500000000310711012074371017600 0ustar peterbepeterbeReturn-path: Envelope-to: helpdesktest@example3.org Delivery-date: Thu, 20 Mar 2008 15:49:29 -0500 Received: from example by element.websitewelcome.com with local-bsmtp (Exim 4.68) (envelope-from ) id 1JcRhZ-0002D3-31 for helpdesktest@example3.org; Thu, 20 Mar 2008 15:49:29 -0500 X-Spam-Checker-Version: SpamAssassin 3.3.0-r613124 (2008-01-18) on element.websitewelcome.com X-Spam-Level: X-Spam-Status: No, score=-1.4 required=8.0 tests=AWL,RP_MATCHES_RCVD autolearn=disabled version=3.3.0-r613124 Received: from mail.jptechnical.com ([71.216.183.25]:3776 helo=jptechnical.com) by element.websitewelcome.com with esmtp (Exim 4.68) (envelope-from ) id 1JcRhY-0002CZ-Dy for helpdesktest@example3.org; Thu, 20 Mar 2008 15:49:28 -0500 Content-class: urn:content-classes:message MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: quoted-printable Subject: Test for IssueTrackerProduct mail parser 2 X-MimeOLE: Produced By Microsoft Exchange V6.5 Date: Thu, 20 Mar 2008 13:49:28 -0700 Message-ID: <466ECD862F94B149B3642E566C67C8D38FB5@companyweb> X-MS-Has-Attach: X-MS-TNEF-Correlator: Thread-Topic: Test for IssueTrackerProduct mail parser 2 thread-index: AciKy8xpS209BBl9Sw2H5n/1F1iZ9A== From: "Jesse Perry" To: This is the content of the email. This is from MS Outlook 2003 through exchange. This one, however, is text only. Jesse Perry=20 jp@jptechnical.com=20 JP Technical=20 www.jptechnical.com=20 office 360-829-6550=20 fax 775-257-0630=20 IssueTrackerProduct/tests/jp-0.email0000644000175000017500000001240611012074371017601 0ustar peterbepeterbeReturn-path: Envelope-to: helpdesktest@example3.org Delivery-date: Thu, 20 Mar 2008 15:48:49 -0500 Received: from example by element.websitewelcome.com with local-bsmtp (Exim 4.68) (envelope-from ) id 1JcRgv-00024O-2B for helpdesktest@example3.org; Thu, 20 Mar 2008 15:48:49 -0500 X-Spam-Checker-Version: SpamAssassin 3.3.0-r613124 (2008-01-18) on element.websitewelcome.com X-Spam-Level: X-Spam-Status: No, score=-1.4 required=8.0 tests=AWL,HTML_MESSAGE, RP_MATCHES_RCVD autolearn=disabled version=3.3.0-r613124 Received: from mail.jptechnical.com ([71.216.183.25]:3772 helo=jptechnical.com) by element.websitewelcome.com with esmtp (Exim 4.68) (envelope-from ) id 1JcRgu-00024F-SW for helpdesktest@example3.org; Thu, 20 Mar 2008 15:48:49 -0500 Content-class: urn:content-classes:message MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----_=_NextPart_001_01C88ACB.CD608363" Subject: Test for IssueTrackerProduct mail parser X-MimeOLE: Produced By Microsoft Exchange V6.5 Date: Thu, 20 Mar 2008 13:48:49 -0700 Message-ID: <466ECD862F94B149B3642E566C67C8D38FB4@companyweb> X-MS-Has-Attach: X-MS-TNEF-Correlator: Thread-Topic: Test for IssueTrackerProduct mail parser thread-index: AciKy8xpS209BBl9Sw2H5n/1F1iZ9A== From: "Jesse Perry" To: This is a multi-part message in MIME format. ------_=_NextPart_001_01C88ACB.CD608363 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: quoted-printable This is the content of the email. This is from MS Outlook 2003 through exchange. =20 Jesse Perry=20 jp@jptechnical.com =20 JP Technical=20 www.jptechnical.com =20 office 360-829-6550=20 fax 775-257-0630=20 =20 ------_=_NextPart_001_01C88ACB.CD608363 Content-Type: text/html; charset="us-ascii" Content-Transfer-Encoding: quoted-printable

This is the content of the email. This is from MS = Outlook 2003 through exchange.

 

Jesse Perry <= /p>

jp@jptechnical.com
JP Technical
www.jptechnical.com
office 360-829-6550
fax 775-257-0630
<= /p>

 

------_=_NextPart_001_01C88ACB.CD608363-- IssueTrackerProduct/tests/email-in-2.email0000644000175000017500000000024511012074371020663 0ustar peterbepeterbeDate: Fri, 8 Feb 2008 18:03:56 +0000 From: Mr Exception To: mail@example.com Sender: special@peterbe.com Subject: Email 2 This is the body IssueTrackerProduct/tests/testIssueTracker.py0000644000175000017500000014034711012074371021706 0ustar peterbepeterbe# -*- coding: iso-8859-1 -* ## ## ## import unittest import re from pprint import pprint from time import time from random import randint import sys, os if __name__ == '__main__': execfile(os.path.join(sys.path[0], 'framework.py')) from Globals import SOFTWARE_HOME from Testing import ZopeTestCase from AccessControl import getSecurityManager from AccessControl.SecurityManagement import newSecurityManager, noSecurityManager from DateTime import DateTime import Acquisition ZopeTestCase.installProduct('MailHost') ZopeTestCase.installProduct('ZCatalog') ZopeTestCase.installProduct('ZCTextIndex') ZopeTestCase.installProduct('SiteErrorLog') ZopeTestCase.installProduct('PythonScripts') ZopeTestCase.installProduct('IssueTrackerProduct') from Products.IssueTrackerProduct.Permissions import IssueTrackerManagerRole, IssueTrackerUserRole from Products.IssueTrackerProduct.Constants import ISSUEUSERFOLDER_METATYPE, \ DEBUG, ISSUE_DRAFT_METATYPE, TEMPFOLDER_REQUEST_KEY, \ FILTERVALUEFOLDER_THRESHOLD_CLEANING, FILTEROPTION_METATYPE #------------------------------------------------------------------------------ # # Some constants # #------------------------------------------------------------------------------ # Open ZODB connection app = ZopeTestCase.app() # Set up sessioning objects ZopeTestCase.utils.setupCoreSessions(app) ZopeTestCase.utils.setupSiteErrorLog(app) # Set up example applications #if not hasattr(app, 'Examples'): # ZopeTestCase.utils.importObjectFromFile(app, examples_path) # Close ZODB connection ZopeTestCase.close(app) #------------------------------------------------------------------------------ pre_submitissue_script_src = """ ## Script (Python) "pre_SubmitIssue" ##parameters= ##title= ## request = context.REQUEST title = request.get('title',u'') if title.lower().startswith(u'a'): return {'title':u'Subject line must NOT start with an A'} """ post_submitissue_script_src = """ ## Script (Python) "post_SubmitIssue" ##parameters=issue ##title= ## if 'Security' in issue.getSections(): # increase the urgency by one notch urgency = issue.getUrgency() options = issue.getUrgencyOptions() # acquired try: urgency = options[options.index(urgency)+1] except IndexError: # was already at the topmost return issue.editIssueDetails(urgency=urgency) """ #------------------------------------------------------------------------------ class NewFileUpload: def __init__(self, file_path): self.file = open(file_path) self.filename = os.path.basename(file_path) self.file_path = file_path def read(self, bytes=None): if bytes: return self.file.read(bytes) else: return self.file.read() def seek(self, bytes, mode=0): self.file.seek(bytes, mode) def tell(self): return self.file.tell() class DodgyNewFileUpload: """ a file upload that returns a blank string no when you read it """ def __init__(self, file_path): self.file = open(file_path) self.filename = os.path.basename(file_path) self.file_path = file_path def read(self, bytes=None, mode=0): return "" from Products.IssueTrackerProduct.IssueTracker import FilterValuer #------------------------------------------------------------------------------ class TestBase(ZopeTestCase.ZopeTestCase): def dummy_redirect(self, *a, **kw): self.has_redirected = a[0] if kw: print "*** Redirecting to %r + (%s)" % (a[0], kw) else: print "*** Redirecting to %r" % a[0] def afterSetUp(self): # install an issue tracker dispatcher = self.folder.manage_addProduct['IssueTrackerProduct'] dispatcher.manage_addIssueTracker('tracker', 'Issue Tracker') # install an error_log #dispatcher = self.folder.manage_addProduct['SiteErrorLog'] #dispatcher.manage_addErrorLog() # install a MailHost if not DEBUG: dispatcher = self.folder.manage_addProduct['MailHost'] dispatcher.manage_addMailHost('MailHost') # if you set this override you won't be able to do a transaction.get().commit() # in the unit tests. #self.mexpenses.http_redirect = self.dummy_redirect request = self.app.REQUEST sdm = self.app.session_data_manager request.set('SESSION', sdm.getSessionData()) #self.has_redirected = False def set_cookie(self, key, value, expires=365, path='/', across_domain_cookie_=False, **kw): self.app.REQUEST.cookies[key] = value #def beforeTearDown(self): def afterClear(self): global __trapped_emails__ __trapped_emails__ = [] class TestFunctionalBase(ZopeTestCase.FunctionalTestCase): def afterSetUp(self): # install an issue tracker dispatcher = self.folder.manage_addProduct['IssueTrackerProduct'] dispatcher.manage_addIssueTracker('tracker', 'Issue Tracker') # install a MailHost if not DEBUG: dispatcher = self.folder.manage_addProduct['MailHost'] dispatcher.manage_addMailHost('MailHost') request = self.app.REQUEST sdm = self.app.session_data_manager request.set('SESSION', sdm.getSessionData()) class IssueTrackerTestCase(TestBase): def test_addingIssue(self): """ test something """ #self.tracker = self.folder['mexpenses'] tracker = self.folder.tracker request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) self.assertEqual(len(tracker.getIssueObjects()), 1) def test_modifyingIssue(self): """ test something """ #self.tracker = self.folder['mexpenses'] tracker = self.folder.tracker request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) issue = tracker.getIssueObjects()[0] request.set('comment', u'COMMENT') issue.ModifyIssue(request) self.assertEqual(len(issue.getThreadObjects()), 1) def test_debatingIssue(self): """ test posting a followup under a different email address than the original """ tracker = self.folder.tracker tracker.sitemaster_email = 'something@valid.com' request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) issue = tracker.getIssueObjects()[0] request.set('comment', u'COMMENT') request.set('fromname', u'Someone Else') request.set('email', u'else@address.com') request.set('notify', 1) issue.ModifyIssue(request) # there should now be a notifiation object self.assertEqual(len(issue.getCreatedNotifications()), 1) # have a look at that notification object notification = issue.getCreatedNotifications()[0] self.assertEqual(notification.getTitle(), u'TITLE') self.assertTrue(isinstance(notification.getTitle(), unicode)) self.assertEqual(notification.getIssueID(), issue.getId()) self.assertEqual(notification.getEmails(), [u'email@address.com']) if tracker.doDispatchOnSubmit(): self.assertTrue(notification.isDispatched()) # we should now expect an email to have been sent to email@address.com assert __trapped_emails__, "not trapped emails" latest_email = __trapped_emails__[0] self.assertTrue(latest_email['mto'].find('email@address.com') > -1) self.assertTrue(latest_email['mfrom'].find('something@valid.com') > -1) def test_debatingIssue_withSmartAvoidanceOfNotifications(self): """ If A posts an issue, B follows up and shortly there after A follows up too, then if the automatic dispatcher is switched off, the notification to A can be ignored since A has already seen the followup. """ tracker = self.folder.tracker # Important tracker.dispatch_on_submit = False A = u'email@address.com' Af = u'From name' request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', Af) request.set('email', A) request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) B = u'else@address.com' Bf = u'Someone Else' issue = tracker.getIssueObjects()[0] request.set('comment', u'COMMENT') request.set('fromname', Bf) request.set('email', B) request.set('notify', 1) issue.ModifyIssue(request) # have a look at that notification object notification = issue.getCreatedNotifications()[0] self.assertFalse(notification.isDispatched()) # A returns and posts a followup request.set('comment', u'REPLY') request.set('fromname', Af) request.set('email', A) request.set('notify', 1) issue.ModifyIssue(request) # Since the second followup done by A must mean that A # doesn't need to be notified about the first notification # since A has already made a newer followup. # However, B's notification should still be there. self.assertEqual(len(issue.getCreatedNotifications()), 1) notification = issue.getCreatedNotifications()[0] # this should be designated to B self.assertEqual(notification.getEmails(), [B]) # Ok, let's do it again. # Now, C joins in so that each notification is designated # to two people. By adding a followup by A or B, there is # no need to send out the notification to A or B. C = u'C@email.com' Cf = u'Mr. C' request.set('comment', u'COMMENT BY C') request.set('fromname', Cf) request.set('email', C) request.set('notify', 1) issue.ModifyIssue(request) # there should now be one new notification designated # to A AND B. self.assertEqual(len(issue.getCreatedNotifications()), 2) # let's look at the latest notification notification = issue.getCreatedNotifications(sort=True)[1] self.assertEqual(notification.getEmails(), [A, B]) request.set('comment', u'COMMENT BY B again') request.set('fromname', Bf) request.set('email', B) request.set('notify', 1) issue.ModifyIssue(request) self.assertEqual(len(issue.getCreatedNotifications()), 2) # let's look at the latest notification notification = issue.getCreatedNotifications(sort=True)[1] self.assertEqual(notification.getEmails(), [A, C]) def test_debatingIssue_withSmartAvoidanceOfNotifications_part2(self): """ same test_debatingIssue_withSmartAvoidanceOfNotifications() but this time test what happens if an issue is created with always notify on or an issue is assigned to someone. """ tracker = self.folder.tracker # Important tracker.dispatch_on_submit = False # add an issue user folder # Since manage_addIssueUserFolder() needs to add the two extra roles # we have to do that first because manage_addIssueUserFolder() isn't # allowed to it because it's not a POST request. tracker._addRole(IssueTrackerUserRole) tracker._addRole(IssueTrackerManagerRole) tracker.manage_addProduct['IssueTrackerProduct']\ .manage_addIssueUserFolder(keep_usernames=True) tracker.acl_users.userFolderAddUser("user", "secret", [IssueTrackerUserRole], [], email="user@test.com", fullname="User Name") # make the always notify of issuetracker be 'user' and A A = 'a@a.com' Af = 'Aaa' checked = [] for each in (A, 'user'): valid, better_spelling = tracker._checkAlwaysNotify(each) if valid: checked.append(better_spelling) tracker.always_notify = checked # If someone else now adds an issue, a notification should # be made going out to user@test.com adn a@a.com B = u'email@address.com' Bf = u'From name' request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', Bf) request.set('email', B) request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) issue = tracker.getIssueObjects()[0] # have a look at that notification object self.assertEqual(len(issue.getCreatedNotifications()), 1) notification = issue.getCreatedNotifications()[0] self.assertFalse(notification.isDispatched()) self.assertEqual(notification.getEmails(), ['a@a.com','user@test.com']) # now, if a@a.com follows up on B's new issue, there'll be # you can cross off a@a.com from the notification # object. request.set('comment', u'COMMENT') request.set('fromname', Af) request.set('email', A) request.set('notify', 1) issue.ModifyIssue(request) # there should now be a new notification object where # the latest one goes out to the submitter of the issue self.assertEqual(len(issue.getCreatedNotifications()), 2) latest_notification = issue.getCreatedNotifications(sort=True)[-1] self.assertEqual(latest_notification.getEmails(), [B]) def test_debatingIssue_withSmartAvoidanceOfNotifications_part3(self): """ same as test_debatingIssue_withSmartAvoidanceOfNotifications() but create an assignment and then later as that assignee, participate in the issue and that should cancel the notification going out to the assignee. """ tracker = self.folder.tracker # Important tracker.dispatch_on_submit = False # add an issue user folder # Since manage_addIssueUserFolder() needs to add the two extra roles # we have to do that first because manage_addIssueUserFolder() isn't # allowed to it because it's not a POST request. tracker._addRole(IssueTrackerUserRole) tracker._addRole(IssueTrackerManagerRole) tracker.manage_addProduct['IssueTrackerProduct']\ .manage_addIssueUserFolder(keep_usernames=True) tracker.acl_users.userFolderAddUser("user", "secret", [IssueTrackerUserRole], [], email="user@test.com", fullname="User Name") # switch on issue assignment tracker.manage_UseIssueAssignmentToggle() self.assertEqual(len(tracker.getAllIssueUsers()), 1) assignee_option = tracker.getAllIssueUsers()[0] self.assertEqual(assignee_option['user'].getUserName(), 'user') # If someone else now adds an issue, a notification should # be made going out to user@test.com adn a@a.com B = u'email@address.com' Bf = u'From name' request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', Bf) request.set('email', B) request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) request.set('assignee', tracker.getAllIssueUsers()[0]['identifier']) request.form['notify-assignee'] = '1' tracker.SubmitIssue(request) issue = tracker.getIssueObjects()[0] # have a look at that notification object self.assertEqual(len(issue.getCreatedNotifications()), 1) notification = issue.getCreatedNotifications()[0] self.assertFalse(notification.isDispatched()) self.assertEqual(notification.getEmails(), ['user@test.com']) self.assertTrue(notification.getAssignmentObject() is not None) assignment = notification.getAssignmentObject() self.assertEqual(assignment.getEmail(), B) # who added it self.assertEqual(assignment.getAssigneeEmail(), "user@test.com") # assigned to # log in as this assignee uf = tracker.acl_users assert uf.meta_type == ISSUEUSERFOLDER_METATYPE user = uf.getUserById('user') user = user.__of__(uf) newSecurityManager(None, user) assert getSecurityManager().getUser().getUserName() == 'user' # now reply a comment as this logged in user which should # evetually nullify the notification going to this assignee request.set('comment', u'COMMENT') #request.set('fromname', Af) #request.set('email', A) request.set('notify', 1) issue.ModifyIssue(request) # there should now be a new notification object where # the latest one goes out to the submitter of the issue self.assertEqual(len(issue.getCreatedNotifications()), 1) latest_notification = issue.getCreatedNotifications()[0] self.assertEqual(latest_notification.getEmails(), [B]) def test_Real0695_bug(self): # in lack of a better name """ test that RSS and RDF feeds have the same security protection like viewing the issuetracker, the list of issues or an issue. """ tracker = self.folder.tracker request = self.app.REQUEST # Adding an issue add_issue_html = tracker.AddIssue(request) self.assertTrue(add_issue_html.find('Description:') > -1) # add an issue so there's something in the ListIssues, and the XML feeds A = u'email@address.com' Af = u'From name' request.set('title', u'TITLE') request.set('fromname', Af) request.set('email', A) request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) # first of all, viewing these with the current user should be fine. #template_list_html = tracker.ListIssues(request) template_list_html = tracker.restrictedTraverse('ListIssues')(request) # expect it to say "# Issues: 0" since there are no issues added self.assertTrue(template_list_html.find('# Issues: 1') > -1) # rss.xml rss_xml = getattr(tracker, 'rss.xml')() self.assertTrue(rss_xml.find('<![CDATA[TITLE (Open)]]>') > -1) # rdf.xml rdf_xml = getattr(tracker, 'rdf.xml')() self.assertTrue(rdf_xml.find('TITLE (Open)') > -1) # Now, let's disallow anonymous access msg = tracker.manage_ViewPermissionToggle() self.assertEqual(msg, 'View permission disabled for Anonymous') # before we log out, let's create a user with the IssueTracker # IssueTrackerManagerRole self.folder.acl_users.userFolderAddUser("manager", "secret", [IssueTrackerManagerRole], []) self.folder.acl_users.userFolderAddUser("user", "secret", [IssueTrackerUserRole], []) # Now, if I log out, none of the viewings above should work self.logout() assert getSecurityManager().getUser().getUserName() == 'Anonymous User' from zExceptions import Unauthorized self.assertRaises(Unauthorized, tracker.restrictedTraverse, 'ListIssues') self.assertRaises(Unauthorized, tracker.restrictedTraverse, 'rss.xml') self.assertRaises(Unauthorized, tracker.restrictedTraverse, 'rdf.xml') def test_preSubmitIssue_hook(self): """ test adding an issue with a script hook called 'pre_SubmitIssue()' """ tracker = self.folder.tracker tracker.dispatch_on_submit = False # no annoying emails on stdout adder = tracker.manage_addProduct['PythonScripts'].manage_addPythonScript adder('pre_SubmitIssue') script = getattr(tracker, 'pre_SubmitIssue') script.write(pre_submitissue_script_src) # With this hook it won't be possible to add a issue where # the subject line starts with the letter a. (silly, yes) request = self.app.REQUEST request.set('title', u'A TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) self.assertEqual(len(tracker.getIssueObjects()), 0) request.set('title', u'Some TITLE') tracker.SubmitIssue(request) self.assertEqual(len(tracker.getIssueObjects()), 1) def test_postSubmitIssue_hook(self): """ test adding an issue with a script hook called 'post_SubmitIssue()' """ tracker = self.folder.tracker tracker.dispatch_on_submit = False # no annoying emails on stdout tracker.can_add_new_sections = True # add an issue user folder # Since manage_addIssueUserFolder() needs to add the two extra roles # we have to do that first because manage_addIssueUserFolder() isn't # allowed to it because it's not a POST request. tracker._addRole(IssueTrackerUserRole) tracker._addRole(IssueTrackerManagerRole) tracker.manage_addProduct['IssueTrackerProduct']\ .manage_addIssueUserFolder(keep_usernames=True) tracker.acl_users.userFolderAddUser("user", "secret", [IssueTrackerManagerRole,'Manager'], [], email="user@test.com", fullname="User Name") adder = tracker.manage_addProduct['PythonScripts'].manage_addPythonScript adder('post_SubmitIssue') script = getattr(tracker, 'post_SubmitIssue') script.write(post_submitissue_script_src) # With this hook it won't be possible to add a issue where # the subject line starts with the letter a. (silly, yes) request = self.app.REQUEST request.set('title', u'A TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('newsection', 'Security') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) uf = tracker.acl_users assert uf.meta_type == ISSUEUSERFOLDER_METATYPE user = uf.getUserById('user') user = user.__of__(uf) newSecurityManager(None, user) assert getSecurityManager().getUser().getUserName() == 'user' def test_getModifyTimestamp(self): """ test issuetracker.getModifyTimestamp() """ tracker = self.folder.tracker # with no issues, the getModifyTimestamp() should be the # same as the issuetrackers' bobobase_modification_time() self.assertEqual(int(tracker.bobobase_modification_time()), tracker.getModifyTimestamp()) # if we add an issue, the issuetrackers' getModifyTimestamp() # should be that of the last added issue. request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) tracker.SubmitIssue(request) issue = tracker.getIssueObjects()[0] self.assertEqual(issue.getModifyTimestamp(), tracker.getModifyTimestamp()) def test_okFileAttachment(self): """ try to add an issue with a crap file attachment """ tracker = self.folder.tracker request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) request.set('fileattachment', NewFileUpload(os.path.abspath(__file__))) tracker.SubmitIssue(request) issue = tracker.getIssueObjects()[0] self.assertEqual(issue.countFileattachments(), 1) def test_crapFileAttachment(self): """ try to add an issue with a crap file attachment """ tracker = self.folder.tracker request = self.app.REQUEST request.set('title', u'TITLE') request.set('fromname', u'From name') request.set('email', u'email@address.com') request.set('description', u'DESCRIPTION') request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) request.set('fileattachment', DodgyNewFileUpload(os.path.abspath(__file__))) # this should fail to add an issue tracker.SubmitIssue(request) self.assertEqual(len(tracker.getIssueObjects()), 0) # this time, try to upload it with a file that is empty empty_file = os.path.join(os.path.dirname(__file__), 'size0_file.jpg') request.set('fileattachment', NewFileUpload(os.path.abspath(empty_file))) tracker.SubmitIssue(request) self.assertEqual(len(tracker.getIssueObjects()), 0) def test_saveIssueDraft(self): """ try to add an issue with a crap file attachment """ tracker = self.folder.tracker request = self.app.REQUEST title = u'TITLE'; request.set('title', title) fromname = u'From name'; request.set('fromname', fromname) email = u'email@address.com'; request.set('email', email) description = u'DESCRIPTION'; request.set('description', description) request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) request.set('fileattachment', NewFileUpload(os.path.abspath(__file__))) tracker.SaveDraftIssue(request) # Because getMyIssueDrafts() depends on cookies and cookies don't # work in ZopeTestCase :( we have to fake this a bit drafts = tracker.getDraftsContainer().objectValues(ISSUE_DRAFT_METATYPE) assert len(drafts) == 1 # Now check that draft draft = drafts[0] self.assertEqual(draft.getTitle(), title) self.assertEqual(draft.getDescription(), description) self.assertEqual(draft.getFromname(), fromname) self.assertEqual(draft.getEmail(), email) self.assertEqual(draft.getType(), tracker.getDefaultType()) self.assertEqual(draft.getUrgency(), tracker.getDefaultUrgency()) # from this it will be possible to get the files back via the # tempfolder assert request.get(TEMPFOLDER_REQUEST_KEY), "no tempfolder set in request" tempfolder = tracker._getTempFolder() files = tempfolder[request.get(TEMPFOLDER_REQUEST_KEY)].objectValues('File') assert files, "no temp files" temp_file = files[0] self.assertEqual(temp_file.getId(), os.path.basename(__file__)) def test_searchIssues(self): """ basic search tests """ tracker = self.folder.tracker request = self.app.REQUEST title = u'titles are working'; request.set('title', title) fromname = u'From name'; request.set('fromname', fromname) email = u'email@address.com'; request.set('email', email) description = u'DESCRIPTION is a in the this test' request.set('description', description) request.set('type', tracker.getDefaultType()) request.set('urgency', tracker.getDefaultUrgency()) request.set('fileattachment', NewFileUpload(os.path.abspath(__file__))) tracker.SubmitIssue(request) assert tracker.getIssueObjects() issue = tracker.getIssueObjects()[0] # search and find it q = 'working' self.assertEqual(issue, tracker._searchCatalog(q, search_only_on=None)[0]) self.assertEqual(issue, tracker._searchCatalog(q, search_only_on='title')[0]) # don't expect to find it q = 'notmentioned' self.assertEqual(tracker._searchCatalog(q, search_only_on='description'), []) self.assertEqual(tracker._searchCatalog(q), []) # search fuzzy and find it q = 'title' self.assertEqual(issue, tracker._searchCatalog(q)[0]) # it should be case insensitive q = 'DeScriPtion' self.assertEqual(issue, tracker._searchCatalog(q)[0]) # low level test of searching by filename self.assertEqual(issue, tracker._searchByFilename(os.path.basename(__file__))[0]) # and do it fuzzy self.assertEqual(issue, tracker._searchByFilename(os.path.basename(__file__).upper())[0]) # or without the extension name = os.path.splitext(os.path.basename(__file__))[0] self.assertEqual(issue, tracker._searchByFilename(name)[0]) def test_filterIssues(self): """ test to call filter issues and note how the filter should be saved and should be reusable later. """ tracker = self.folder.tracker request = self.app.REQUEST # first, the user who performs this test is a normal zope acl # user. Let's add a name and email so that we can make sure # this is information is saved in the saved filter self.set_cookie(tracker.getCookiekey('name'), u'Bob') self.set_cookie(tracker.getCookiekey('email'), u'bob@test.com') # initially there shouldn't be a folder called 'saved-filters' self.assertFalse(hasattr(tracker, 'saved-filters')) # and there shouldn't be a zcatalog called 'saved-filters-catalog' self.assertFalse(hasattr(tracker, 'saved-filters-catalog')) # On the homepage, the links to see only say issues "On hold" is # /ListIssues?Filterlogic=show&f-statuses=on%20hold # Let's mimick that: request.set('Filterlogic','show') request.set('f-statuses','on hold') tracker.ListIssuesFiltered() # this should now have create the saved-filters folder # and the saved-filters-catalog self.assertTrue(hasattr(tracker, 'saved-filters')) self.assertTrue(hasattr(tracker, 'saved-filters-catalog')) # let's look at what was created in the saved-filters folder saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) # since I'm here logged into Zope as a normal Zope user # we can expect to find that the saved filter should be to me zopeuser = tracker.getZopeUser() path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_adder = ','.join([path, name]) saved_filter = saved_filters[0] self.assertEqual(saved_filter.acl_adder, acl_adder) self.assertEqual(saved_filter.getTitle(), u"Only on hold issues") # this is who created the filter (quite unimportant) self.assertEqual(saved_filter.adder_fromname, u'Bob') self.assertEqual(saved_filter.adder_email, 'bob@test.com') # and we didn't need to associate with a cookie key self.assertEqual(saved_filter.key, '') # the logic was to show self.assertEqual(saved_filter.filterlogic, 'show') # some attributes are automatically set for the issue metadata self.assertEqual(saved_filter.sections, None) self.assertEqual(saved_filter.urgencies, None) self.assertEqual(saved_filter.types, None) self.assertEqual(saved_filter.statuses, [u'on hold']) # We should have a saved-filters-catalog created catalog = tracker.getFilterValuerCatalog() self.assertTrue(catalog is not None) # and there should only be one brain it right now self.assertTrue(len(catalog.searchResults()) == 1) saved_filter_from_brain = catalog.searchResults()[0].getObject() self.assertTrue(saved_filter_from_brain == saved_filter) # the high level function getMySavedFilters() uses the catalog # to extract the saved filters with the most recent one # first. saved_filter_from_mysavedfilters = tracker.getMySavedFilters()[0] self.assertTrue(saved_filter_from_mysavedfilters == saved_filter) # If you run ListIssuesFiltered() again, it should have to create # one more new saved filter tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) # But if we change the parameters a little bit it should have # created a new saved filter request.set('f-statuses','taken') tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 2) # getMySavedFilters() is smart in that it returns the filters ordered. # Test that the more recent one comes first saved_filters_from_mysavedfilters = tracker.getMySavedFilters() self.assertEqual(saved_filters_from_mysavedfilters[0].statuses, [u'taken']) # Test the function getCurrentlyUsedSavedFilter(request_only=True) assert tracker.getCurrentlyUsedSavedFilter() is None saved_filter_id = tracker.getCurrentlyUsedSavedFilter(request_only=False) # now check that this is the correct one self.assertEqual(saved_filter_id, saved_filters_from_mysavedfilters[0].getId()) # another (more long winded) way of checking this is by that since the # last filter was to filter by "taken". Check that this is what the # filter does that comes from getCurrentlyUsedSavedFilter(request_only=False) current_saved_filter = getattr(getattr(tracker, 'saved-filters'), saved_filter_id) self.assertEqual(current_saved_filter.statuses, [u'taken']) # Some options that can be passed directly to _ListIssuesFiltered # are: # skip_filter # skip_sort # # To set these for ListIssuesFiltered() you have to put them in the # REQUEST. These are useful if you for example want to ignore possibly # filters in session such as for the homepage where it uses # ListIssuesFiltered() but without any filtering. # The skip_sort is useful to set when you don't want any sorting since # sorting will only cost time. # XXX: Only able to test this WITH issues def test_filterIssues_anonymous_user(self): """ test filtering issues when the user is not logged in """ # when *not* logged in there are two ways to remember a saved filter: # by name and email # by a cookie key # Let's first try to filter issues as a complete nobody noSecurityManager() tracker = self.folder.tracker request = self.app.REQUEST tracker.set_cookie = self.set_cookie # On the homepage, the links to see only say issues "On hold" is # /ListIssues?Filterlogic=show&f-statuses=on%20hold # Let's mimick that: request.set('Filterlogic','show') request.set('f-statuses','on hold') tracker.ListIssuesFiltered() # let's look at what was created in the saved-filters folder saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) saved_filter = saved_filters[0] self.assertTrue(saved_filter.getKey() in request.cookies.values()) # run it again and it shouldn't create another saved filter tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) def test_filterIssues_anonymous_named_user(self): """ test filtering when the user is no logged in but has a name and email in the cookie. """ noSecurityManager() tracker = self.folder.tracker request = self.app.REQUEST tracker.set_cookie = self.set_cookie self.set_cookie(tracker.getCookiekey('name'), u'Bob') self.set_cookie(tracker.getCookiekey('email'), u'bob@test.com') # On the homepage, the links to see only say issues "On hold" is # /ListIssues?Filterlogic=show&f-statuses=on%20hold # Let's mimick that: request.set('Filterlogic','show') request.set('f-statuses','on hold') tracker.ListIssuesFiltered() # let's look at what was created in the saved-filters folder saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) saved_filter = saved_filters[0] self.assertEqual(saved_filter.adder_email, 'bob@test.com') self.assertEqual(saved_filter.adder_fromname, u'Bob') # run it again and it shouldn't create another saved filter tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) def test_filterIssues_anonymous_named_user_no_email(self): """ test filtering when the user is no logged in but has a name and email in the cookie. """ noSecurityManager() tracker = self.folder.tracker request = self.app.REQUEST tracker.set_cookie = self.set_cookie self.set_cookie(tracker.getCookiekey('name'), u'Bob') # On the homepage, the links to see only say issues "On hold" is # /ListIssues?Filterlogic=show&f-statuses=on%20hold # Let's mimick that: request.set('Filterlogic','show') request.set('f-statuses','on hold') tracker.ListIssuesFiltered() # let's look at what was created in the saved-filters folder saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) saved_filter = saved_filters[0] self.assertEqual(saved_filter.adder_fromname, u'Bob') # run it again and it shouldn't create another saved filter tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) def test_filterIssues_recycleable(self): """ test to call filter issues and note how the filter should be saved and should be reusable later. When you *go back* to run a filter you've already done before it should be able to reuse an existing object instead of having to create a new one.""" tracker = self.folder.tracker request = self.app.REQUEST # 1 request.set('Filterlogic','show') request.set('f-statuses','on hold') tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) # 2 request.set('f-statuses','taken') tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 2) # 3 request.set('f-statuses','on hold') tracker.ListIssuesFiltered() saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 2) def test_filterIssues_from_cookie_after_purge(self): """ If all the saved-filters are deleted and someone has a cookie of an old savedfilter, if the saved filter is deleted, getting it from the cookie shouldn't raise an AttributeError. """ tracker = self.folder.tracker request = self.app.REQUEST tracker.set_cookie = self.set_cookie ckey = tracker.getCookiekey('remember_savedfilter_persistently') tracker.set_cookie(ckey, 1) # 1 request.set('Filterlogic','show') request.set('f-statuses','on hold') tracker.ListIssuesFiltered() self.assertTrue('__issuetracker_savedfilter_id-tracker' in request.cookies) saved_filters = getattr(tracker, 'saved-filters').objectValues() self.assertEqual(len(saved_filters), 1) # now, mess with it tracker.manage_delObjects(['saved-filters','saved-filters-catalog']) # if the cookie causes an AttributeError, then ListIssuesFiltered() # here wouldn't work. Just running this is the final test. tracker.ListIssuesFiltered() def test_clean_saved_filters(self): """ CleanOldSavedFilters() is called by manage_UpdateEverything() but also if the number of saved filters exceeds FILTERVALUEFOLDER_THRESHOLD_CLEANING. We'll first try the manual way of cleaning. """ tracker = self.folder.tracker container = tracker._getFilterValueContainer() for i in range(100): oid = 'random-%s' % i instance = FilterValuer(oid, 'filter name') container._setObject(oid, instance) valuer = container._getOb(oid) if randint(1, 5) == 1: valuer.set('acl_adder', 'fake acl adder') elif randint(1, 4) == 1: valuer.set('key', str(randint(1, 10))) else: valuer.set('adder_email', 'email') valuer.mod_date = DateTime(time() - randint(1000, 1000000)*10) valuer.index_object() max_ = FILTERVALUEFOLDER_THRESHOLD_CLEANING count_filtervaluers = len(container.objectIds()) assert count_filtervaluers < max_ # there should also be equally many indexed objects as there # are reported by len(objectids) catalog = tracker.getFilterValuerCatalog() search = {'meta_type': FILTEROPTION_METATYPE} brains = catalog.searchResults(**search) assert len(brains) == count_filtervaluers # there are now 100 old saved filters whose # bobobase_modification_time ranges between 4 to 0 months old msg = tracker.CleanOldSavedFilters() msg_regex = re.compile('Deleted (\d+) old saved filters') no_deleted = int(msg_regex.findall(msg)[0]) self.assertTrue(no_deleted < count_filtervaluers) left = count_filtervaluers - no_deleted brains = catalog.searchResults(**search) assert len(brains) == left, \ "no. brains=%s, left=%s" %(len(brains), left) def test_clean_saved_filters_then_implode(self): """ Similar to test_clean_saved_filters() except this time we're making sure ALL saved filters are so old that if we send implode_if_possible=True to CleanOldSavedFilters() and expect the container to disappear. """ tracker = self.folder.tracker container = tracker._getFilterValueContainer() for i in range(100): oid = 'random-%s' % i instance = FilterValuer(oid, 'filter name') container._setObject(oid, instance) valuer = container._getOb(oid) if randint(1, 5) == 1: valuer.set('acl_adder', 'fake acl adder') elif randint(1, 4) == 1: valuer.set('key', str(randint(1, 10))) else: valuer.set('adder_email', 'email') valuer.mod_date = DateTime(time() - randint(1000, 10000)*3600) valuer.index_object() self.assertTrue('saved-filters' in tracker.objectIds()) msg = tracker.CleanOldSavedFilters(implode_if_possible=True) self.assertTrue('saved-filters' not in tracker.objectIds()) # and the catalog should be empty catalog = tracker.getFilterValuerCatalog() search = {'meta_type': FILTEROPTION_METATYPE} brains = catalog.searchResults(**search) self.assertTrue(len(brains) == 0) def test_unicode_in_statuses(self): """ test that it's possible to set a verb:action pair to the statuses that is unicode. """ tracker = self.folder.tracker request = self.app.REQUEST for status, verb in tracker.getStatusesMerged(aslist=True): self.assertTrue(isinstance(status, unicode)) self.assertTrue(isinstance(verb, unicode)) # All the default ones are easy, lets spice it up a bit statuses_and_verbs = [u'open, open', u'taken, take', u'on hold, put on hold', u'a pr\xe9c\xe9dente, faire pr\xe9c\xe9dente', u'rejected, reject', u'completed, complete'] request.set('statuses-and-verbs', statuses_and_verbs) tracker.manage_editIssueTrackerProperties(carefulbooleans=True, REQUEST=request) for status, verb in tracker.getStatusesMerged(aslist=True): self.assertTrue(isinstance(status, unicode)) self.assertTrue(isinstance(verb, unicode)) #class IssueTrackerFunctionalTestCase(TestFunctionalBase): def test_suite(): from unittest import TestSuite, makeSuite suite = TestSuite() suite.addTest(makeSuite(IssueTrackerTestCase)) # suite.addTest(makeSuite(IssueTrackerFunctionalTestCase)) return suite from Products.MailHost.MailHost import MailBase def _monkeypatch_send(self, mfrom, mto, messageText): if 0: import inspect print "_send(%r, %r, %r)" % (mfrom, mto, messageText[:40]+'...') for i in range(2,6): try: #caller_module = inspect.stack()[i][1] caller_method = inspect.stack()[i][3] caller_method_line = inspect.stack()[i][2] except IndexError: break print "\t%s:%s"%(caller_method, caller_method_line) print "" __trapped_emails__.append(dict(mfrom=mfrom, mto=mto, messageText=messageText)) #print >>sys.stderr, "from:%s To:%s" % (mfrom, mto) MailBase._send = _monkeypatch_send __trapped_emails__ = [] if __name__ == '__main__': framework() IssueTrackerProduct/tests/CHANGES.log0000644000175000017500000000036511012074371017576 0ustar peterbepeterbe- 0.4 Added class StandaloneWordRegex to testUtils.py - 0.3 Added class AddParam2URLTestCase to testUtils.py - 0.2 Added class SplitTermsTestCase to testUtils.py - 0.1 Added testUtils.py with unittest class TimeSinceTestCaseIssueTrackerProduct/tests/__init__.py0000644000175000017500000000000111012074371020117 0ustar peterbepeterbe#IssueTrackerProduct/tests/email-in-1.email0000644000175000017500000000024411012074371020661 0ustar peterbepeterbeDate: Fri, 8 Feb 2008 18:03:56 +0000 From: "Peter" To: mail@example.com Sender: mail@peterbe.com Subject: Test subject line This is the body IssueTrackerProduct/tests/testEmailIn.py0000644000175000017500000003713211012074371020615 0ustar peterbepeterbe# -*- coding: iso-8859-1 -* ## ## ## import unittest import sys, os import stat if __name__ == '__main__': execfile(os.path.join(sys.path[0], 'framework.py')) from Globals import SOFTWARE_HOME from Testing import ZopeTestCase from AccessControl import getSecurityManager from AccessControl.SecurityManagement import newSecurityManager import Acquisition ZopeTestCase.installProduct('MailHost') ZopeTestCase.installProduct('ZCatalog') ZopeTestCase.installProduct('ZCTextIndex') ZopeTestCase.installProduct('SiteErrorLog') ZopeTestCase.installProduct('PythonScripts') ZopeTestCase.installProduct('IssueTrackerProduct') #------------------------------------------------------------------------------ # # Some constants # #------------------------------------------------------------------------------ # Open ZODB connection app = ZopeTestCase.app() # Set up sessioning objects ZopeTestCase.utils.setupCoreSessions(app) # Set up example applications #if not hasattr(app, 'Examples'): # ZopeTestCase.utils.importObjectFromFile(app, examples_path) # Close ZODB connection ZopeTestCase.close(app) #------------------------------------------------------------------------------ pre_submitissue_script_src = """ ## Script (Python) "pre_SubmitIssue" ##parameters= ##title= ## request = context.REQUEST title = request.get('title',u'') if title.lower().startswith(u'a'): return {'title':u'Subject line must NOT start with an A'} """ post_submitissue_script_src = """ ## Script (Python) "post_SubmitIssue" ##parameters=issue ##title= ## if 'Security' in issue.getSections(): # increase the urgency by one notch urgency = issue.getUrgency() options = issue.getUrgencyOptions() # acquired try: urgency = options[options.index(urgency)+1] except IndexError: # was already at the topmost return issue.editIssueDetails(urgency=urgency) """ #------------------------------------------------------------------------------ class TestBase(ZopeTestCase.ZopeTestCase): def dummy_redirect(self, *a, **kw): self.has_redirected = a[0] if kw: print "*** Redirecting to %r + (%s)" % (a[0], kw) else: print "*** Redirecting to %r" % a[0] def afterSetUp(self): # install an issue tracker dispatcher = self.folder.manage_addProduct['IssueTrackerProduct'] dispatcher.manage_addIssueTracker('tracker', 'Issue Tracker') # install an error_log dispatcher = self.folder.manage_addProduct['SiteErrorLog'] dispatcher.manage_addErrorLog() # if you set this override you won't be able to do a transaction.get().commit() # in the unit tests. #self.mexpenses.http_redirect = self.dummy_redirect request = self.app.REQUEST sdm = self.app.session_data_manager request.set('SESSION', sdm.getSessionData()) #self.has_redirected = False # def tearDown(self): # pass class POP3TestCase(TestBase): """ Test to create a POP3 account object and several accepting email accounts inside it. """ def test_creatingAccount(self): """ test to create a POP3 account """ tracker = self.folder.tracker self.assertEqual(tracker.getPOP3Accounts(), []) tracker.createPOP3Account('mail.example.com', 'peter', 'secret') self.assertEqual(len(tracker.getPOP3Accounts()), 1) account = tracker.getPOP3Accounts()[0] self.assertEqual(account.getTitle(), 'mail.example.com') self.assertEqual(account.getHostname(), 'mail.example.com') self.assertEqual(account.getPort(), 110) self.assertEqual(account.getUsername(), 'peter') self.assertFalse(account.doDeleteAfter()) self.assertFalse(account.doSSL()) self.assertEqual(len(account.getAcceptingEmails()), 0) # try editing it account.manage_editAccount(hostname='m.example.com', username='peterbe', portnr=210, delete_after=1) self.assertEqual(account.getHostname(), 'm.example.com') self.assertEqual(account.getPort(), 210) self.assertEqual(account.getUsername(), 'peterbe') self.assertTrue(account.doDeleteAfter()) def test_creatingAcceptingEmail(self): """ test to create a POP3 account accepting email object """ tracker = self.folder.tracker account = tracker.createPOP3Account('mail.example.com', 'peter', 'secret') # there are unfortunately two different ways to create accepting email # objects. Either directly on the account or via the issuetracker # itself. With the latter option, you have to pass the ID of the # account. # The latter one is more user friendly and easier to "access" because # that's what the DTML files do. ae = tracker.createAcceptingEmail(account.getId(), 'mail@example.com') self.assertEqual(ae.aq_parent.absolute_url(), account.absolute_url()) self.assertEqual(ae.getEmailAddress(), 'mail@example.com') self.assertFalse(ae.doSendConfirm()) self.assertFalse(ae.revealIssueURL()) ## try to mess with it # invalid email address self.assertRaises(ValueError, tracker.createAcceptingEmail, account.getId(), 'mail @ example.com') # non-existant pop3 account self.assertRaises(AttributeError, tracker.createAcceptingEmail, 'zxy', 'mail@example.com') ## test to edit the accepting email object ae.editDetails(email_address='www@example.com', send_confirm=True, reveal_issue_url=True) self.assertEqual(ae.getEmailAddress(), 'www@example.com') self.assertTrue(ae.doSendConfirm()) self.assertTrue(ae.revealIssueURL()) def test_acceptingEmailMatch(self): """ try creating a accepting email address and add some whitelist and blacklist email addresses. """ tracker = self.folder.tracker account = tracker.createPOP3Account('mail.example.com', 'peter', 'secret') ae = tracker.createAcceptingEmail(account.getId(), 'mail@example.com') # just blacklist ae.editDetails(blacklist_emails=['test@peterbe.com']) self.assertFalse(ae.acceptOriginatorEmail('TEST@peterbe.COM')) self.assertTrue(ae.acceptOriginatorEmail('TEST.ElSE@peterbe.COM')) ae.editDetails(blacklist_emails=['*@peterbe.com'], whitelist_emails=['exception@peterbe.com']) self.assertFalse(ae.acceptOriginatorEmail('TEST@peterbe.COM')) self.assertTrue(ae.acceptOriginatorEmail('eXception@peterbe.COM')) from poplib import POP3, error_proto class FakePOP3(POP3): username = 'test' password = 'test' files = [] def __init__(self, hostname, port=110): self.hostname = hostname self.port = port def getwelcome(self): return "Welcome to fake account" def user(self, user): if user != self.username: raise error_proto("Wrong username.") def pass_(self, pswd): if pswd != self.password: raise error_proto("Wrong password.") def list(self, which=None): # eg. ('+OK 4 messages:', ['1 71017', '2 2201', '3 7723', '4 44152'], 34) files = self.files responses = [] for i, f in enumerate(files): responses.append('%s %s' % (i+1, os.stat(f)[stat.ST_SIZE])) return ('+OK %s messages:' % len(files), responses, None) def retr(self, which): # ['response', ['line', ...], octets] filename = self.files[which-1] return ('response', open(filename, 'r').xreadlines(), None) def quit(self): pass class EmailInTestCase(TestBase): """ Here we'll try to actually send some emails in to the issuetracker by setting up a fake POP3 server. """ def test_emailIn1(self): """ test a very basic email in """ tracker = self.folder.tracker u, p = 'test', 'test' # doesn't really matter account = tracker.createPOP3Account('mail.example.com', u, p) email = 'mail@example.com' ae = tracker.createAcceptingEmail(account.getId(), email) abs_path = lambda x: os.path.join(os.path.dirname(__file__), x) FakePOP3.files = [abs_path('email-in-1.email')] # Monkey patch! from Products.IssueTrackerProduct import IssueTracker IssueTracker.POP3 = FakePOP3 result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 1 issue') > -1) # this should have created an issue self.assertEqual(len(tracker.getIssueObjects()), 1) def test_emailIn2(self): """ test rejecting an email based on from address """ tracker = self.folder.tracker u, p = 'test', 'test' # doesn't really matter account = tracker.createPOP3Account('mail.example.com', u, p) email = 'mail@example.com' ae = tracker.createAcceptingEmail(account.getId(), email) ae.editDetails(blacklist_emails=['*@peterbe.com'], whitelist_emails=['special@peterbe.com']) abs_path = lambda x: os.path.join(os.path.dirname(__file__), x) # this sends from mail@peterbe.com and another one from special@peterbe.com FakePOP3.files = [abs_path('email-in-1.email'), abs_path('email-in-2.email')] # Monkey patch! from Products.IssueTrackerProduct import IssueTracker IssueTracker.POP3 = FakePOP3 result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 1 issue') > -1) # this should have created an issue self.assertEqual(len(tracker.getIssueObjects()), 1) def test_emailIn3(self): """ test setting section, urgency and type by the subject line """ tracker = self.folder.tracker u, p = 'test', 'test' # doesn't really matter account = tracker.createPOP3Account('mail.example.com', u, p) email = 'mail@example.com' ae = tracker.createAcceptingEmail(account.getId(), email) abs_path = lambda x: os.path.join(os.path.dirname(__file__), x) # this sends from mail@peterbe.com and another one from special@peterbe.com FakePOP3.files = [abs_path('email-in-3.email'),] # Monkey patch! from Products.IssueTrackerProduct import IssueTracker IssueTracker.POP3 = FakePOP3 result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 1 issue') > -1) issue = tracker.getIssueObjects()[0] self.assertEqual(issue.getSections(), ['General']) self.assertEqual(issue.getUrgency(), 'critical') self.assertEqual(issue.getType(), 'bug report') self.assertEqual(issue.getTitle(), 'NonExistant: There is a problem') def test_emailIn4(self): """ Test emails CCed in """ tracker = self.folder.tracker u, p = 'test', 'test' # doesn't really matter account = tracker.createPOP3Account('mail.example.com', u, p) email = 'mail@example.com' ae = tracker.createAcceptingEmail(account.getId(), email) abs_path = lambda x: os.path.join(os.path.dirname(__file__), x) # 'email-in-4.email' sends to peter@example.com but is CCed to # mail@example.com FakePOP3.files = [abs_path('email-in-4.email'),] # Monkey patch! from Products.IssueTrackerProduct import IssueTracker IssueTracker.POP3 = FakePOP3 result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 1 issue') > -1) # this should have created an issue self.assertEqual(len(tracker.getIssueObjects()), 1) def test_emailIn5(self): """ Test emails in multipart """ tracker = self.folder.tracker self._send_in_emails('email-in-5.email') result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 1 issue') > -1) issue = tracker.getIssueObjects()[0] self.assertTrue(isinstance(issue.getTitle(), unicode)) # XXX I'm not sure what this test is meant to test. # Perhaps the functionality of emailing in HTML emails should # change to show HTML safely. def test_emailIn_none(self): """ Test emails in multipart """ tracker = self.folder.tracker self._send_in_emails([]) result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 0 issue') > -1) issues = len(tracker.getIssueObjects()) self.assertEqual(issues, 0) def test_emailInMultipart(self): """ when emailing in an email with multipart text and html, favor the text part. """ tracker = self.folder.tracker self._send_in_emails('multipart-email-with-ms.email', accepting_email='ims@issuetrackerproduct.com') result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 1 issue') > -1) issue = tracker.getIssueObjects()[0] description = issue.description self.assertTrue('THIS IS THE PLAIN PART' in description) display_format = issue.display_format self.assertEqual(display_format, 'plaintext') def _send_in_emails(self, email_filenames, accepting_email='mail@example.com'): tracker = self.folder.tracker u, p = 'test', 'test' # doesn't really matter account = tracker.createPOP3Account('mail.example.com', u, p) email = accepting_email ae = tracker.createAcceptingEmail(account.getId(), email) abs_path = lambda x: os.path.join(os.path.dirname(__file__), x) # 'email-in-5.email' is multipart/alternative and the HTML part of it # is not quoted. if not isinstance(email_filenames, (tuple, list)): email_filenames = [email_filenames] FakePOP3.files = [abs_path(x) for x in email_filenames] # Monkey patch! from Products.IssueTrackerProduct import IssueTracker IssueTracker.POP3 = FakePOP3 def test_jp_regression_tests(self): """ these tests come from a bug report by Jesse. """ tracker = self.folder.tracker u, p = 'test', 'test' # doesn't really matter account = tracker.createPOP3Account('mail.example.com', u, p) email = 'helpdesktest@example3.org' ae = tracker.createAcceptingEmail(account.getId(), email) abs_path = lambda x: os.path.join(os.path.dirname(__file__), x) # 'email-in-5.email' is multipart/alternative and the HTML part of it # is not quoted. FakePOP3.files = [abs_path('jp-0.email'), abs_path('jp-1.email')] # Monkey patch! from Products.IssueTrackerProduct import IssueTracker IssueTracker.POP3 = FakePOP3 result = tracker.check4MailIssues(verbose=True) self.assertTrue(result.find('Created 2 issues') > -1) def test_suite(): from unittest import TestSuite, makeSuite suite = TestSuite() suite.addTest(makeSuite(POP3TestCase)) suite.addTest(makeSuite(EmailInTestCase)) return suite if __name__ == '__main__': framework() IssueTrackerProduct/tests/testUtils.py0000644000175000017500000003045711012074371020402 0ustar peterbepeterbeimport unittest import sys, os if __name__ == '__main__': execfile(os.path.join(sys.path[0], 'framework.py')) from Products.IssueTrackerProduct import Utils from DateTime import DateTime class IssueLinkFinderTestCase(unittest.TestCase): def _find(self, text, zfill, trackerid=None, prefix=None): c = Utils.getFindIssueLinksRegex(zfill, trackerid, prefix) return c.findall(text) def testBasic(self): t='This is #1234 issue' self.assertEqual(self._find(t, 4), ['#1234']) def testBasicMultiple(self): t='This is #1234 issues #5421 test' self.assertEqual(self._find(t, 4), ['#1234', '#5421']) def testWithTrackerid(self): t='Remember Real#1234? or #9876 but not Demo#2468 or then:#1235' self.assertEqual(self._find(t, 4, 'real'), ['Real#1234', '#9876','#1235']) def testWithTrackerid2(self): t='Remember Real#1234? or #9876 but not Demo#2468 or then:#1235' self.assertEqual(self._find(t, 4, 'DEMO'), ['#9876','Demo#2468','#1235']) def testPrefixed(self): t='prefixed with 000- is #000-0103 but not Real#000-0104' self.assertEqual(self._find(t, 4, prefix='000-'), ['#000-0103']) def testPrefixedWithTrackerid(self): t='prefixed with 000- is #000-0103 but not Real#000-0104' self.assertEqual(self._find(t, 4, 'real', prefix='000-'), ['#000-0103','Real#000-0104']) def testWithBracketsNoTrackerid(self): t='In brackets (#1234) with tracker id demo like (demo#0987)' self.assertEqual(self._find(t, 4), ['#1234']) def testWithBracketsWithTrackerid(self): t='In brackets (#1234) with tracker id demo like (demo#0987)' self.assertEqual(self._find(t, 4, 'DeMo'), ['#1234','demo#0987']) def testLinebroken(self): t = 'First there is #00000\nThen there is #99999' self.assertEqual(self._find(t, 5, ''), ['#00000', '#99999']) def testStringstart(self): t = '#111 or (#113) or (Real#115) but not (#1122) or (Real#8888) or (OReal#116)' self.assertEqual(self._find(t, 3, 'real'), ['#111','#113','Real#115']) def testWithTrackeridS(self): t='Theres Real#1234 and theres Demo#1235' self.assertEqual(self._find(t, 4, ('DeMo','real')), ['Real#1234', 'Demo#1235']) class TimeSinceTestCase(unittest.TestCase): def testYears1A(self): t1 = DateTime('2005/01/01') # 2004 was a leap year t2 = DateTime('2003/01/01') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 years and 1 day") def testYears1B(self): t1 = DateTime('2004/01/01') t2 = DateTime('2002/01/01') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 years") def testYears2A(self): t1 = DateTime('2005/01/01') # 2004 was a leap year t2 = DateTime('2003/01/02') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 years") def testYears2B(self): t1 = DateTime('2004/01/02') t2 = DateTime('2002/01/01') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 years and 1 day") def testYears3A(self): t1 = DateTime('2005/02/01') # 2004 was a leap year t2 = DateTime('2003/01/01') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 years and 1 month and 2 days") def testYears3B(self): t1 = DateTime('2004/02/01') # 2004 was a leap year t2 = DateTime('2002/01/01') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 years and 1 month and 1 day") def testMonths(self): t1 = DateTime('2005/04/01') t2 = DateTime('2005/05/01') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "1 month") def testDays1(self): t1 = DateTime('2005/04/01') t2 = DateTime('2005/04/02') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "1 day") def testDays2(self): t1 = DateTime('2005/04/01') t2 = DateTime('2005/04/03') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 days") def testHours1(self): t1 = DateTime('2005/04/01 12:00') t2 = DateTime('2005/04/01 13:00') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "1 hour") def testHours2(self): t1 = DateTime('2005/04/01 12:00') t2 = DateTime('2005/04/01 14:00') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 hours") def testHours3(self): # the timeSince() function drops the hour # part if the difference is in days t1 = DateTime('2005/04/01 12:00') t2 = DateTime('2005/04/02 14:00') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "1 day") def testMinutes1(self): # the timeSince() function drops the hour # part if the difference is in days t1 = DateTime('2005/04/01 12:00') t2 = DateTime('2005/04/01 12:30') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, 0) def testMinutes2(self): # the timeSince() function drops the hour # part if the difference is in days t1 = DateTime('2005/04/01 12:00') t2 = DateTime('2005/04/01 12:30') difference = Utils.timeSince(t1, t2, minute_granularity=1) self.assertEqual(difference, "30 minutes") def testMinutes2(self): # the timeSince() function drops the hour # part if the difference is in days t1 = DateTime('2005/04/01 12:00') t2 = DateTime('2005/04/01 12:01') difference = Utils.timeSince(t1, t2, minute_granularity=1) self.assertEqual(difference, "1 minute") def testWeek1(self): # use the week notation t1 = DateTime('2005/04/01') t2 = DateTime('2005/04/08') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "1 week") def testWeek2(self): # use the week notation t1 = DateTime('2005/04/01') t2 = DateTime('2005/04/15') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 weeks") def testWeek2point1(self): # use the week notation t1 = DateTime('2005/04/01') t2 = DateTime('2005/04/16') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "2 weeks and 1 day") def testWeek3(self): # use the week notation t1 = DateTime('2005/04/01') t2 = DateTime('2005/04/22') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, "3 weeks") def test2Weeks(self): # use the week notation t1 = DateTime('2005/12/08 15:54:18.715 GMT') t2 = DateTime('2006/01/06 10:26:18.571 GMT') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, '4 weeks') def test2Weeks(self): # use the week notation t1 = DateTime('2005/11/12 00:08:30.937 GMT') t2 = DateTime('2006/01/09 09:47:07.123 GMT') difference = Utils.timeSince(t1, t2) self.assertEqual(difference, '1 month and 4 weeks') def testLongDistanceDates(self): # If the result contains a year part, month part, week part # and day part the number of parts is controlled by # @max_no_sections (default 3) t1 = DateTime('2004/11/12 00:08:30.937 GMT') t2 = DateTime('2006/05/01 09:47:07.123 GMT') difference_default = Utils.timeSince(t1, t2) self.assertEqual(difference_default, '1 year and 5 months and 2 weeks') difference_2_parts = Utils.timeSince(t1, t2, max_no_sections=2) self.assertEqual(difference_2_parts, '1 year and 5 months') difference_99_parts = Utils.timeSince(t1, t2, max_no_sections=99) self.assertEqual(difference_99_parts, '1 year and 5 months and 2 weeks and 6 days') class SplitTermsTestCase(unittest.TestCase): def testBasic1(self): inp = "peter anders bengt" exp = ['peter','anders','bengt'] self.assertEqual(Utils.splitTerms(inp), exp) def testUnbalanced(self): inp = 'peter "bengt anders bengtsson' exp = ['peter','"bengt','anders','bengtsson'] self.assertEqual(Utils.splitTerms(inp), exp) def testOneQuote(self): inp = 'peter "bengt anders" bengtsson' exp = ['peter','"bengt anders"','bengtsson'] self.assertEqual(Utils.splitTerms(inp), exp) def testTwoQuotes(self): inp = 'peter "bengt anders" bengtsson "and again" peter' exp = ['peter','"bengt anders"','bengtsson', '"and again"','peter'] self.assertEqual(Utils.splitTerms(inp), exp) def testEndingQuote(self): inp = 'peter "bengt anders"' exp = ['peter','"bengt anders"'] self.assertEqual(Utils.splitTerms(inp), exp) def testStartingQuote(self): inp = '"bengt anders" bengtsson' exp = ['"bengt anders"','bengtsson'] self.assertEqual(Utils.splitTerms(inp), exp) class AddParam2URLTestCase(unittest.TestCase): def testBasic(self): inpu, inpp = "http://www.com", {'msg':'foo bar'} exp = "http://www.com?msg=foo%20bar" self.assertEqual(Utils.AddParam2URL(inpu, inpp), exp) def testBasicPlus(self): inpu, inpp = "http://www.com", {'msg':'foo bar'} exp = "http://www.com?msg=foo+bar" self.assertEqual(Utils.AddParam2URL(inpu, inpp, plus_quote=1), exp) class StandaloneWordRegex(unittest.TestCase): def _find(self, word, text): return Utils.createStandaloneWordRegex(word).findall(text) def testOneAgainstOne(self): word = "peter" text = "Peter" exp = ['Peter'] self.assertEqual(self._find(word, text), exp) def testOneAgainstSentence(self): word = "peter" text = "Peter Bengtsson" exp = ['Peter'] self.assertEqual(self._find(word, text), exp) def testSentenceAgainstOne(self): word = "Peter Bengtsson" text = "peter" exp = [] self.assertEqual(self._find(word, text), exp) def testSentenceAgainstSentenceI(self): word = "Peter Bengtsson" text = "Peter Bengtsson" exp = [text] self.assertEqual(self._find(word, text), exp) def testSentenceAgainstSentenceII(self): word = "PETER BENGTSSON" text = "peter bengtsson" exp = ['peter bengtsson'] self.assertEqual(self._find(word, text), exp) def testFred(self): word = 'Fred Damberger' text = 'Fred Damberger' exp = [text] self.assertEqual(self._find(word, text), exp) class ValidEmailAddressTestCase(unittest.TestCase): def testPositives(self): T = lambda email: self.assertEqual(Utils.ValidEmailAddress(email), True) T('peter@fry-it.com') T(u'peter@fry-it.com') T('peter+julika@peterbe.com') T("peter'julika@peterbe.com") T('o.k@foo.com') T('ok@911.com') def testNegatives(self): F = lambda email: self.assertEqual(Utils.ValidEmailAddress(email), False) F('ASD@GHJG.7') F('peter @fry-it.com') F('invalid@foo.c0m') F(u'invalid@foo.c0m') F('invalid@foo.b@r.com') F('invalid@f#o.com') F('invalid.@foo.com') F('invalid@foo.') def test_suite(): from unittest import TestSuite, makeSuite suite = TestSuite() suite.addTest(makeSuite(IssueLinkFinderTestCase)) suite.addTest(makeSuite(TimeSinceTestCase)) suite.addTest(makeSuite(SplitTermsTestCase)) suite.addTest(makeSuite(AddParam2URLTestCase)) suite.addTest(makeSuite(StandaloneWordRegex)) suite.addTest(makeSuite(ValidEmailAddressTestCase)) return suite if __name__ == '__main__': framework() IssueTrackerProduct/tests/size0_file.jpg0000644000175000017500000000000011012074371020540 0ustar peterbepeterbeIssueTrackerProduct/tests/email-in-3.email0000644000175000017500000000033411012074371020663 0ustar peterbepeterbeDate: Fri, 8 Feb 2008 18:03:56 +0000 From: Mr Exception To: mail@example.com Sender: special@peterbe.com Subject: General, NonExistant, critical, bug report: There is a problem This is the body IssueTrackerProduct/tests/README.txt0000644000175000017500000000157111012074371017521 0ustar peterbepeterbeYou might want to set this so you don't try to send out emails: export DEBUG_ISSUETRACKERPRODUCT=1 You might want to disable CheckoutableTemplates: export DISABLE_CHECKOUTABLE_TEMPLATES=1 To run all IssueTracker tests ./bin/zopectl test --dir Products/IssueTrackerProduct/tests/ To just run testIssueTracker.py ./bin/zopectl test --dir Products/IssueTrackerProduct/tests/ --tests-pattern=testIssueTracker To just run testIssueTracker.py but only test called test_debatingIssue() ./bin/zopectl test --dir Products/IssueTrackerProduct/tests/ --tests-pattern=testIssueTracker --test=test_debatingIssue Include --keepbytecode when the set of tests gets large. At the time of writing, this doesn't make any difference to the time it takes. If you're testing this in Zope 2.8 you'll have to use this syntax instead: ./bin/zopectl test --dir Products/IssueTrackerProduct/tests testEmailInIssueTrackerProduct/tests/multipart-email-with-ms.email0000644000175000017500000002404311012074371023527 0ustar peterbepeterbeReturn-Path: Received: from mail.fry-it.com ([unix socket]) by mail1 (Cyrus v2.1.18-IPv6-Debian-2.1.18-5.1) with LMTP; Fri, 11 Apr 2008 17:18:44 +0100 X-Sieve: CMU Sieve 2.2 Received: from cluster-e.mailcontrol.com (cluster-e.mailcontrol.com [217.79.216.190]) by mail.fry-it.com (Postfix) with ESMTP id A408560C1F6 for ; Fri, 11 Apr 2008 17:18:44 +0100 (BST) Received: from mail.example.com (smtp.example.com [91.84.144.218]) by rly53e.srv.mailcontrol.com (MailControl) with ESMTP id m3BGIHt3002843 for ; Fri, 11 Apr 2008 17:18:25 +0100 X-MimeOLE: Produced By Microsoft Exchange V6.5 Content-class: urn:content-classes:message MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----_=_NextPart_001_01C89BEF.A6221966" Subject: New User - Steve Ballmer Date: Fri, 11 Apr 2008 17:18:16 +0100 Message-ID: <0325D0008198874B9D36597E50A2B7A86A8D9A@imsexch01.example.local> X-MS-Has-Attach: X-MS-TNEF-Correlator: Thread-Topic: New User - Steve Ballmer Thread-Index: Acib76X0hvJbINk7QgmApabvT3qHQA== From: "Bill Gates" To: Cc: "Steve Ballmer" X-Scanned-By: MailControl A-08-00-04 (www.mailcontrol.com) on 10.69.0.163 This is a multi-part message in MIME format. ------_=_NextPart_001_01C89BEF.A6221966 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: quoted-printable Hi You =20 THIS IS THE PLAIN PART =20 Bill=20 =20 Bill Gates =20 =20 Bla bla bla =20 Signature =20 ------_=_NextPart_001_01C89BEF.A6221966 Content-Type: text/html; charset="us-ascii" Content-Transfer-Encoding: quoted-printable

Hi You

 

 

Many thanks and have a great = weekend.

 

Bill

 

Bill = Gates

Senior title

 

 

some | special = unit selection

 

77 Queen Victoria Street, London XXX = XXX

 

Website www.example.com

 =

Investment Manager Selection Limited = and MM Bla bla bla = Services Authority

 

signature England no. 3104985.  MM Asset Management Limited is registered in = England no. 3834095.  Telephone calls may be = recorded.

 

------_=_NextPart_001_01C89BEF.A6221966--IssueTrackerProduct/tests/email-in-5.email0000644000175000017500000000301611012074371020665 0ustar peterbepeterbeFrom - Sat Mar 15 19:18:05 2008 Delivered-To: peterbe@gmail.com Date: Fri, 14 Mar 2008 10:38:53 -0700 Message-Id: <200803141738.m2EHcrWm030967@hugeapi.sav.sea.dotster.net> From: support@mydomain.com To: mail@example.com Content-Type: multipart/alternative; boundary=----_NextPart_000_002C_01BFABBF.4A7D6BA0 MIME-Version: 1.0 Subject: A multipart alternative problem in utf8 ------_NextPart_000_002C_01BFABBF.4A7D6BA0 Content-Type: text/plain; charset="utf-8" THIS IS THE HTML PART ------_NextPart_000_002C_01BFABBF.4A7D6BA0 Content-Type: text/html; charset="utf-8" Title of HTML PART
THIS IS THE HTML EMAIL

 

 


------_NextPart_000_002C_01BFABBF.4A7D6BA0-- IssueTrackerProduct/Notification.py0000644000175000017500000001150511012074372017660 0ustar peterbepeterbe# IssueTrackerProduct # # Peter Bengtsson # License: ZPL # # python # Zope from Globals import InitializeClass from OFS import SimpleItem from AccessControl import ClassSecurityInfo from zLOG import LOG, ERROR, INFO, PROBLEM, WARNING from DateTime import DateTime from Acquisition import aq_inner, aq_parent # Product from IssueTracker import IssueTracker, base_hasattr from Constants import * import Utils #---------------------------------------------------------------------------- class IssueTrackerNotification(SimpleItem.SimpleItem, IssueTracker ): """ Issue Tracker Notification """ meta_type = NOTIFICATION_META_TYPE icon = '%s/notification.gif'%ICON_LOCATION security=ClassSecurityInfo() _properties=({'id':'title', 'type': 'ustring', 'mode':'w'}, {'id':'change', 'type': 'ustring', 'mode':'w'}, {'id':'issueID', 'type': 'string', 'mode':'w'}, {'id':'comment', 'type': 'utext', 'mode':'w'}, {'id':'emails', 'type': 'lines', 'mode':'w'}, {'id':'success_emails','type':'lines', 'mode':'w'}, {'id':'anchorname', 'type': 'string', 'mode':'w'}, {'id':'assignment', 'type': 'string', 'mode':'w'}, {'id':'fromname', 'type': 'ustring', 'mode':'w'}, {'id':'date', 'type': 'date', 'mode':'w'}, {'id':'dispatched', 'type': 'boolean', 'mode':'w'}, ) manage_options = ( {'label':'Properties', 'action':'manage_propertiesForm'}, ) # legacy: attributes that have been added later assignment = '' def __init__(self, id, title, issueID, emails, fromname=u'', comment=u'', date=None, dispatched=False, anchorname='', change=u'', assignment='', REQUEST=None, **extra_headers): """ create notification """ self.id = str(id) self.title = title self.change = change self.issueID = str(issueID) self.comment = comment self.emails = emails self.success_emails = [] self.anchorname = anchorname self.assignment = assignment self.fromname = fromname self.dispatched = dispatched if date is None: date = DateTime() self.date = date self.extra_headers = extra_headers def getTitle(self): """ return title of the notification """ return self.title def getIssueID(self): """ return issueID of the notification """ return self.issueID def getAssignmentObject(self): """ return the assignment object this notification was about or return None """ if self.assignment: object_id = self.assignment # expect the assignment object to be located in the same # container as this notification parent = aq_parent(aq_inner(self)) if base_hasattr(parent, object_id): obj = getattr(parent, object_id) if obj.meta_type == ISSUEASSIGNMENT_METATYPE: return obj return None def isDispatched(self): """ return dispatched """ return not not self.dispatched def dispatch(self): """ dispatch self """ self.dispatcher([self]) def setSuccessEmail(self, email): """ set an email that was a success to send to. The email must exist in self.emails and not already a success """ emails = self.getEmails() success_emails = self.getSuccessEmails() if Utils.ss(email) in [Utils.ss(each) for each in emails]: if Utils.ss(email) not in [Utils.ss(each) for each in success_emails]: success_emails.append(email) self.success_emails = success_emails def getEmails(self): """ return the list of emails to send to """ return self.emails def _setEmails(self, emails): self.emails = emails def getSuccessEmails(self): """ return list of emails that have successfully been sent to """ # due to legacy, if the success_emails attribute doesn't exist, # it must be because this self is an object that was created # _before_ the 'success_emails' attribute was introduced. # If that's the case, return 'emails' instead return getattr(self, 'success_emails', self.getEmails()) def MarkNotificationDispatch(self): """ set as dispatched """ self.dispatched = True InitializeClass(IssueTrackerNotification) IssueTrackerProduct/Thread.py0000644000175000017500000004457111012074372016452 0ustar peterbepeterbe# IssueTrackerProduct # www.issuetrackerproduct.com # # Peter Bengtsson # License: ZPL # # python import sys # Zope from Globals import InitializeClass, DTMLFile from AccessControl import ClassSecurityInfo from zLOG import LOG, ERROR, INFO, PROBLEM, WARNING from DateTime import DateTime # Is CMF installed? try: from Products.CMFCore.utils import getToolByName as CMF_getToolByName except ImportError: CMF_getToolByName = None # Product from Issue import IssueTrackerIssue from TemplateAdder import addTemplates2Class import Utils from Utils import unicodify from Constants import * from Permissions import VMS from I18N import _ #---------------------------------------------------------------------------- manage_addIssueTrackerIssueThreadForm = DTMLFile('dtml/NotImplemented', globals()) def manage_addIssueTrackerIssueThread(*args, **kw): """ This is not supported """ raise NotImplementedError, "This method should not be used" #---------------------------------------------------------------------------- class IssueTrackerIssueThread(IssueTrackerIssue): """ Issuethreads class """ meta_type = ISSUETHREAD_METATYPE icon = '%s/issuethread.gif'%ICON_LOCATION _properties=({'id':'title', 'type': 'ustring', 'mode':'w'}, {'id':'comment', 'type': 'utext', 'mode':'w'}, {'id':'threaddate', 'type': 'date', 'mode':'w'}, {'id':'fromname', 'type': 'ustring', 'mode':'w'}, {'id':'email', 'type': 'string', 'mode':'w'}, {'id':'acl_adder', 'type': 'string', 'mode':'w'}, {'id':'display_format','type': 'string', 'mode':'w'}, ) security=ClassSecurityInfo() manage_options = ( {'label':'Properties', 'action':'manage_propertiesForm'}, {'label':'Contents', 'action':'manage_main'}, ) acl_adder = '' # backward compatability def __init__(self, id, title, comment, threaddate, fromname, email, display_format=None, acl_adder='', submission_type=''): """ create thread """ self.id = str(id) self.title = unicodify(title) self.comment = unicodify(comment) if isinstance(threaddate, basestring): threaddate = DateTime(threaddate) self.threaddate = threaddate self.fromname = unicodify(fromname) self.email = email if display_format: self.display_format = display_format else: self.display_format = self.default_display_format if acl_adder is None: acl_adder = '' self.acl_adder = acl_adder self.submission_type = submission_type self.email_message_id = None def getTitle(self): """ return title """ return self.title def getThreadDate(self): """ return threaddate """ return self.threaddate def getModifyDate(self): return self.bobobase_modification_time() def getFromname(self, issueusercheck=True): """ return fromname """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # the userfolder (as it was saved) no longer exists return self.fromname if uf.meta_type == ISSUEUSERFOLDER_METATYPE: if uf.data.has_key(name): issueuserobj = uf.data[name] return issueuserobj.getFullname() or self.fromname elif CMF_getToolByName and hasattr(uf, 'portal_membership'): mtool = CMF_getToolByName(self, 'portal_membership') member = mtool.getMemberById(name) if member.getProperty('fullname'): return member.getProperty('fullname') return self.fromname def getEmail(self, issueusercheck=True): """ return email """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # the userfolder (as it was saved) no longer exists return self.email if uf.meta_type == ISSUEUSERFOLDER_METATYPE: if uf.data.has_key(name): issueuserobj = uf.data[name] return issueuserobj.getEmail() or self.email elif CMF_getToolByName and hasattr(uf, 'portal_membership'): mtool = CMF_getToolByName(self, 'portal_membership') member = mtool.getMemberById(name) if member.getProperty('email'): return member.getProperty('email') return self.email def getACLAdder(self): """ return acl_adder """ return self.acl_adder def _setACLAdder(self, acl_adder): """ set acl_adder """ self.acl_adder = acl_adder def getComment(self): """ return comment """ return self.comment def getCommentPure(self): """ return comment purified. If the comment contains HTML for example, remove it.""" comment = self.getComment() if self.getDisplayFormat() =='html': # textify() coverts "Something" to "Something". Simple. comment = Utils.textify(comment) # a very common thing is that the description contains # these faux double linebreaks and when you run textify() # on '

 

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

 

','') return comment def _unicode_comment(self): """ make the comment of this thread a unicode string """ self.comment = unicodify(self.comment) self._prerendered_comment = unicodify(self._prerendered_comment) def _prerender_comment(self): """ Run the methods that pre-renders the comment of the issue. """ comment = self.getComment() display_format = self.display_format formatted = self.ShowDescription(comment+' ', display_format) if self.getSubmissionType()=='email': formatted = Utils.highlight_signature(formatted, 'class="sig"') formatted = self._findIssueLinks(formatted) self._prerendered_comment = formatted def _getFormattedComment(self): """ return the comment formatted """ if getattr(self, '_prerendered_comment', None): formatted = self._prerendered_comment else: comment = self.getComment() display_format = self.display_format formatted = self.ShowDescription(comment+' ', display_format) return formatted def showComment(self): """ combine ShowDescription (which is generic) with this threads display format.""" formatted = self._getFormattedComment() return self.HighlightQ(formatted) def getSubmissionType(self): """ return how it was submitted, empty string if not found """ return getattr(self, 'submission_type', '') def getEmailMessageId(self): """ if the email was submitted via email it will most likely have a message id """ # important to use the aq_base because otherwise we might pick it up # from the parenting issue base = getattr(self, 'aq_base', self) return getattr(base, 'email_message_id', None) def _setEmailMessageId(self, message_id): """ set the email message id """ assert message_id.strip(), "Message_id not valid" self.email_message_id = message_id.strip() def _setEmailOriginal(self, original_email): """ set the original_email attribute """ self.original_email = original_email def hasEmailOriginal(self): """ return if we have a 'original_email' attribute set """ return hasattr(self, 'original_email') def ShowOriginalEmail(self, REQUEST): """ return the original email text """ if REQUEST: REQUEST.RESPONSE.setHeader('Content-Type','text/plain') return self.original_email def index_object(self, idxs=['comment','meta_type','fromname','email']): """A common method to allow Findables to index themselves.""" path = '/'.join(self.getPhysicalPath()) catalog = self.getCatalog() # because the ZCatalog might not yet have the # 'filenames' KeywordIndex we can't catalog this object # with that index. # Performing the following check every time takes # time so by 2007 this whole if statement below can probably # be removed because by then, must people will have updated # their issuetrackers to enable the new 'filenames' # KeywordIndex indexes = catalog._catalog.indexes if 'filenames' not in idxs and indexes.has_key('filenames'): idxs.append('filenames') catalog.catalog_object(self, path, idxs=idxs) def getFromname_idx(self): return self.getFromname() def getComment_idx(self): return self.getComment() def unindex_object(self): """A common method to allow Findables to unindex themselves.""" self.getCatalog().uncatalog_object('/'.join(self.getPhysicalPath())) security.declareProtected(VMS, 'manage_editProperties') def manage_editProperties(self, REQUEST): """ re-prerender the description of the issue after manual change """ result = IssueTrackerIssue.manage_editProperties(self, REQUEST) try: self._prerender_comment() except: if DEBUG: raise else: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Unable to _prerender_comment() in manage_editProperties()", error=sys.exc_info()) return result def manage_afterAdd(self, REQUEST, RESPONSE): """ intercept so that we prerender always """ try: self._prerender_comment() except: if DEBUG: raise else: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Unable to _prerender_comment() after add", error=sys.exc_info()) security.declareProtected(VMS, 'assertAllProperties') def assertAllProperties(self): """ make sure issue has all properties """ props = { # currently nothing } count = 0 for key, default in props.items(): if not self.__dict__.has_key(key): self.__dict__[key] = default count += 1 # check that self.fromname is as good as self.getFromname() attr_fromname = self.getFromname(issueusercheck=False) linked_fromname = self.getFromname(issueusercheck=True) if linked_fromname != attr_fromname: # for sanity, check that the linked fromname is ok if linked_fromname: self.fromname = linked_fromname count += 1 # check that self.email is as good as self.getFromname() attr_email = self.getEmail(issueusercheck=False) linked_email = self.getEmail(issueusercheck=True) if linked_email != attr_email: # for sanity, check that the linked email is ok if linked_email: self.email = linked_email count += 1 return count def showThreadFileattachments(self, only_temporary=0): """ wrap around the showFileattachments() method """ files = [] container = self.getFileattachmentContainer(only_temporary=only_temporary) return self.showFileattachments(container) InitializeClass(IssueTrackerIssueThread) #----------------------------------------------------------------------------- class IssueTrackerDraftIssueThread(IssueTrackerIssueThread): """ There are used for the 'Save as draft' feature when writing a followup on an issue. The major difference between these and IssueTrackerIssueThread is that these draft objects must not be indexed in the Catalog. """ meta_type = ISSUETHREAD_DRAFT_METATYPE icon = '%s/issuethreaddraft.gif'%ICON_LOCATION security=ClassSecurityInfo() manage_options = ( {'label':'Contents', 'action':'manage_main'}, {'label':'Properties', 'action':'manage_draftthread_properties'}, ) def __init__(self, id, issueid, action, title=None, comment=None, threaddate=None, fromname=None, email=None, display_format=None, acl_adder=None, is_autosave=False): """ create draft thread """ self.id = str(id) self.issueid = issueid self.action = unicodify(action) self.title = unicodify(title) self.comment = unicodify(comment) if isinstance(threaddate, basestring): threaddate = DateTime(threaddate) self.threaddate = threaddate self.fromname = unicodify(fromname) self.email = email self.display_format = display_format self.is_autosave = bool(is_autosave) if not acl_adder: # '', 0 or None acl_adder = '' self.acl_adder = acl_adder # legacy support is_autosave = False def getIssueId(self): """ return issueid """ return self.issueid def getIssuePath(self): """ return a relative URL to where the issue is """ rootpath = self._getIssueContainer().absolute_url_path() if rootpath == '/': return '/' + self.getIssueId() else: return rootpath + '/' + self.getIssueId() def getModifyDate(self): return self.bobobase_modification_time() def index_object(self, *args, **kws): """ do NOT index this object """ pass def unindex_object(self, *args, **kws): """ nothing to unindex """ pass def manage_afterAdd(self, REQUEST, RESPONSE): """ the base class defines this to prerender the comment, something we don't want to do. """ pass def ModifyThread(self, title=None, comment=None, display_format=None, fromname=None, email=None, acl_adder=None, is_autosave=False, REQUEST=None): """ since normal threads don't allow changes, we need to add this very custom method to the drafts """ if title is not None: self.title = title if comment is not None: self.comment = comment if display_format is not None: self.display_format = display_format if fromname is not None: self.fromname = fromname if email is not None: self.email = email if acl_adder is not None: self.acl_adder = acl_adder self.is_autosave = bool(is_autosave) def isAutosave(self): """ return if this was saved as an autosave or a plain draft """ return self.is_autosave def shortDescription(self, maxlength=55, html=True): """ return a simplified description where the title is shown and then as much of the description as possible. """ title = self.getTitle() if title is None: title = self.action if not title.strip(): if html: title = "(%s)" % _("No subject") else: title = "(%s)" % _("No subject") desc = self.getCommentPure() shortened = self.lengthLimit(title, maxlength, "|...|") if shortened.endswith('|...|'): # the title was shortened shortened = shortened[:-len('|...|')] if html: return "%s..."%shortened else: return shortened+'...' else: # i.e. title==shortened # put some of the description ontop if len(shortened) + len(desc) > maxlength: desc = self.lengthLimit(desc, maxlength-len(title)) if html: return u"%s, %s"%(shortened, desc) else: return u"%s, %s"%(shortened, desc) def get__dict__keys(self): """ return the names of the keys we might have """ return ('issueid', 'action', 'title', 'comment', 'fromname', 'email', 'display_format', 'acl_adder', 'is_autosave') def get__dict__nicely(self): """ same as get__dict__keys() but we wrap it nicely """ ok = [] for key in self.get__dict__keys(): if self.__dict__.get(key, None) is not None: ok.append({'key':key, 'value':self.__dict__.get(key)}) return ok dtmls = ({'f':'dtml/draftissuethread_properties', 'n':'manage_draftthread_properties'}, ) addTemplates2Class(IssueTrackerDraftIssueThread, dtmls, "dtml") InitializeClass(IssueTrackerDraftIssueThread) IssueTrackerProduct/IssueTrackerNotifyables.py0000644000175000017500000004356511012074372022051 0ustar peterbepeterbe# python from types import ListType # Zope from Products.PageTemplates.PageTemplateFile import PageTemplateFile from OFS import SimpleItem, Folder from AccessControl import ClassSecurityInfo from Globals import MessageDialog, InitializeClass, DTMLFile # Product import Utils from Constants import * class Notifyables: # Global container # def hasGlobalContainer(self): """ Check if a global container exists """ if hasattr(self, DEFAULT_NOTIFYABLECONTAINER_ID): return true else: return false def isGlobalHere(self): """ Check if we're "standing" in a global container """ if hasattr(self, 'notifyables'): return false else: return true def _getManagementFormURL(self, msg=None): """ return the URL to redirect to for returning to the appropriate interface. """ if self.isGlobalHere(): url = self.absolute_url() url += '/manage_GlobalManagementForm' else: url = self.absolute_url() url += '/manage_ManagementNotifyables' if msg is not None: params = {'manage_tabs_message':msg} url = Utils.AddParam2URL(url, params) return url def getGlobalContainer(self): """ Return the global_notifyables object """ return getattr(self, DEFAULT_NOTIFYABLECONTAINER_ID) def getManagementForm(self): """ Return correct management form depending on container """ if self.isGlobalHere(): return self.manage_GlobalManagementForm else: return self.manage_ManagementNotifyables #return self.manage_ManagementForm # Notifyables # def hasNotifyables(self): """ see if there are any notifyables at all """ if len(self.getNotifyables()) > 0: return 1 else: return 0 def getNotifyables(self, only=None): """ Return all Notifyable objects """ if only =='': only = None if only == 'global': local_container = None else: local_container = self.getNotifyablesObjectContainer(only='local') if only == 'local': global_container = None else: global_container = self.getNotifyablesObjectContainer(only='global') meta_type = NOTIFYABLE_METATYPE if local_container is None: local_notifyables = [] else: local_notifyables = local_container.objectValues(meta_type) if global_container: global_notifyables = global_container.objectValues(meta_type) return local_notifyables + global_notifyables else: return local_notifyables def getNotifyablesByGroup(self, group, only=None): """ return a list of all notiyables belonging to this group """ checked = [] for notifyable in self.getNotifyables(only=only): if notifyable.partofGroup(group): checked.append(notifyable) return checked def getNotifyablesEmailName(self, only=None): """ wrap getNotifyables and return dictionary """ email_name = {} for nf in self.getNotifyables(only=only): email_name[nf.getEmail()] = nf.getName() return email_name def manage_addNotifyables(self, REQUEST): """ Save notifyables (only via the web) """ new_emails = REQUEST.get('new_email',[]) no_created = 0 no_attempted = 0 for c in range(len(new_emails)): email = REQUEST['new_email'][c] alias = REQUEST['new_alias'][c] groups = REQUEST.get('new_groups',[]) if email != '': no_attempted += 1 if not isinstance(groups, list): groups = [groups] if Utils.ValidEmailAddress(email): self.manage_addNotifyable(email, alias, groups) no_created += 1 if no_created == no_attempted: if len(new_emails) > 1: mtm = "Notifyables created." else: mtm = "Notifyable created." else: if len(new_emails) > 1: mtm = """%s out of %s were created. Check your input data."""%(no_created, no_attempted) else: mtm = "Notifyable not created. Check your input data" form = self.getManagementForm() return form(REQUEST, manage_tabs_message=mtm) def manage_addNotifyable(self, email, alias='', groups=[], REQUEST=None): """ Create notifyable object """ email, alias = email.strip(), alias.strip() if Utils.ValidEmailAddress(email): container = self.getNotifyablesObjectContainer() id = self.GenerateNotifyableId(email) n = IssueTrackerNotifyable(id, alias, email, groups) container._setObject(id, n) if REQUEST is not None: mtm= "%s created."%NOTIFYABLE_METATYPE return MessageDialog(title=mtm, message=mtm, action='%s/manage_main' % REQUEST['URL1']) else: raise "InvalidEmailAddress", \ "Email address used (%s) was invalid"%email def manage_delNotifyables(self, REQUEST): """ Prepare which notifyable ids to remove """ ids = REQUEST.get('del_notify_ids',[]) container = self.getNotifyablesObjectContainer() container.manage_delObjects(ids) msg = "Notifyables deleted." url = self._getManagementFormURL(msg) REQUEST.RESPONSE.redirect(url) def _filterNotifyGroups(self, groups): """ Return the list groups but filter out groups that don't exists in getNotifyableGroups() """ existing_group_ids = [] for group in self.getNotifyableGroups(): existing_group_ids.append(group.getId()) n_groups=[] for group in groups: if group.strip() in existing_group_ids and \ group.strip() not in n_groups: n_groups.append(group.strip()) return n_groups # Notify groups # def hasOldProperty(self): """ Returns how many items in the old property """ if hasattr(self, 'notify_groups') and \ type(self.notify_groups)==ListType: return len(self.notify_groups) else: return 0 def convertOldGroups2Objects(self, REQUEST=None): """ If still having the old property, recreate as objects """ all_object_groups = self.getNotifyableGroupIds() if self.hasOldProperty(): for each in self.notify_groups: try: self.manage_addNotifyableGroup(each) except: pass self.notify_groups = [] if REQUEST is not None: form = self.getManagementForm() mtm = "Old property now updated for groups." return form(REQUEST, manage_tabs_message=mtm) def getGroupsByIds(self, ids): """ Return the objects of notifyable group ids """ objects = self.getNotifyableGroups() r_objects = [] for object in objects: if object.getId() in ids: r_objects.append(object) return r_objects def getNotifyableGroups(self, only=None): """ Get all notifyable groups """ if only =='': only = None if only=='global': local_container = None else: local_container = self.getNotifyablesObjectContainer(only='local') if only=='local': global_container = None else: global_container = self.getNotifyablesObjectContainer(only='global') meta_type = NOTIFYABLEGROUP_METATYPE if local_container is None: local_notifyables = [] else: local_notifyables = local_container.objectValues(meta_type) if global_container: global_notifyables = global_container.objectValues(meta_type) return local_notifyables + global_notifyables else: return local_notifyables def getNotifyableGroupIds(self): """ return the ids of all group objects """ ids=[] for object in self.getNotifyableGroups(): ids.append(object.getId()) return ids def manage_delNotifyGroups(self, notify_groups, REQUEST=None): """ delete some groups from self.notify_groups """ container = self.getNotifyablesObjectContainer() container.manage_delObjects(notify_groups) msg = 'Notifygroups deleted.' if REQUEST is not None: url = self._getManagementFormURL(msg) REQUEST.RESPONSE.redirect(url) else: return msg def manage_saveNotifyGroup(self, notify_group, REQUEST): """ add one group (via the web only) """ self.manage_addNotifyableGroup(notify_group) msg = "%s created."%NOTIFYABLEGROUP_METATYPE url = self._getManagementFormURL(msg) REQUEST.RESPONSE.redirect(url) def GenerateNotifyableId(self, email, length=10, int_length=None): """ generate a random id for a notifyable string """ email= email.replace('@','_at_') return email def createNotifyGroupId(self, group): """ generate a random id for a notifyable string """ group = Utils.safeId(group.strip()) group = group.replace(' ','_').replace('-','_').lower() return group def manage_addNotifyableGroup(self, notify_group, REQUEST=None): """ Create a notifyable group """ dest = self.getNotifyablesObjectContainer() id = self.createNotifyGroupId(notify_group) group = IssueTrackerNotifyableGroup(id, notify_group) dest._setObject(id, group) self = dest._getOb(id) if REQUEST is not None: mtm= "%s created."%NOTIFYABLEGROUP_METATYPE return MessageDialog(title=mtm, message=mtm, action='%s/manage_main' % REQUEST['URL1']) def getNotifyablesObjectContainer(self, only=None): """ Return the container where notifyables and groups""" # Tests whether we are in an IssueTracker if only is None: if hasattr(self, 'notifyables'): return self.notifyables else: return getattr(self,DEFAULT_NOTIFYABLECONTAINER_ID) elif only=='local': if hasattr(self, 'notifyables'): return self.notifyables else: return None elif only=='global': if hasattr(self, DEFAULT_NOTIFYABLECONTAINER_ID): return getattr(self,DEFAULT_NOTIFYABLECONTAINER_ID) else: return None else: return None # Templates # dtml_file = 'dtml/NotifyableManagementPartForm' NotifyableManagementPartForm = DTMLFile(dtml_file, globals()) class IssueTrackerNotifyable(SimpleItem.SimpleItem): """ IssueTrackerNotifyable class """ meta_type = NOTIFYABLE_METATYPE icon = '%s/issuetracker_notifyable.gif'%ICON_LOCATION meta_types = [] _properties=({'id':'alias', 'type': 'string', 'mode':'w'}, {'id':'email', 'type': 'string', 'mode':'w'}, {'id':'groups', 'type': 'lines', 'mode':'w'}, ) security=ClassSecurityInfo() manage_editNotifyableForm = DTMLFile('dtml/editNotifyableForm', globals()) manage_options = ( {'label':'Properties', 'action':'manage_editNotifyableForm'}, ) def __init__(self, id, alias, email, groups=[]): """ init """ if not Utils.ValidEmailAddress(email): raise "InvalidEmailAddress",\ "The email address (%s) was incorrect"%email else: self.id = id self.alias = alias.strip() self.email = email.strip() self.groups = groups def getName(self): """ Return self.alias. This might be in the future: self.firstname + self.lastname """ return self.alias def getEmail(self): """ return the email address """ return self.email def getTitle(self): """ return alias or email address """ name = self.getName() if name: return name else: return self.getEmail() def getGroups(self): """ return groups """ return self.groups def partofGroup(self, group): """ case insensitivly check if 'group' is part of this 'self.groups' """ these = [Utils.ss(x) for x in self.getGroups()] if isinstance(group, basestring): return Utils.ss(group) in these else: return Utils.ss(group.getTitle()) in these def showGroups(self): """ return blank or comma separated with brackets """ def manage_editNotifyable(self, alias=None, email=None, groups=None, REQUEST=None): """ save changes to Notifyable """ no = self n={'id':no.id} if alias is not None: self.alias = alias.strip() if email is not None and Utils.ValidEmailAddress(email.strip()): self.email = email.strip() if groups is not None: if type(groups) != ListType: groups = [groups] self.groups = groups msg = 'Notifyable updated.' if REQUEST is not None: url = self.absolute_url()+'/manage_editNotifyableForm' params = {'manage_tabs_message':msg} url = Utils.AddParam2URL(url, params) REQUEST.RESPONSE.redirect(url) else: return msg def getEmail(self): """ Return self.email """ return self.email def getAlias(self): """ Return self.alias """ return self.alias InitializeClass(IssueTrackerNotifyable) class IssueTrackerNotifyableGroup(SimpleItem.SimpleItem): """ IssueTrackerNotifyableGroup class """ meta_type = NOTIFYABLEGROUP_METATYPE icon = '%s/issuetracker_notifyablegroup.gif'%ICON_LOCATION meta_types = [] _properties=({'id':'title', 'type': 'string', 'mode':'w'}, ) security=ClassSecurityInfo() manage_editNotifyableGroupForm = DTMLFile('dtml/editNotifyableGroupForm', globals()) manage_options = ( {'label':'Properties', 'action':'manage_editNotifyableGroupForm'}, ) def __init__(self, id, title): """ init """ self.id = id self.title = title def getId(self): """ return id """ return self.id def getTitle(self): """ return title """ return self.title def manage_editNotifyableGroup(self, title=None, REQUEST=None): """ edit properties """ if title is not None: self.title = title if REQUEST is not None: mtm="Group changed." form = self.manage_editNotifyableGroupForm return form(REQUEST, manage_tabs_message=mtm) InitializeClass(IssueTrackerNotifyable) zpt_file = 'zpt/addNotifyableContainerForm' manage_addNotifyableContainerForm = PageTemplateFile(zpt_file, globals()) def manage_addNotifyableContainer(dispatcher, REQUEST=None): """ Create a notifyable container object """ id = DEFAULT_NOTIFYABLECONTAINER_ID title = DEFAULT_NOTIFYABLECONTAINER_TITLE dest = dispatcher.Destination() container = IssueTrackerNotifyableContainer(id, title) dest._setObject(id, container) self = dest._getOb(id) if REQUEST is not None: mtm= "%s created."%NOTIFYABLECONTAINER_METATYPE if int(REQUEST.get('goto_after',0)): page = self.manage_GlobalManagementForm return page(self, REQUEST, manage_tabs_message=mtm) else: return MessageDialog(title=mtm, message=mtm, action='%s/manage_main' % REQUEST['URL1']) class IssueTrackerNotifyableContainer(Folder.Folder, Notifyables): """ IssueTrackerNotifyableContainer class """ meta_type = NOTIFYABLECONTAINER_METATYPE icon = '%s/issuetracker_notifyablegroup.gif'%ICON_LOCATION #meta_types = [NOTIFYABLEGROUP_METATYPE] _properties=({'id':'title', 'type': 'string', 'mode':'w'}, {'id':'groups', 'type': 'lines', 'mode':'w'}, ) security=ClassSecurityInfo() dtml_file = 'dtml/GlobalManagementForm' manage_GlobalManagementForm = DTMLFile(dtml_file, globals()) manage_options = ( {'label':'Management', 'action':'manage_GlobalManagementForm'}, Folder.Folder.manage_options[0] ) def __init__(self, id, title, groups=[]): self.id = id self.title = title self.groups = groups def getRandomString(self, length=5, loweronly=0, numbersonly=0): """ return a completely random piece of string """ script = Utils.getRandomString return script(length, loweronly, numbersonly) InitializeClass(IssueTrackerNotifyableContainer) IssueTrackerProduct/version.txt0000644000175000017500000000000711012074372017101 0ustar peterbepeterbe0.9.3 IssueTrackerProduct/Webservices.py0000644000175000017500000000503311012074372017512 0ustar peterbepeterbe# IssueTrackerProduct # # www.issuetrackerproduct.com # Peter Bengtsson # License: ZPL # import Utils class IssueTrackerWebservices: """ Define an interface of functions that can be used using webservices. This class must be used as a base class for the IssueTracker """ def ws_countIssues(self): """ return how many issues there in this issuetracker """ return self.countIssueObjects() def ws_SubmitIssue(self, title, description, fromname, email, display_format=None, type=None, urgency=None, sections=None, url2issue='', confidential=False, hide_me=False, status=None, acl_adder=None, catalog=True): """ return the absolute URL of the issue that is created with this function. """ title = title.strip() description = description.strip() fromname = fromname.strip() email = email.strip() if not display_format: display_format = self.getDefaultDisplayFormat() if not type: type = self.getDefaultType() print ("Type defaults to %s"%type) elif type not in self.getTypeOptions(): # badly spellt for opt in self.getTypeOptions(): if Utils.ss(opt) == Utils.ss(type): type = opt break print ("Type is set to %s"%type) if not urgency: urgency = self.getDefaultUrgency() if not sections: sections = self.getDefaultSections() elif isinstance(sections, basestring): sections = [sections] confidential = Utils.niceboolean(confidential) hide_me = Utils.niceboolean(hide_me) if not status: status = self.getStatuses()[0] submission_type = 'Webservices' id = self.generateID(self.randomid_length, self.issueprefix) self.createIssueObject(id, title, status, type, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, index=catalog, acl_adder=acl_adder, submission_type=submission_type) where = self._getIssueContainer() return getattr(where, id).absolute_url() IssueTrackerProduct/TemplateAdder.py0000644000175000017500000001453111012074372017747 0ustar peterbepeterbe__doc__="""Generic Template adder with CheckoutableTemplates Use like this:: from TemplateAdder import addTemplates2Class as aTC ----- class MyProduct(...): def foo(...): zpts = ('zpt/viewpage', ('zpt/view_index', 'index_html'), {'f':'zpt/dodgex', 'n':'do_give', 'd':'Some description'}, {'f':'zpt/view_page', 'n':'view', 'o':'HTML'}, # same thing different name on the optimize keyword {'f':'zpt/view_page2', 'n':'view2', 'optimize':'HTML'}, ) aTC(MyProduct, zpts) dtmls = ('manage_delete', ('dtml/cool_style.css','stylesheet.css','CSS'), ) aTC(MyProduct, dtmls) dtmls_opt_all = ('dtml/page1','dtml/page2') aTC(MyProduct, dtmls_opt_all, optimize='HTML') ---- The second parameter (the list of files to add) can be notated in these different ways: 1) 'zpt/name_of_file' Expect to find a file called 'name_of_file.zpt' 2) ('zpt/name_of_file','name_of_attr') Expect to find a file called 'name_of_file.zpt', and once loaded the attribute will be called 'name_of_attr'. This is useful if you have dedicated 'index_html' templates all sitting in the same directory to be used for different classes. 3) ('zpt/name_of_file','name_of_attr', 'optimzesyntax') Same as (2) except the last item in the tuple (or list) is the optimization syntax to use. 4) {'f':'zpt/name_of_file'} Exactly the same effect as (1) 5) {'f':'zpt/name_of_file', 'n':'name_of_attr'} Exactly the same effect as (2) 6) {'f':'zpt/name_of_file', 'd':'Some description'} Exactly the same effect as (1) but CT template is flagged with 'Some description' for the description. 7) {'f':'zpt/name_of_file', 'n':'name_of_attr', 'd':'Some description'} Exactly the same as (4) but with the description set. 8) {'f':'zpt/name_of_file', 'o':'optimizesyntax'} Same as (4) but with the optmization syntax variable set. Note: 1) Use of dict-style items, always requires the 'f' key. 2) The addTemplates2Class() constructor accepts a keyword argument like optimize='CSS' that sets the optimization argument on all templates defined in that tuple/list. Exceptions withing are taken into account. Changelog: 0.1.9 Possible to override the usage of checkoutable templates even if installed 0.1.8 Ability to set TEMPLATEADDER_LOG_USAGE environment variable to debug which files get instanciated. 0.1.7 Changed so that it can work with Zope 2.8.0 0.1.6 Removed the need to pass what extension it is 0.1.5 Fixed bug that if one template in 'templates' does optimize, then the rest had to suffer from that accept that too. 0.1.4 If CheckoutableTemplates is not installed, the default template handlers are used.. 0.1.3 Added support of 'optimize' parameter in CheckoutableTemplates. 0.1.2 Fixed bug in use of variable name 'template' 0.1.1 Added support for description parameter in dict- style. 0.1.0 Started """ __version__='0.1.9' import os import time from Globals import DTMLFile from Products.PageTemplates.PageTemplateFile import PageTemplateFile try: from Products.CheckoutableTemplates import CTDTMLFile as CTD from Products.CheckoutableTemplates import CTPageTemplateFile as CTP except ImportError: CTD = DTMLFile CTP = PageTemplateFile #------------------------------------------------------------------------------ # if you set this to a filepath instead of None or False it will write down # each template that gets used in a tab separated fashion LOG_USAGE = os.environ.get('TEMPLATEADDER_LOG_USAGE', None) from Constants import DISABLE_CHECKOUTABLE_TEMPLATES #------------------------------------------------------------------------------ def addTemplates2Class(Class, templates, extension=None, optimize=None, Globals=globals(), use_checkoutable_templates=not DISABLE_CHECKOUTABLE_TEMPLATES): if use_checkoutable_templates: dtml_adder = CTD zpt_adder = CTP else: # If you don't want to use checkoutable templates, the reassign dtml_adder = DTMLFile zpt_adder = PageTemplateFile root = '' optimize_orgin = optimize for template in templates: optimize = optimize_orgin description = '' if type(template)==type([]) or type(template)==type(()): if len(template)==3: template, dname, optimize = template else: template, dname = template elif type(template)==type({}): dname = template.get('n', template['f'].split('/')[-1]) description = template.get('d','') optimize = template.get('o', template.get('optimize', optimize)) template = template['f'] else: # can't set 'optimize' this way dname = template.split('/')[-1] root = apply(os.path.join, template.split('/')) f = root # now we need to figure out what extension this file is if template.startswith('dtml/') or template.endswith('.dtml'): extension = 'dtml' elif template.startswith('zpt/') or template.endswith('.zpt'): extension = 'zpt' else: # guess work if os.path.isfile(template + '.dtml'): extension = 'dtml' elif os.path.isfile(template + '.zpt'): extension = 'zpt' if LOG_USAGE: tmpl = '%s\t%s\t%s\t%s\n' open(LOG_USAGE.strip(),'a').write(tmpl % (Class.__name__, extension, f, description)) if extension == 'zpt': setattr(Class, dname, zpt_adder(f, Globals, d=description, __name__=dname, optimize=optimize)) elif extension == 'dtml': setattr(Class, dname, dtml_adder(f, Globals, d=description, optimize=optimize)) else: raise "UnrecognizedExtension", \ "Unrecognized template extension %r" % extension IssueTrackerProduct/upgrade.py0000755000175000017500000002317311012074372016670 0ustar peterbepeterbe#!/usr/bin/env python """ The upgrade script downloads the latest IssueTrackerProduct from www.issuetrackerproduct.com and upgrades and installs the new files. Peter Bengtsson, mail@peterbe.com, (c) 2005 """ __changes__=''' 1.2 July 2005 Ability to create a new IssueTrackerProduct if not found 1.1 July 2005 First CVSed version ''' __version__='1.2' import os, sys, glob, gzip, tarfile from cStringIO import StringIO from urllib import urlopen import urllib2 latest_versionnr_url = "http://www.issuetrackerproduct.com/Download/getLatestVersionNumber" latest_versionurl_url = "http://www.issuetrackerproduct.com/Download/getLatestVersionURL" CVS_ANON_LOGIN = "cvs -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/issuetracker login" CVS_ANON_CHECKOUT = "cvs -z3 -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/issuetracker co IssueTrackerProduct" class VersionController: def __init__(self, home_path='.', verbose=False): self.home_path = home_path self.latest_version = _getLatestVersion() self.this_version = _getCurrentVersion(home_path) self.latest_version_url = _getLatestVersionURL() self.verbose = verbose def isUsingCVS(self): """ return true if in the home_path there is a CVS dir """ poss_path = os.path.join(self.home_path, 'CVS') return os.path.exists(poss_path) def cvsUpdate(self): """ try to do a CVS update """ _update = 1 if self.verbose: _update = raw_input("Upgrade %s [Y/n] " % shortname) if _update.lower().strip() not in ('y',''): _update = 0 if _update: _prev = os.path.abspath(os.curdir) os.chdir('..') os.system(CVS_ANON_LOGIN) os.system(CVS_ANON_CHECKOUT) os.chdir(_prev) def canUpgrade(self): """ return true if there is a newer version to download and install """ return self.this_version != self.latest_version def upgrade(self, pretend=False): return self._installURL(self.latest_version_url, pretend=pretend) def _installURL(self, URL, pretend=False): """ given a URL to a gzipped file, download it and replace the existing stuff """ assert URL.endswith('.tgz') or URL.endswith('.tar.gz') filename = URL.split('/')[-1] # download it downloadfile = open(filename, 'wb') req = urllib2.Request(URL) req.add_header('User-agent', 'Upgrade script (www.issuetrackerproduct.com)') furl = urllib2.urlopen(req) downloadfile.write(furl.read()) downloadfile.close() tar = tarfile.open(filename) _current_files = _listdir_fullpaths_filtered(self.home_path) _current_folders = _filter_whatispaths(_current_files) _current_files_copy = _current_files[:] for tarinfo in tar: assert tarinfo.name.startswith('IssueTrackerProduct'), \ "Incorrectly gzipped file" shortname = tarinfo.name.replace('IssueTrackerProduct/','') if not shortname: continue if shortname in _current_folders or \ shortname[:-1] in _current_folders: continue # directories aren't important elif shortname.find('mainbuttons') > -1 or \ shortname.find('actionbuttons') > -1: # old crap that might be in the tgz continue if shortname in _current_files: # the default value depends on if the file has changed shortname_path = os.path.abspath(os.path.join(self.home_path, shortname)) if tarinfo.size > 1000: if tarinfo.size == len(open(shortname_path).read()): _upgrade = 0 else: _upgrade = 1 else: # if the file is less than 1000 bytes, don't do a size comparison # because it can fail with really small files like 'version.txt' # whose content can change from "0.6.12" to "0.6.13" which is the same # length but different in content. Larger files and larger probability # that the changes also change the total file length. extracted_content = tar.extractfile(tarinfo).read() if extracted_content == open(shortname_path).read(): _upgrade = 0 else: _upgrade = 1 # if verbose, ask about each if self.verbose and _upgrade: _upgrade = raw_input("Upgrade %s [Y/n] " % shortname) if _upgrade.lower().strip() not in ('y',''): _upgrade = 0 if _upgrade: print "U %s" % shortname tar.extract(tarinfo, os.path.join(self.home_path, '..')) else: _install = 1 if self.verbose: _install = raw_input("Install %s [Y/n] " % shortname) if _install.lower().strip() not in ('y',''): _install = 0 if _install: print "I %s" % tarinfo.name.replace('IssueTrackerProduct','') tar.extract(tarinfo, os.path.join(self.home_path, '..')) if shortname in _current_files_copy: _current_files_copy.remove(shortname) this_script_filename = globals()['__file__'] if this_script_filename in _current_files_copy: _current_files_copy.remove(this_script_filename) elif this_script_filename.replace('./','') in _current_files_copy: _current_files_copy.remove(this_script_filename.replace('./','')) if self.verbose: if _current_files_copy: print >>sys.stderr, "The following files are not needed anymore" for f in _current_files_copy: print >>sys.stderr, f _delete = raw_input("\tDelete %s [y/N] " % f) if _delete.lower().strip() not in ('y',''): _delete = 1 if _delete: os.remove(f) def _filter_whatispaths(files): """ return a list of what is directories from a list of files and directories """ dirs = {} for v in files: splitted = os.path.split(v) if splitted[0]: dirs[splitted[0]] = 1 return dirs.keys() def _listdir_fullpaths_filtered(root, relpath=True): """ return a list of filepaths similar to os.path but open each directory. """ all = [] def _rejectFile(f): if f[-3:] in ('pyc','bak'): return True if f.endswith('~'): return True if f.startswith('#') or f.startswith('.#'): return True return False for base, dirs, files in os.walk(root): if base.endswith('CVS'): continue for file in files: if not _rejectFile(file): _path = os.path.join(base, file) if relpath: _path = _path.replace(root,'') if _path.startswith('/'): _path = _path[1:] all.append(_path) return all def _getCurrentVersion(home_path=None): if home_path: f = os.path.join(home_path, 'version.txt') else: f = 'version.txt' return open(f).read().strip() def _getLatestVersion(): return urlopen(latest_versionnr_url).read().strip() def _getLatestVersionURL(): return urlopen(latest_versionurl_url).read().strip() #------------------------------------------------------------------------------- def cli(): from optparse import OptionParser parser = OptionParser() parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="Each step is confirmed by the user" ) # parser.add_option( options, args = parser.parse_args() verbose = not not options.verbose if os.path.abspath('.').endswith('IssueTrackerProduct'): home_path = os.path.abspath('.') elif 'IssueTrackerProduct' in os.listdir('.'): home_path = os.path.abspath(os.path.join('.','IssueTrackerProduct')) elif sys.argv[1:] and (sys.argv[1].endswith('IssueTrackerProduct') or sys.argv[1].endswith('IssueTrackerProduct/')): home_path = sys.argv[1] else: # perhaps they just want to download it! _mkdir = raw_input("IssueTrackerProduct folder can't be found. Create? [y/N] ") if _mkdir.lower().strip() in ('n',''): raise OSError, "IssueTrackerProduct can't be found. See(k) help." else: os.mkdir('IssueTrackerProduct') home_path = os.path.abspath(os.path.join('.','IssueTrackerProduct')) open(os.path.join(home_path, 'version.txt'),'w').write('0.0.0') vc = VersionController(home_path, verbose=verbose) if vc.isUsingCVS(): vc.cvsUpdate() elif vc.canUpgrade(): vc.upgrade() else: print >>sys.stderr, "Latest version (%s) already installed" % vc.latest_version return 0 # everything went fine if __name__=='__main__': sys.exit(cli()) IssueTrackerProduct/Permissions.py0000644000175000017500000000076711012074372017555 0ustar peterbepeterbe# IssueTrackerProduct # www.IssueTrackerProduct.com # Peter Bengtsson # # Permissions and security constants for the IssueTrackerProduct # VMS = 'View management screens' AddIssuesPermission = "Add Issue Tracker Issues" DeleteIssues = "Delete Issue Tracker Issues" ChangeIssuePermission = "Change Issue Tracker Issues" IssueTrackerManagerRole = 'IssueTracker Manager' IssueTrackerUserRole = 'IssueTracker User' PERM_ACCESS_ISSUEUSER_INFORMATION = 'Access Issue User Information' IssueTrackerProduct/addhrefs.py0000644000175000017500000002171311012074372017014 0ustar peterbepeterbe## ## addhrefs.py ## by Peter Bengtsson, 2004-2005, mail@peterbe.com ## ## License: ZPL (http://www.zope.org/Resources/ZPL) ## __doc__='''A little function that puts HTML links into text.''' __version__='0.9.4' __changes__ = ''' 0.9.4 Made improveURL a publically available function. 0.9.3 Fixed a bug when text contains "m." but wasnt followed but a a-z. 0.9.2 Added supports for URLs starting with m., mobile. and www2. Added 1 new unit test 0.9.1 Fixed broken link parsing containing {curly brackets} Added 15 new unit tests 0.9 Better support for strings already containing .;:,"') _start_dropouts = list('(<') def _massageURL(url): while url[-1] in _end_dropouts: url = url[:-1] if url[0] in _start_dropouts: url = url[1:] return url def improveURL(url): # ok_middle_name_starts looks something like this: # ('ftp','http','www.','mobile.','m.','www2.') # If our url here starts with any of those that end in a . # then add http:// to it for each in ok_middle_name_starts: if each.endswith('.') and url.startswith(each): return 'http://'+url return url def _makeLink(url): return '%s'%(improveURL(url), url) def _makeMailLink(url): return '%s'%(improveURL(url), url) def _rejectEmail(email, start): if email.startswith("mailto:"): email = email[7:] if email.find(':') > -1: return True return False _bad_in_url = list('!()<>') _dont_start_url = list('@') def _rejectURL(url, start): """ return true if the URL can't be a URL """ if url.lower()=='https': return True for each in _bad_in_url: if url.find(each) > -1: return True whereat = url.find('@') if whereat > -1: for each in "http:// ftp:// https://".split(): url = url.replace(each, '') if not -1 < url.find(':') < whereat: return True if start in _dont_start_url: return True return False def _make_regexp(regexp): _whitespace = "[\s\({}<>\)]" #_not_whitespace = "[^\s\({}<>\)]" _not_whitespace = "[^\s{}<>]" ## don't allow url to end in ( or < but fine with ) or > # _not_whitespace = "[^\s<>\)]" regexp = regexp.replace("\s", _whitespace) regexp = regexp.replace("\S", _not_whitespace) regexp = re.compile(regexp) return regexp ok_middle_name_starts = ('ftp','http','www.','mobile.','m.','www2.') ok = {'start': ('^','\(','{','>','<','@','\s',''), 'middle':('ftp\S+', 'http\S+', 'www\.\w\S+', 'mobile\.\w\S+', 'm\.\w\S+',), 'end':('\)','}','>','\s','$'), } #_url_regex = _make_regexp('((^|\(|<|@|\s|)(ftp\S+|http\S+|www\.\S+)(\)|>|\s|$))') _or = lambda some_list: "|".join(some_list) _url_regex = _make_regexp('((%s)(%s)(%s))'%(_or(ok['start']), _or(ok['middle']), _or(ok['end']))) #_mailto_regex = re.compile('((^|\(|<|\s|)(\S+@\S+\.\S+)(\)|>|\s|$))') _mailto_regex = _make_regexp('((%s)(\S+@\S+\.\S+)(%s))' % (_or(ok['start']), _or(ok['end']))) def addhrefs(text, return_everything=0, emaillinkfunction=_makeMailLink, urllinkfunction=_makeLink): if not callable(emaillinkfunction): if emaillinkfunction is not None: _msg = "%r is not callable email link function" print >>sys.stderr, _msg%emaillinkfunction emaillinkfunction = _makeMailLink if not callable(urllinkfunction): if urllinkfunction is not None: _msg = "%r is not callable URL link function" print >>sys.stderr, _msg%urllinkfunction urllinkfunction = _makeLink info_emails = [] info_urls = [] urls = _url_regex.findall(text) for each in urls: whole, start, url, end = each if whole.endswith('">'): # reject it because it looks like it's taken out of a tag continue if whole.endswith('<'): # the next thing is a tag, if that tag is a # the chicken out! pos = text.find(whole) if text[pos+len(whole)-1:pos+4+len(whole)] == '': continue #print each url = _massageURL(url) if _rejectURL(url, start): continue link = urllinkfunction(url) if return_everything: info_urls.append((url, link)) better = whole.replace(url, link) text = text.replace(whole, better, 1) mails = _mailto_regex.findall(text) for each in mails: # print each whole, start, url, end = each url = _massageURL(url) if _rejectEmail(url, start): continue if url.find(':') > -1: link = urllinkfunction(url) if return_everything: info_urls.append((url, link)) better = whole.replace(url, link) else: link = emaillinkfunction(url) info_emails.append((url, link)) better = whole.replace(url, link) text = text.replace(whole, better) if return_everything: return text, info_urls, info_emails else: return text def test(): raise "TODO", "Move these slowly into testAddhrefs.py" t="this some text http://www.peterbe.com/ with links www.peterbe.com in it" t='''this some text http://www.peterbe.com/ with links www.peterbe.com in it Example''' t2='this some text http://www.peterbe.com/ '\ 'with links www.peterbe.com in it '\ 'Example' print addhrefs(t) t3='''this some text http://www.peterbe.com/ with links www.peterbe.com in it Example www,peterbe.com and www.peterbe.com ''' t4='''https://www.imdb.com (www.peterbe.com/?a=e) asd tra la www.google.com''' t = 'word (www.peterbe.com) word' t = 'word and so on' t = 'Go to: http://www.peterbe.com. There youll find' t = 'Go to: http://www.peterbe.com:' t = '''https://www.imdb.com.''' t = 'Hello mail@peterbe.com to you' t = 'Hello and to you' #t = open('sample-htmlfree.txt').read() t = 'Link1 link www.2.com' t = "Link1 link www.2.com" t = '''1. http://www.peterbe.com 2. www.peterbe.com 3. 4. mail@foobar.com 5. "Name "''' t = 'xxx mail@peterbe.com peter@grenna.net' t += ' xxx www.peterbe.com www.google.com xxx' t = 'mail@peterbe.com 123@a.com or www2.ibm.com or www.ibm.com?asda=ewr&gr:int=34.' t = 'peter@grenna.net 123@a.com ftp://ftp.uk.linux.org/' t = 'http://david:otton@www.something.com david:otton@www.something.com' t = ''' xxx abc ''' t='''www.msn.co.uk http://msn.co.uk http://www.msn.co.uk ftp:/google.com ''' t = 'At http://localhost/ I have apache and at http://localhost:8080 '\ ' I have Zope David used http://enchanter or http://enchanter/' t = 'See http://www.something.com/page?this=that#001' t = 'Bla bla https bla bla and http bla' t = '

mail@peterbe.com

\n\n

www.something.com

' t = '

http://something.com

\n\n

mail@peterbe.com

' t = '

http://example.com

\n\n

kilobug@freesurf.fr

' t += '\n\nhttp://www.dil(bert.com' if __name__=='__main__': test() IssueTrackerProduct/ReportScript.py0000644000175000017500000001262011012074372017671 0ustar peterbepeterbe# IssueTrackerProduct # # www.issuetrackerproduct.com # Peter Bengtsson # License: ZPL # # python from urllib import quote import urllib2 try: import timeoutsocket # set a timeout for TCP connections timeoutsocket.setDefaultSocketTimeout(60) except ImportError: timeoutsocket = None # zope from Globals import DTMLFile, InitializeClass, package_home from Products.PythonScripts.PythonScript import PythonScript from AccessControl import ClassSecurityInfo from Acquisition import aq_inner, aq_parent from DateTime import DateTime from zLOG import LOG, INFO from Shared.DC.Scripts.Script import defaultBindings # Script, BindingsUI, from Constants import * from Permissions import VMS import Utils #------------------------------------------------------------------------------- manage_addIssueReportScriptForm = DTMLFile('dtml/addReportScript', globals()) _default_file = os.path.join(package_home(globals()), 'www', 'default_report_script.py') def _suck_url(url): return urllib2.urlopen(url).read() def manage_addIssueReportScript(self, id, name='', url2script=None, REQUEST=None, submit=None): """ Add Isse Report Script object """ url = url2script if not id and name: name = name.strip() if name.find('-') > -1: id = name.replace(' ','_') else: id = name.replace(' ','-') id = Utils.safeId(id) elif not id: id = url.split('/')[-1] if id.endswith('.py'): id = id[:-3] id = str(id) id = self._setObject(id, ReportScript(id)) if REQUEST is not None: file = REQUEST.form.get('file', '') if type(file) is not type(''): file = file.read() if not file: if url: # download it from the net file = _suck_url(url) else: file = open(_default_file).read() self._getOb(id).write(file) if name: self._getOb(id).ZPythonScript_setTitle(name) try: u = self.DestinationURL() except: u = REQUEST['URL1'] if submit==" Add and Edit ": u="%s/%s" % (u,quote(id)) REQUEST.RESPONSE.redirect(u+'/manage_main') return '' class ReportScript(PythonScript): """ A ReportScript is a script where issue objects are passed through. The script looks at the issue and decides if it should be included in this particular report. """ meta_type = REPORTSCRIPT_METATYPE manage_options = (PythonScript.manage_options[0],) + \ ({'label':'Test', 'action':'manage_TestReport'},) + \ (PythonScript.manage_options[1],) + \ PythonScript.manage_options[3:] security = ClassSecurityInfo() def __init__(self, id): self.id = id self.ZBindings_edit(defaultBindings) self._makeFunction() self.last_yield_parentpath = None self.last_yield_count = None self.last_yield_date = None def getId(self): return self.id def getTitle(self): """ return title """ return self.title_or_id() security.declareProtected(VMS, 'Download2FS') def Download2FS(self, REQUEST=None, RESPONSE=None): """ return as a file for download """ if RESPONSE is not None: RESPONSE.setHeader('Content-Type', 'text/x-python') _inline = 'inline;filename="%s.py"' RESPONSE.setHeader('Content-Disposition', _inline % self.getId()) return self.read() def manage_afterAdd(self, REQUEST, RESPONSE): """ clear the last_yield_* in case this script has been copied from another issuetracker. """ if getattr(self, 'last_yield_count', None): if self.last_yield_parentpath != '/'.join(aq_parent(aq_inner(self)).getPhysicalPath()): self.last_yield_count = None self.last_yield_parentpath = None self.last_yield_date = None def setYieldCount(self, count): """ a yield count is an integer number of how many issues were returned when this report is run. This number is stored with a date and which issuetracker path was used. The last thing is required because the script might be moved to a different issuetracker where the count doesn't make sense because the issues are different. """ count = int(count) assert count > -1, "count must be 0 or greater" self.last_yield_parentpath = '/'.join(aq_parent(aq_inner(self)).getPhysicalPath()) self.last_yield_count = count self.last_yield_date = DateTime() def getYieldCountAndDate(self): """ return a tuple of the the (count, date) if possible """ if getattr(self, 'last_yield_count', None): return (self.last_yield_count, self.last_yield_date) return None security.declareProtected(VMS, 'manage_TestReport') manage_TestReport = DTMLFile('dtml/test_report', globals()) ZPythonScriptHTML_editForm = DTMLFile('dtml/ReportScriptEdit', globals()) manage = manage_main = ZPythonScriptHTML_editForm ZPythonScriptHTML_editForm._setName('ZPythonScriptHTML_editForm') InitializeClass(ReportScript) IssueTrackerProduct/I18N.py0000644000175000017500000000031011012074372015701 0ustar peterbepeterbe#try: # from Products.PlacelessTranslationService.MessageID import MessageIDFactory # _ = MessageIDFactory('itp') #except ImportError: # def _(s): # return s def _(s, *a, **k): return sIssueTrackerProduct/refresh.txt0000644000175000017500000000000011012074372017043 0ustar peterbepeterbeIssueTrackerProduct/__init__.py0000644000175000017500000003066611012074372017002 0ustar peterbepeterbe# IssueTrackerProduct # www.IssueTrackerProduct.com # Peter Bengtsson # import os import stat from time import time from AccessControl.Permission import registerPermissions from zLOG import LOG, ERROR, INFO from App.Dialogs import MessageDialog import IssueTracker import Thread import IssueTrackerNotifyables as Notifyables import Issue import Email import IssueUserFolder import ReportScript import Utils from Constants import * from Permissions import * from I18N import * try: from slimmer import js_slimmer, css_slimmer except ImportError: css_slimmer = js_slimmer = None """IssueTracker Product""" def dummyFunction(zope): """ dummy function because we don't want to use the ZMI for some classes. """ return MessageDialog(title="Add Issue Error", message="Don't add Issue Tracker Issues with the Zope management interface;"\ "instead, use the Issue Tracker only", action="manage_main") def initialize(context): """ Initialize IssueTracker product """ if 1:#try: context.registerClass( IssueTracker.IssueTracker, constructors = ( # This is called when IssueTracker.manage_addIssueTrackerForm, # someone adds the product IssueTracker.manage_addIssueTracker, # also useful in the DTML add template IssueTracker.manage_hasAquirableMailHost ), icon = "www/issuetracker.gif" ) context.registerClass( Issue.IssueTrackerIssue, constructors = (dummyFunction,), permission = AddIssuesPermission, icon = 'www/issue.gif', #visibility = None # settings this to None disables copy/cut/paste support ) context.registerClass( Notifyables.IssueTrackerNotifyableContainer, constructors = ( # This is called when Notifyables.manage_addNotifyableContainerForm, # someone adds the product Notifyables.manage_addNotifyableContainer ), icon = "www/issuetracker_notifyablecontainer.gif" ) context.registerClass( IssueUserFolder.IssueUserFolder, constructors=( IssueUserFolder.manage_addIssueUserFolderForm, IssueUserFolder.manage_addIssueUserFolder, IssueUserFolder.manage_getUsersToConvert ), icon='www/issueuserfolder.gif', ) context.registerClass( ReportScript.ReportScript, constructors=( ReportScript.manage_addIssueReportScriptForm, ReportScript.manage_addIssueReportScript, ), icon='www/issuereportscript.gif', ) def registerIcon(filename, **kw): _registerIcon(OFS.misc_.misc_.IssueTrackerProduct, filename, **kw) def registerJS(filename, **kw): _registerJS(OFS.misc_.misc_.IssueTrackerProduct, filename, **kw) def registerCSS(filename, **kw): _registerCSS(OFS.misc_.misc_.IssueTrackerProduct, filename, **kw) registerIcon('issue.gif') registerIcon('issuedraft.gif') registerIcon('issuethreaddraft.gif') registerIcon('issuethread.gif') registerIcon('issuetracker_notifyable.gif') registerIcon('issuetracker_notifyablegroup.gif') registerIcon('issueassignment.gif') registerIcon('notification.gif') registerIcon('issuetracker_pop3account.gif') registerIcon('issuetracker_acceptingemail.gif') registerIcon('issuetracker_logo_error.gif') registerIcon('bar.gif') registerIcon('issuereportscontainer.gif') registerIcon('emailicon.gif') registerIcon('reports.gif') registerIcon('statistics.gif') registerIcon('report-big.png') registerIcon('close.gif') registerIcon('paperclip.gif') registerIcon('gradhead.png') registerIcon('gradissuehead.png') registerIcon('gradtablehead.png') registerJS('core.js') registerJS('jquery-latest.min.js', slim_if_possible=False) registerJS('jquery-latest.pack.js', slim_if_possible=False) # legacy icons = Utils.uniqify(ICON_ASSOCIATIONS.values()) for icon in icons: registerIcon(icon, epath='icons') menuicons = ('add.gif', 'list.gif', 'complete.gif', 'home.gif', 'user.gif','login.gif', 'logout.gif') for micon in menuicons: registerIcon(micon, epath='menuicons') # tiny_mce_images = {'tinymce/themes/simple/images': # ('bold.gif','bullist.gif','cleanup.gif','italic.gif', # 'numlist.gif','redo.gif','spacer.gif','strikethrough.gif', # 'underline.gif','undo.gif'), # } # for path, imagenames in tiny_mce_images.items(): # for imagename in imagenames: # registerIcon(imagename, epath=path, # startpath='') ##context.registerHelp() ##context.registerHelpTitle('IssueTrackerProduct Help') else:#except: """If you can't register the product, tell someone. Zope will sometimes provide you with access to "broken product" and a backtrace of what went wrong, but not always; I think that only works for errors caught in your main product module. This code provides traceback for anything that happened in registerClass(), assuming you're running Zope in debug mode.""" import sys, traceback, string type, val, tb = sys.exc_info() sys.stderr.write(string.join(traceback.format_exception(type, val, tb), '')) traceback.print_exc(sys.stdout) # for all those people in debug mode zope del type, val, tb LOG("IssueTrackerProduct", ERROR, "Could not be installed", error=sys.exc_info()) import OFS, App from App.Common import rfc1123_date from ZPublisher.Iterators import filestream_iterator from Globals import package_home, DevelopmentMode from OFS.content_types import guess_content_type FILESTREAM_ITERATOR_THRESHOLD = 2 << 16 # 128 Kb (from LocalFS StreamingFile.py) class BetterImageFile(App.ImageFile.ImageFile): # that name needs to improve def __init__(self, path, _prefix=None, max_age_development=3600, max_age_production=3600*24*7, content_type=None, set_expiry_header=True): if _prefix is None: _prefix = getConfiguration().softwarehome elif type(_prefix) is not type(''): _prefix = package_home(_prefix) path = os.path.join(_prefix, path) self.path = path self.set_expiry_header = set_expiry_header if DevelopmentMode: # In development mode, a shorter time is handy max_age = max_age_development else: # A longer time reduces latency in production mode max_age = max_age_production self.max_age = max_age self.cch = 'public,max-age=%d' % max_age data = open(path, 'rb').read() if content_type is None: content_type, __ = my_guess_content_type(path, data) if content_type: self.content_type=content_type else: raise ValueError, "content_type not set or couldn't be guessed" #self.content_type='text/plain' self.__name__=path[path.rfind('/')+1:] self.lmt=float(os.stat(path)[8]) or time.time() self.lmh=rfc1123_date(self.lmt) self.content_size = os.stat(path)[stat.ST_SIZE] def index_html(self, REQUEST, RESPONSE): """Default document""" # HTTP If-Modified-Since header handling. This is duplicated # from OFS.Image.Image - it really should be consolidated # somewhere... RESPONSE.setHeader('Content-Type', self.content_type) RESPONSE.setHeader('Last-Modified', self.lmh) RESPONSE.setHeader('Cache-Control', self.cch) RESPONSE.setHeader('Cache-Length', self.content_size) if self.set_expiry_header: RESPONSE.setHeader('Expires', self._expires()) header=REQUEST.get_header('If-Modified-Since', None) if header is not None: header=header.split(';')[0] # Some proxies seem to send invalid date strings for this # header. If the date string is not valid, we ignore it # rather than raise an error to be generally consistent # with common servers such as Apache (which can usually # understand the screwy date string as a lucky side effect # of the way they parse it). try: mod_since=long(DateTime(header).timeTime()) except: mod_since=None if mod_since is not None: if getattr(self, 'lmt', None): last_mod = long(self.lmt) else: last_mod = long(0) if last_mod > 0 and last_mod <= mod_since: RESPONSE.setStatus(304) return '' if self.content_size > FILESTREAM_ITERATOR_THRESHOLD: return filestream_iterator(self.path, 'rb') else: return open(self.path,'rb').read() def _expires(self): return rfc1123_date(time()+self.max_age) def my_guess_content_type(path, data): content_type, enc = guess_content_type(path, data) if content_type in ('text/plain', 'text/html'): if os.path.basename(path).endswith('.js-slimmed'): content_type = 'application/x-javascript' elif os.path.basename(path).find('.css-slimmed') > -1: # the find() covers both 'foo.css-slimmed' and # 'foo.css-slimmed-data64expanded' content_type = 'text/css' return content_type, enc def _registerIcon(product, filename, idreplacer={}, epath=None, startpath='www'): # A helper function that takes an image filename (assumed # to live in a 'www' subdirectory of this package). It # creates an ImageFile instance and adds it as an attribute # of misc_.MyPackage of the zope application object (note # that misc_.MyPackage has already been created by the product # initialization machinery by the time registerIcon is called). objectid = filename if epath is not None: path = os.path.join(startpath, epath) else: path = startpath for k,v in idreplacer.items(): objectid = objectid.replace(k,v) setattr(product, objectid, #App.ImageFile.ImageFile(os.path.join(path, filename), globals()) BetterImageFile(os.path.join(path, filename), globals()) ) def _registerJS(product, filename, path='js', slim_if_possible=True): objectid = filename setattr(product, objectid, BetterImageFile(os.path.join(path, filename), globals()) ) obj = getattr(product, objectid) if js_slimmer is not None and OPTIMIZE: if slim_if_possible: slimmed = js_slimmer(open(obj.path,'rb').read()) new_path = obj.path + '-slimmed' open(new_path, 'wb').write(slimmed) setattr(obj, 'path', new_path) def _registerCSS(product, filename, path='css', slim_if_possible=True): objectid = filename setattr(product, objectid, BetterImageFile(os.path.join(path, filename), globals()) ) obj = getattr(product, objectid) if css_slimmer is not None and OPTIMIZE: if slim_if_possible: slimmed = css_slimmer(open(obj.path,'rb').read()) new_path = obj.path + '-slimmed' open(new_path, 'wb').write(slimmed) setattr(obj, 'path', new_path) IssueTrackerProduct/www/0000755000175000017500000000000011012074373015503 5ustar peterbepeterbeIssueTrackerProduct/www/reports.gif0000644000175000017500000000017211012074372017667 0ustar peterbepeterbeGIF89a¢ÿÀÀÀÿÿÿÌÌÌ™™™fff!ù,@?ºÜ0A„Ië,Ðê›·ÅžJ!•\¡®l;¼p,¿Ô@ØxfD¤Ý õL™[ÄôM„"ä¡.Žì";IssueTrackerProduct/www/default_report_script.py0000644000175000017500000000057411012074372022465 0ustar peterbepeterbe##parameters=issue # You're given an issue as the first parameter on this report # script. Your job, with this script, is to look at this issue # object and decide if you want to show it or not. Either return # True or False at the end of this script. # example code if issue.countThreads() > 1: return True # default thing to do is to NOT include the issue return FalseIssueTrackerProduct/www/report.gif0000644000175000017500000000013211012074372017500 0ustar peterbepeterbeGIF89a ¡  zúúú!þ(c) IssueTrackerProduct, „¢+Æë‚” <g¨—ëí\Ùĉ–‰ ;IssueTrackerProduct/www/issuetracker_acceptingemail.gif0000644000175000017500000000012511012074372023720 0ustar peterbepeterbeGIF89a€ÿÿÿ!ù,,Œ©Ýk\”é);'`mæXâFfrà!²ä'¡oH±øyùµ×wEóA;IssueTrackerProduct/www/gradhead.png0000644000175000017500000000031611012074372017747 0ustar peterbepeterbe‰PNG  IHDRwÂn*UbKGDÿÿÿ ½§“ pHYs  šœtEXtCommentCreated with The GIMPïd%nEIDAT(ÏÍ‘± À ï¼ÿÙ‰Bú.4‰ÎÖ[ÖÉOkÏ”ˆ$ÊTÓJ€Rçru@Õ…@€/~ú;¯ú ¼ELÑbÓ1¥IEND®B`‚IssueTrackerProduct/www/issueuserfolder.gif0000644000175000017500000000055111012074372021415 0ustar peterbepeterbeGIF89aÕ=Ű©¶¦s;64Ƴ«|wbUPŠ|vëßjÒÄ|¾¼Ç²«xicÀ¯©vjeF>:¶¢›—…~4-*Ôľ/)"оsc]űª‚qk˶°þû ôó ۨ<41n`[`TOòëèл³¢œ™JA=˺„í×Ïмµ˜†€]RN̼rsfaÅ¿½Ä´­»§Ÿêåka]Ê·¦ƒwsóï&³ ™€€€PPPÀÀÀÿÿÿÿÿÿÿÿ!ù=,†ÀžpH,qHœ±ˆËñœÊe¯ù¬&¯BO§ I(Ü0w7Õ=74•«ê¼‘q:gNqaÉŸî2o` wyp9"&65rƒf;UŽ9 +'of2%)/—¢9;-( ¡z9$3«S87ÁÂÃÁdB;ÈÉÊÈ=A;IssueTrackerProduct/www/statistics.gif0000644000175000017500000000024211012074372020361 0ustar peterbepeterbeGIF89a³éééììì¥rrrr¥È††r—r¢Ø¥¥rˆÈ†ØØrÿÿÿ!ù,OpÉI«½8ËÃmùU’‚áXžÁˆde¢C½À’ /óbƒ›jA ³Àoð¼“(Œ=£¨eUò $—M«; âˆãž§h»ßðv;IssueTrackerProduct/www/issuethread.gif0000644000175000017500000000022411012074372020507 0ustar peterbepeterbeGIF89aãÿÿÿïïïÏÏÏÀÀÀ»»»ªªªŠŠŠfffEEE111ÿÿÿÿÿÿÿÿÿÿÿÿ!ù,AðÉù–½T^ :ÀÚâÞ’Š1Zù©+G&)“aßÞÄ-’àW:­`SHÔl”f2“a²¨Ñ)v"ºnC_I;IssueTrackerProduct/www/issuereportscontainer.gif0000644000175000017500000000031011012074372022635 0ustar peterbepeterbeGIF89aã  zPPP€€€ÀÀÀÿÿúúúÿÿÿ!þ(c) IssueTrackerProduct!ù,ZðIªµSŽŽÿG …Ñž¨•E'¼o8h‚œh€Ï€­, ð|%¨xü‡‚±‡H¢RSÐ5”6­W&u{õR“—KÖ l³ ÙaN¯Ïy€~Ï×?";IssueTrackerProduct/www/notification.gif0000644000175000017500000000034511012074372020661 0ustar peterbepeterbeGIF89aĻֻÝëÝÿ ÌàÌ`ªÌª3…3 ] DD™Â™_f£fîõ€ˆ¸ˆÀÀÀPPPfÿÿÿÿÿÿ!ù,b`%ŽUð<ÐlEH°D´l,Ñ5|°QïGØâW‰PŽÈìq„LŠ”ˆÃHaÀ Š<>`Á`KíRHr÷¨8šOªTŠX¹ݯ„IQG§€e~kkd‡ˆ‰ˆqŽ!;IssueTrackerProduct/www/issuetracker.gif0000644000175000017500000000021611012074372020674 0ustar peterbepeterbeGIF89a¢QGCš†îÚÔл³þþþÿÿÿÿÿÿ!ù,Sxz³Ž^„Ô¶£¡’Q çMFž3I PxÕ¦4”}K pþ‚ › †è”…JS7T@d’ø`_4«SÀˆ/¥µW&A;IssueTrackerProduct/www/issuetracker_logo_error.gif0000644000175000017500000000415211012074372023130 0ustar peterbepeterbeGIF89a$6÷ÿ‰‰ˆ÷¶¬XJEìËÃ(éÒÅìÄ´—‘ŽH:5ääãĦ›·©¦¥•ŠLFCí˼óêé—zv¨œ—YVRúÛØæÏÂ3,'öÒÌòÌÄã¯õõõ…wn,&$Æ®¢zsnì½¹£‰ƒÔÒЈogWD;ËÉÄÚ»­eJFæ¾µå»ZPKÛÉÅvYT­­¬âÀ±éÈ´ÜÜÜÓÍËâµ°e\[ò¹±üö󯣟պ´è½·øùù¶—Œåº²Ëº˲ª`SL2/-üüüÓ¿»puòž³´³ëÙÖIA:⹮ú´»»ºla_ôÍÊK3,’“‘”Š„¾£œðƵËËÉÛ»¦ÜÎÊⵤìÁ¹á·³¨Š~ë¿®£¦¢Ø®§?>;æÆÄˆ{pËÆÁîÃÃ,zzvîïí:#‡„~ŽxÒ²£ÅÅà uljññðÜ´¯í±£é¿±°Ÿ›èÀ·ŸŽƒW=7jSMšŠ845â¼¹dUR꺶…~z}leééé‘tlþýýœ•’‹Ê¹µ{kpüûúyvyd\îž ð¾ºÁ¦¡óõòå¾²ãÍÍŲ¯ÞÁµæ¼´ki€uqêÅ»tyv»±­<3/⻩""" È´²¾¡•461>*& þþþ=83!qvrfb`fie±ž’Þɼ+2+ookm[O›™èêçÖÀ³×Ø×ÞÜÕììësdc˺«Ÿ¡oidùþùüþýß¿¬±”Ž÷øõn\W¸œ–õæäçæä佮לּœ˜eUZa\WhVSÿüûéÜÛìÌÈ›€oßàßìéé›zè·²úÍ¿SOTtghëÔÒÖÖÕþãÒ༳Ỵïêî廬’†€è±«Ç«§ôòð¨}vÙ½¸„`ZcF4ײ°öÁ·þÁ·ôË½×ÆÆÐ¹®ø½´ÇËÉÆÉÄÐÇÉÛÄÂkga¾À½ï½µÉª›QOM蹳ݻíññœ™——˜—=./ïÅ»ÉÐÌïûë¿á°âÚÙ{{xàÙÖáÔÐ ÿÿÿÿÿÿ!ùÿ,$6ÿÿ (Õ2¢:ŒðG°¡Ã‡[5`@F„ !jÜØê“$zˆÊ¸±dCA>DÀzÌ’&MúsB ¬R<ˆÉóß‚ LHQˆÌÌž§ˆè‡ŠÃ=¯v"ÝDà 0V‘BáèÔ†<À¤Ñ°Š*T ¾jÓàÌ'ÏH‰€äUí?OLô+æ ¡ûêªÍ±¬Ó64ð“ÁîÃTgè(.…š‚¿ª;si”Š äðÈ0`Ô?D„¢Ê-M¤ÐAi¤aÂýÈ’B„@TÄ,ÔPM0Ë §ˆ2ˆ ô³ß%ÀäAÀ À"L ˆ?$õ‘Š$³¨rЍ`%+Râ38Tƒ#U °‚QÀä+¢“À))¤?@¡ jàB€CºÀQˆ ¤à‡G½0JF€±A‹`ra’5cpôŒõHÙ(œ¥1W*Ĥ7'àЇdÜ8P"¡wF„3¤`CL+ôàp A ÆdäO§7C„c6tŒ3®€ÿ Aÿn#!)WXX[[[vv«Èº…{{}­¯®ëëë23?W223../ïïï±ÿÜWWo>…ÃôôôccŠ÷¯ÿævvª\\\‰ŠŠYYo{qGî‘m(B©z>&6<9pppe:_gqEAFwz¼~~~vv†ÿäokl§ÉÉË/™™Íf,C›;”A¸¸¸  ÙÀ<ÿ͆†‡¸cCŽs {{{öžko­hY&ÿ؃òÁÀÀÀ|3Iÿ£í”%ƒƒ…NA‹‹‹¥¥ÛSSUT ž >^^^¤¤Úaak×N„ššÏãS···yyyýýýKTYww¬nosÆ9§šššÁÂÄ……©Ñ‡„…ï°=<<º¹º‡.¢&a”qfg ‡`¿¿¿ÛÛÛ¾¿áéS €€‚331ÂD|cfe]_œu3Gÿ .ÿ׌ŒÿÒh=&ÿs#!-ƒƒ´$H=(ÿ¤÷÷÷(]´´´®R}òòòÌÌëÿÈ÷š› [ÿÿÿ!ùº,óué¢s¨É,Úà!°!é±gF/A4Ôe ÏG¡¶p ˆA•Qqù„ÇÇ"D^¸RPi# [ Ix…%ÑZƒÄhÈÁ ‚NptÈ “(? DH ›%]4˜Z“ËCT*øDr@á45Œ\ú!¤á“aŽ´ˆCÀÄ*2²*Äj*‚¢7ˆ¤ºeÁÒÓ ÉAJÏ%$ÎHù#g#‹VL¼€q£4$mÜ1bV(îÀ:QjÌn”$“IìªÓ€Ò +Ëä¹0ÅÍOT²ŒJ{£@CP4©*á¤VÀ;IssueTrackerProduct/www/issuetracker_notifyable.gif0000644000175000017500000000017111012074372023110 0ustar peterbepeterbeGIF89a¢  ¤Ì™fÿÿÌÿÌ™ÿÿÿ!ù,>XºÜ<0Ê6„½— °*ùàl~1rŠw¦ky‚qkÎ*iã9[¸/ˆZ o ˜n\þEg WëH$V;IssueTrackerProduct/www/report-big.png0000644000175000017500000000445711012074372020274 0ustar peterbepeterbe‰PNG  IHDR @ÈbKGDÿÿÿ ½§“ pHYs  šœtIMEÕ ÿX'F¼IDATHǽ—kpTåÇçì-»I6»¹ßv“ФQ.i”ÉA†b¬„Ô äf«vª`°àŒŠ¨LQ*ŽÊ˜LªS- ÷6`Çj„LÈ…ÜCî1—ÝM6»›ìæœ~ØdI™éíûé=ç¼ïûŸçù?ÿç9œ9s.Ο?ÿ.…B üC¥R¹ C£Íf;SYYùïÚÚÚ ›ÍfdߢÇ\þ‡3//ï+£Ñ˜€–­[·N–eY®¯¯÷­ž˜˜ðÍ­V«,˲<::ú£'O_{‹÷Î;^<§Ñ¨³ý¸rêccc477³wï^"""HOOç©§ž¢¼¼œ•+WRRRÂÁƒ¹xñ"¿ÁÏOK]]f³€õë×û jooG­Ví·{÷é³fŹ å .|Ø”Óâ€Ë5ƹsçih¨gÉ’ûèêêáûÊj.\(¥¹å:õõM|ñE1'NœbÙ²¥vîÜ9å=œN'þþþ¸\v>ûì4·ß~;¡¡¡Úììß/¶ttt tttTø\ÝÜÜ|ÃO’ýGœç¾e KKKåîîî!ihh”³²²äääd922R~î¹?KK—.­Q*•k½ÏbÇãu¹c˜½ï ßª$ÀÁ˜gµÒ§Ã†^3A{ÙqžáEŸ[,Xà›ët:’’ÉÏÏÇápqõj%IIIBnn®Öãñ*qjƒËå`xdˆO—ðÎÇ]¼{d„þöfÇûùò{=§/‡PTª ²ª €#‡ñÀý™iÎû˜‡–e"KÓï $$“)†… ÓIJJ"--MÄä2 hT*Výv1¦¹k蔈ÚKÆ‚@Z¤DÆE-±1FLúaVeç°zÕïxÿL5ËWÞIzÖ£twua…'˸w^,·%Dãñx‰;::êóŒÏb»Ýîýèò c}v4»ž13.*óŒ‘)*܉1‡÷€ÎÎNÊ**y{×_ÈÝð j…’ã‡NQtâ0 ç ×[ë½^@’$°Ïb…B€Û­-µ”œ¿€&ô—tˆ8F=¨d?HqX„@Æ&¼`Ž3aŽãów_Æ:hA ìØºžžŽ¢â™·Ï8{R@¸Éb…RNåA=Ñ‹ÒY…F©ÂæRØÍòÙZ"‚pŒ{·}óm ú(”á<¹åyžüÃÓÔ7öŸ„ \©jÀjµLWT5 òY6y; *]ÚÀ0 †@”Z%’_)±Z²S‡1ÙÎP\Ú@`@¯¿þ sf'ó¶ílܸY–ghwTTäôG|À‹³ÙŒÛí¢ ‡¦–V¢"CiojÅ=l£),˜y6=ÿúO×½dIMMA­VräÈaT*Ë—+ÑéT pòäaž}v+QQÑ 3ã2ÓbìzÜVë0GOŸÆBKWCN bk Õm´v]Çgš$þþAÚÚÚÈËÛÆÙ³Åœ:u “ÉL\\"‹…¨¨hßÙ¢¨¸¼ŒKHˆãè±cÿSù»t©”“'“’ò+¾þúëÐéüéíí&##½^?ÉfÏôm ùÈ51áýh±“Ÿÿuuu3@ŠŠŠp8|þùK™Lf6oÞÌêÕpàÀ{lذ‘7ß|‹E‹î¡¶ö!!!“ÀSêèpžiEB@iÙwlܸ‰ØØXAA__‹erþ|1sç¦P]]å#OOO7½½Ý$$ÄÓ××$SYYOqq1÷Ý·„ššÒÒ~ŸŸßô<–I9ÕŒŽ:¸÷Þ¥TU]ÅdŠehhˆ+W®0oÞv»¯¾úÒgqRR§OŸäá‡#9yW¯zŠ‹Ï20ÐËž=o0>îš‘ÏÊ)æOÅ£¿›mˆˆˆPêêêD‘ÑQóæÍåøñ¼ôÒNßæÊÊ«œ;÷5’$ÑÜ|‹e•JCEÅ „†FN™’ÙÕÕ6™Ï! F$Idùòe‚ôÜqG*J¥„ŸúÇÇÝdffPX˜(Šìß¿‡+VLê~~~*ßÚ††š›òÛMMÍlÞ¼‰Å‹Óù¾¢ŒCŸÐÚXÍ#9°í¹?±5o ƒ½jk®ãç§%&&–ŒŒÅ”—WñÑG<ñÄZrr²Q«ý(.ö†%%eþ­-îèh <<œÂÂB®]k¤ªº–÷~Â?JGyxGu-¼ñö> !F´?JK¿c÷îݼòÊ«¬[·–à`’¤fÅŠeȲÌüùwP]]uk‹ † I³rñbÛ·oçƒ÷ÿÀ¡=¨8ôkÍfÏk/31!#*ŒA¤¦¦b·p÷Ý‹Eطﯴ¶¶“—÷¼¯©Œ‰‰˜ñf±Ù¼¥+22œ””¹¬YóçÏ/Ájµ±oï–Ü¿’OþžÞh@¥à¯Ñ‘™™ALŒ™êêk<øàC8ÒÒÒ Ân&8ØHw÷ÄÇ'1]Â}ÀÁÁFêêêØ²e+iii=z”ìì.]*cá¢LÊÊ/3{ölÆÆ<(QS^QÂà€Y–ùðÃ÷ÈÌ\Èä366FVÖô÷÷c6Ç7)±¢(Ü ì-]&S,¹¹¹èõ$''²iÓÙµëUt:öï×›FWj°Z\,¸+@žÖ¶f IHˆ'+ëAV­Z5ÙN9q¹Féëë#&Æ<#Æ>`oΉ A¬^F£cdÄëþµk×2ÃÃ*QéF­• GÌš•Db¢€N§ãž{üwáñ¸q:] ÕŒF€œœœqùÿ4¶mÛÖ < +;;;¿={öì/>ýôŸ¨T¢àe¢xS%ºAH©tôzNò‚™ÿ|¢¯ùûû£Õj¥¢¢¢`˜BCC35õm]]ÝÊ©~ˆŸ~È¢(J’$õ—6Ð:@ÁÏ?<€ÿ/|HÛÞ'7˜<IEND®B`‚IssueTrackerProduct/www/issuedraft.gif0000644000175000017500000000022111012074372020335 0ustar peterbepeterbeGIF89aãEEEŠŠŠššš»»»ÏÏÏßßßÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ!ù ,>ðÉI+ÀjŒ¾@‡ŸŠCðq¢¸µÝ!d+ÂáÕpA¸“‚ !´j¬Ãôœ˜Ò㕆ª¥+ͺýD;IssueTrackerProduct/www/issue.gif0000644000175000017500000000023611012074372017322 0ustar peterbepeterbeGIF89aãÿÿÿïïïßßßÏÏÏÀÀÀ»»»šššŠŠŠvvvEEE111!!!ÿÿÿÿÿÿ!ù,KðÉùš½T^ :ÀZã^zÂ$AiŠÞÁÊ1^dWßð($ ¯×)$„® ©` ™*ÄD‚7Á‚͓ҳd20ï—ûÙŽ•âsH-‰;IssueTrackerProduct/www/issuetracker_notifyablegroup.gif0000644000175000017500000000204711012074372024171 0ustar peterbepeterbeGIF89a÷ÅÙt'”””ÍÍͽ½½BS˜ìì윜œÿ¦Aÿ™4ÿ³NQdù¤¤¤áááÞÞÞóóójf`ŸŒldÁdŽˆDPPPò²M@b@ÿ½XNW¦*…*ϤXÓ¨bÝÝÝ6{“ö1Vm¬ ÓÿÙÙÙW…ÜH@6^^^øøø---f¥fÿ¥@'y'1‡1ððð==ÃÃÃjjjhhhúúúÒ×ã\uø÷÷÷CCCÒ¤Sµµµ»~!¡¡¡¹‡*ghU57Lƒ………—Ƥ½X¤vRføêîõjUKælêä×^t;;;~H¸‹6Sm󅘅†fG7ò“.ìõÿ[$C—2ßßßéééï‘,.RNÌÌÌlˆý ;xIaCÔÁœÿ¶Q=‡’æ—2Òt"9µ9¨ÏºÀ€'zzzú–4r,SÂSÿ¤?QxåêêêKKKdÏПRì°K±±±c{ý3n3tÒtÏ™M‰‰‰bÊb½]EhÆ]]]}pWæææNgN?T•fUPddd¥¥¥ÕÕÕÚÚÚ3„3“““ñññââ⨨¨:‰:†††i0<^¼™£ºVÃVêÏ¡666ÿš5oŒþƒƒƒP¯Tõõõ\qýERpòî後G›ÍõÒĨQœ‘‚Ú‚F¦FÕ¤XBoÒJgãàõïq_Pƒvê¥CÆc#eee<<<“3)))к©©©-NÍV¯V]®hµPkkkÿ¯J4SݹwÊÊÊ”ÇúJ]Jy7DlF&ÿ²MR~á³NlllSqïÜÜÜåÄ‹FXký­Yÿÿÿ!ùÅ,ÿ‹ H° A‚˜XuÙ4c`YzH ôÀÈ’`4sP¬mPa«HŽÞÁQàO„™JBêS8¼”Ú#À$]"™ 3ªÎ® f0@Á*.¢â!’ ÅiÈøj‘ÊÇ¢^@b©ú VvLG’3Z¦èèsbB­Gp1xP¬)¹¶0êÐéG”1<514$T^·@¨‘Áɉ¨WtÐ$àfI"4ýö…¬;ŽÄdh•‡ÖKL@ que!*RXãƒx\T¦P;=PL"N²…ƒ*!­)!°@ƒ‡ø¬€q0 ;IssueTrackerProduct/www/bar.gif0000644000175000017500000000010411012074372016730 0ustar peterbepeterbeGIF89a€€ÿÿÿ!þCreated with The GIMP!ù ,D;IssueTrackerProduct/www/issueassignment.gif0000644000175000017500000000050711012074372021414 0ustar peterbepeterbeGIF89aÕ&œÛÜÛ=Ç=çîçÊ×ÊÈ­È˯ËÈÂÈȲȻµ»×3U3ÝÂÝ+K+®Ä®u̱̙@Z@—UÏUÞÝÞÙÝÙI¿I„Ž„2L2šÆš.X.5K5ÍÆÍf_f@f@DÄD>¢>AÄAÿÿÿÿÿÿ!ù&,d@“ph"DáÑXj–J’sê$E©Í Fô´J±‚€Ñ<6 ›BÉ8åo)C<*’ΡÊ,!@ |EN SVEr Žr]PDrFII_ž›]¢£¡¥Q¨BA;IssueTrackerProduct/www/gradissuehead.png0000644000175000017500000000044611012074372021024 0ustar peterbepeterbe‰PNG  IHDR4 •é”bKGDîîîŠið pHYs  šœtIMEÕ 8 ÆŒPtEXtCommentCreated with The GIMPïd%nŠIDATÓmA1Ã$†ÿÿ,o¢ Í¶=%ËÆsN„‚@Bß$UàýÁ¨S@$EJµ5RD ÖúH8¬rðÝ`Qm}¨9Ù’7ÝÖT_:v–Û«ÏŸ‚âÓ0Múw [i¹Á.±74°ãˆ±K(:ǧ Ó½>‹ú¼»mGÏ€IEND®B`‚IssueTrackerProduct/www/numbers/0000755000175000017500000000000011012074373017156 5ustar peterbepeterbeIssueTrackerProduct/www/numbers/7.gif0000644000175000017500000000154011012074372020012 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, =Hp࿃ LÈð߆Btà`ÁŠ>”#G‹1vI¤B‘K¦4©R#J—YN¼H3 ;IssueTrackerProduct/www/numbers/4.gif0000644000175000017500000000154611012074372020015 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, CH࿃þ¨°!Æ B<(¡ÄŠ /ˆ˜ÑâFŠ?v\è±dÆ‚!ŠÄ8²¥I—Y*”QäÄ‚8;IssueTrackerProduct/www/numbers/0.gif0000644000175000017500000000154211012074372020005 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, ?Hp࿃ LÈð߆Bt ¢Áƒ3V¤ˆq£ÆŽ 9Šü8Ò£É$IR”(bˆ/ÆTX°&€€;IssueTrackerProduct/www/numbers/2.gif0000644000175000017500000000154011012074372020005 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, =Hp࿃ LÈð߆Bt ¢Áƒ+bÔ(ÑâÆ†5zü8’$E†!Ar,hbG•[ª,H3 ;IssueTrackerProduct/www/numbers/9.gif0000644000175000017500000000154211012074372020016 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, ?Hp࿃ LÈð߆Bt ¢Áƒ3V¤ˆq£ÆŽ 9ŠüØñ¢D‹!Q¦¼˜âɆ/ÆTX°&€€;IssueTrackerProduct/www/numbers/1.gif0000644000175000017500000000153511012074372020010 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, :HP࿃L˜p!ÃFtH±bC 1fÜÈñßDA†ì8¢H‹M¦T¹2"Â0;IssueTrackerProduct/www/numbers/5.gif0000644000175000017500000000154411012074372020014 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, AHp࿃ LÈð߆Btà`ÁŠ-bTˆQbÄŽ?6|èQ£I†$;ŠÌ¨QbI”!¾ä8±åE‚;IssueTrackerProduct/www/numbers/3.gif0000644000175000017500000000154411012074372020012 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, AHp࿃ LÈð߆Btà`ÁŠJ”˜1"FŠ?z´(’¤I~T¹ÒàɉÆD9‘äÅ‚;IssueTrackerProduct/www/numbers/6.gif0000644000175000017500000000154211012074372020013 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, ?Hp࿃ LÈð߆Bt@¡Ä‹-”1cÁŽ=Š I’bɇ(Gš ‰qbɉÆdX°¦À€;IssueTrackerProduct/www/numbers/8.gif0000644000175000017500000000154211012074372020015 0ustar peterbepeterbeGIF89a ÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÿ, ?Hp࿃ LÈð߆Bt ¢Áƒ3V¤ˆq£ÆŽ 9v”øQdÉ“SZ$¹q¢Dˆ/ÆdX°¦À€;IssueTrackerProduct/www/close.gif0000644000175000017500000000013111012074372017271 0ustar peterbepeterbeGIF89a Âÿÿÿ™™™¥¦¥çççïïï, &¢¼®$ÊI½*¢(eb'}#EVÚ„†-hši_2î6ü“;IssueTrackerProduct/www/gradtablehead.png0000644000175000017500000000037211012074372020761 0ustar peterbepeterbe‰PNG  IHDR¢ÏÄbKGD«««jÝ pHYs  šœtIMEÕ XûMNtEXtCommentCreated with The GIMPïd%n^IDATWMÁÄ@Ã$ý7²Er@²?ð`ÛsND¥|$¥Â'¤¢´CÏ/ãaÍB/ñ=µD‚B%¡4@_Y¤IdŠ´7«FôËz;».•%³˜2AàP ë;q#sIEND®B`‚IssueTrackerProduct/www/menuicons/0000755000175000017500000000000011012074373017503 5ustar peterbepeterbeIssueTrackerProduct/www/menuicons/list.gif0000644000175000017500000000022711012074372021145 0ustar peterbepeterbeGIF89a³ ÀÀÀ´âüüÆlüjlfffÌÌ™™™™f™ÿÿÿ!ù ,D0ÉI«,8k ®ØEC!Ve§„¼0›¬m:Ñ šÀ±„ë¶™LÇ{É~AßÐU<.“È]SY þ ج+¾à𷓈;IssueTrackerProduct/www/menuicons/user.gif0000644000175000017500000000032311012074372021145 0ustar peterbepeterbeGIF89aÄ=@ a))zjN233™>>ŸAA ]]®ii´tt¹}}¾…a?šsg§zO¡yk¬Y°„u¹–t¾~……‘‘ÈššÍǫ̲š×¡Ô½¨å¬™ý¾©ýÀ¬ýÌ»ÿÿÿ!ù,Pà'Ždiž¨¨aØ”Š×ä8óJ\ί§sWg“‹Ø^™M$ò²(>ÆGñ4Uƒ(=”‰BÁ€ „ÇBÌnj·>îø<àÅïB;IssueTrackerProduct/www/menuicons/home.gif0000644000175000017500000000042711012074372021124 0ustar peterbepeterbeGIF89aÄ\šÜ̪¤„†„¼Öô”¾ìÜêü´êüdfd¼V,ÔN$´ztÌR$äòüJ¬ÌN¬ÎôÜ‚\üúüìòüä^,¤ÖôœÎô¼FÜrLÔR$ÄÞô¤:fÌdªìŒªÄÿÿÿÌ^>ŸAA ii´tt¹}}¾¡yk!ù,u %ŽdiZ5'é,I|´¢"Ý“@[Ô}»Iä7ØY‘bÇèX‹Ápbp€Ë‡ÃÑlJž+ÀáM€t@ze€< µ) Û!éK"r) {{ :#= Š’%e &r '!;IssueTrackerProduct/www/menuicons/complete.gif0000644000175000017500000000023711012074372022003 0ustar peterbepeterbeGIF89a³ ´âüüjlüÆl´¶´  œžœ|z|”’”ÌÌ™üþüÿÿÿ!ù ,LP¡I+URØ PˆrŒdi*¦®ë&p,Çn!3ýâ„‘> ¯–PôE¤*AÌåšN4syÁàù2”Ê"וbLèÑ€ÅfG;IssueTrackerProduct/www/menuicons/logout.gif0000644000175000017500000000025011012074372021477 0ustar peterbepeterbeGIF89aã kµsµÎ1{½1„ÆRœÎÜÜÜ÷ï¥÷÷Öÿ÷Æÿÿçÿÿÿÿÿÿÿÿÿÿÿÿ!ù,UðIé»XÄ•J@†Æö%K(JE©,¨*my0Œ:d´á¸Ð`7"Ô€¹¡r0¶K¥DÐC¢LÓúò#·ûÕƼ‰@[™¬¦ç÷ @•Ïe«;IssueTrackerProduct/www/menuicons/add.gif0000644000175000017500000000040111012074372020714 0ustar peterbepeterbeGIF89aÄ  $A454GHHZYY/Yxbbbmlmtst|||t¹†††ŽŽŽ<™á¡¡¡²±²¿¿¿ÈÈÈÎÎΙÓÿÓÓÓÙÙÙÞÝÞäääÿÿÿììíóóôøùøüüýÿÿÿ!ù,~ &Žd96hÃ(IbŠÍ'ÏÈñÆW~yÇW˜Œ™ÇÓ)„’â3iN8œ$IJ|:èf“‘ JˆOdLî~IƒÚic$RÁPy ø œ´®,j{#tu|ˆ ‘ŒŽ…j~šœ–#‰¥¥&š/"!;IssueTrackerProduct/www/menuicons/menusprites.gif0000644000175000017500000000247311012074372022555 0ustar peterbepeterbeGIF89a–Æz  =@$A  a((x))z45433™J¬GHH>>ŸAA f™/YxkµjN2f̤:ZYYsµÎ1t¹vYObbb¼F{½dfd]]®fff…a?mlmÌN¼V,ii´ÌR$ÔN$1„ÆtstÔR$ss¹|z|Ì^`VÛçð`ëíäÉÝùVûõ‹ è @ §ù xZȰ¡Ã‡j™H±"EAZ¾XœXFKG!ZþI²¤É?H"A™§¥Ë—.W"y)fL–5°ÈÃbgÏ2óüÑùGhÑEy‚Ú´©té˦yj. šÓ§OžIYeqô¨Ð¬3>d²ìH)Óª]Ëö!*þT–<|²dÆŒ! Ã7̇gú†¡áPL¾@ò:tR‡I\üã Éž†”pà@P‰Í(Õ(±`ƒ&Þðø[;IssueTrackerProduct/www/icons/chm.gif0000644000175000017500000000021611012074372020052 0ustar peterbepeterbeGIF89a¢€ÀÀÀ€€€€€ÿÿÿÿÿÿÿÿ!ù,Sxº+î,Qj B*jkW8q (JxhP™ƒ!D- ë{ʼ äXÏ€Ñ1bÊÀ9"‡²[j‘L%M‘äPP#džJ”ˆÝAÉT(³û=ußµH;IssueTrackerProduct/www/icons/exe.gif0000644000175000017500000000016311012074372020065 0ustar peterbepeterbeGIF89a¢¿ÀÀÀ€€€ÿÿÿÿÿÿ!ù,8XºÜ50Ê ƒ½ø1ø`ømŠ’år!Ï´ÌÖ8qçôÎÛ°ßÌ÷#òH®—`Él:Žè";IssueTrackerProduct/www/icons/pps.gif0000644000175000017500000000021311012074372020102 0ustar peterbepeterbeGIF89a¢€€€€ÀÀÀ÷÷÷ÿÿÿÿÿÿ!ù,P8:ÐîËÈ9mñcï W‚ÀuÔ`˜]gªkeN*%Å–Y&˜ì20È[dÅ£"@]2§ÔjÑ8Ëz—ÂAv[lŠ©>Ÿ“ºh/;IssueTrackerProduct/www/icons/doc.gif0000644000175000017500000000116311012074372020052 0ustar peterbepeterbeGIF89aÆ7ƒ5IcÊÛ÷ÒáùÿÿÿþþþXyƦµËîóû­Õy£è²ÁÕ¬»Ñx’Õš¶ÞØåú  ;à.98žÀø¡š°AÄ€PHXx¡!:À ð I†'BXØQP¢ hˆ˜Éð âK—¤ €å&Üð@5ÑJ”h ;IssueTrackerProduct/www/icons/gz.gif0000644000175000017500000000053111012074372017723 0ustar peterbepeterbeGIF89aÕ/Ó¯G¥”_ùé¼Ó°Góè¿°“;Ñ»}Æ®gÝÅyöêÂùêÁùê¿¡ƒ,ðá¶øêÂÛÉË©DêÛ«ãÓŸhi¼›<Ñ®F޲µ”9¶–9ΫEȦCªŒ4¤…/Àž>±’7Å¢A£†0Ò¯GÆ£Aá@Ù½h¬Œ3wx•¿=ºš;Ù¼g³´Òþ÷ÐgdT¢Œ?øð¿ÿÿÿ!ù/,vÀ—pH©@q©šXLC€tP|N(Œg*¼ÁBáHÉÃp`´Þð¸üíªÛïwzÅg­úvz.~+……,-v~|‡+‰‹Ž„{}|”xš.sq/acegikmB!"#%^KF$JBA;IssueTrackerProduct/www/icons/djvu.gif0000644000175000017500000000041311012074372020252 0ustar peterbepeterbeGIF89aÄžæ¡ ‰WkÖÆï$UH·ÿÿÿþÇRkN˜©½&fJÆÔÎÞ=«ß×èR­çåï)\³{RÆ!996½R­Æ½Þs”çç÷oJ½ÎÖcµ!ù,ˆ a,Bœè"®¢ZAE\,ƒÊŽŽñB<]ß‹HÁ áA|¿Õ‚ó‹hƒµLšD#Œ@­X±Ï-•øî \%„|µ4u²–ŒÀžnE"k zSJ"… TŠ€“"z™’J2< ˆ"(( '"!;IssueTrackerProduct/www/icons/fla.gif0000644000175000017500000000022211012074372020042 0ustar peterbepeterbeGIF89a¢ÿÿÿÿ3™™™™Ìÿÿÿÿÿ!ù,WXºÜ}0J§¸øBjÇÈ€u8Ý—MŒ‚c x«ö¼f&í¢‚Bô.Øåö{:¦GÍ ¯'ø1‘…ÒÐGT½Rañؼ%§\`¸f%b+’x/G©/;IssueTrackerProduct/www/icons/txt.png0000644000175000017500000000117611012074372020147 0ustar peterbepeterbe‰PNG  IHDR(-StIMEÔ &,¡ÃŠ pHYs  ÒÝ~ügAMA± üaMPLTEûûûœœ¶ÐÐßüüÿ´ˆ|ááèßßçÝÝåÜÜæääîççðååïááíÿÿÿ|UD¢xØØàÕÕÞÓÓÝÔÔßÝÝçÔ¡põÞÿ–‡××ßÐÐÚÏÏÙÎÎÚ””®÷ÃþÞ‘òÈz¾‹sÔÔÞËËצýÒNþÖXþËUé™NߨßÚÚäÏÏÚÎÎØÊÊ×þÚyþáOþÉBþ´OÚ¢†èèñââìÓÓßÌÌØýîßýÞ5þÉ6þ·8Üv/ààìÞÞëÚÚç••¯ýÓSþÖ*þ¼&þ3Ò£–ÜÜéÛÛèØØæýåÀýÜþ¹þ£Ûp/ÕÕäÒÒàûÆ'þÆþ­ø}а®ÎÎÝÌÌÛúØŸþÛþªþÙsA××åÉÉÚø½þ¹þòe œŒŸ÷ÏþÏþžþ‚ÔXùùþôíìúÁþ²þ´>Þ{9æ´Tþ周 ~;2#tRNS@æØf§IDATxÚc`ƒœÜ<(`âååMKÏÈÌÊF`NLJNIeDˆ‰‹O`d„ „†…GDFE#üüƒ‚Càîž^Þ>¾Lp{G'gW7˜€™¹…¥·µ­T@WOßÀÐÈØÄT* ¢ª¦®¡©¥­•“WP’”R‚ ˆŠ‰ H¤e |ü‚BÂ"ÿ›IEND®B`‚IssueTrackerProduct/www/icons/img.png0000644000175000017500000000136311012074372020102 0ustar peterbepeterbe‰PNG  IHDR(-StIMEÔ &,¡ÃŠ pHYs  ÒÝ~ügAMA± üa˜PLTEûûû””­ÆÆÕððôèèîææíääëààéßßèÜÜåÚÚä××âóóöççîååìááéÛÛåmnnvc`”Љ¦¨©µµ´¤£¤ŽŽ€€€rrr__aXZq–••´pcÓ»²Ûàá××ׯÆÅ³²²¨¨¨•••†††OQf´¶´ìé«£óüþßßßÎÎÍÇÇǶ´µ¡Ÿ ”””]_uÙÙãÖÖá·ÁÃàšÎgaóïïÁÒߊš­Á½¼’š˜y~~Œ‰‹_`uº¶³æt\¾72åÔÕÎäKx£›Ÿ£w§r@}Mu{z_`vÔÔàÒÒÞ»…}í[:Å6*Ê—œnÍë4…¸x—|¼g8¡6Mp]__vÑÑÝÎÎÛÁ}só‘nÜN;ž@LbÃã=˜ÆDZŠ Åšb»[8yNTUjËËÙÈÈÖ¯ZY÷Ê·ôŒp¯CG˜Õê\ºÞ>q¿Õßז@\nORvÐÐÜÄÄÓÂÂÑÌb`Â@;¼ˆ“ŒÄÛQ´Ú¨ÊÙÓãʑŎ¦±°ÍÍÚ¿¿ÐûûüøøúõõøîîòííòÉÉ×ôô÷êêðÞÞçæñ¬ötRNS@æØfÑIDATxÚc````‚F(`ªohmjkg‡‹™›ùÙ™Z‘šØØÈ-)-+¯¨¬ª®á¨­ ¤¥gdfeçäæå"£¢cbãâ“’SRAÆúøúù‡„†y‡Gìœ]\ÝÜ=<½¼&¦fæ–VÖ6¶v\ÜÆ@u M-m]=}C#€Œ¬œ¼‚¢’²Šª'7P@PHXDTL\BRJ"r) /?‡\€™……•®‚ 0Ì(ž^èýIEND®B`‚IssueTrackerProduct/www/icons/tar.gif0000644000175000017500000000053411012074372020074 0ustar peterbepeterbeGIF89aÕ0Ó¯Gùé¼Ó°GÛÃw°“;ãÓŸóè¿öꡃ,ðá¶Ñ»}£’]øêÂÆ®gùêÁÛÉË©DêÛ«ùê¿¥”_hi£†0¿=Ù¼gÙ½h¬Œ3³´ÒȦCΫEá@Å¢AÆ£Awx•Àž>Ñ®FÒ¯GªŒ4µ”9¶–9ºš;±’7޲¼›<¤…/þ÷ÐgdTøð¿¢Œ?ÿÿÿ!ù0,y@˜pHN—Eq©¡¤@C€T ‚x,'ŠT!À€ÄÁ8‘ÂCÑ@¼Þð¸üíªÛïwzkÏbµú{-ut~€…‚.„‡‡ƒ/.…~||‘ŒˆŽŠxs o0acegikmB#!*%+^KFJBA;IssueTrackerProduct/www/icons/deb.png0000644000175000017500000000135711012074372020063 0ustar peterbepeterbe‰PNG  IHDR(-StIMEÔ &,¡ÃŠ pHYs  ÒÝ~ügAMA± üažPLTEûûû“Fx2¯\ÿ²ÿÁ<Ú%IÑ~ ÿ¸ÿ·ÿ½4ÿÆDÿÍK²n= â‘ÿÁ)ÿÀ$ÿ¹!ÿ·+ÿÁ9ÿËHÿÓWÿ×V”T·`÷«+ÿË<ÿÊ7ÿÆ3ÿ¿*ÿ±#ÿ¼0ÿÅ?ÿÏOÿÙ_ÿÛ]µb å—ÿÖPÿÓJÿÑGÿÎAÿÄ3ÿ«ÿ¶)ÿ¿6ÿÊFÿÓVÿÜ_Áyê&ÿÛ_ÿÚZÿÖQÿÍEÿ¥ÿ¯ ÿ¸.ÿÄ=ÿÍMÿÖWÊ}î¤-ÿãnÿâkÿàdÿßaÿ±0ÿœÿ«ÿµ&ÿÇDÍ{ô«5ÿìÿé|ÿãrÿÄ@î“ÿ·,ÿ¸-ÿÊGÎxø²=ÿô‘ÿîˆÿÑZÿËOó&ÿÊNÿÆEÿÄ>Öuú·Eÿõ™ÿáuÿßp÷ª3ÿÄQÿ¿Qú¹Pâ¢Adž.œQ õ­7ÿÙsÿâ€ÿî’ÿÊ\ãˆÛ$à¢Fî¿fà¦O‡>å„ï¡)ÿé’ÿÏmî®Eÿô¢ÿÿ·å¶aê’ÿÔuÿÿ×ÿ߈íÅtï¨5ÿê˜ÿ÷«ÿÿÎæ¯Ræã“ çö,ptRNS@æØfÇIDATxÚc`€‚ŽNdÐÜÒÚÖŽàÖÕ744644A¹eå•UÕ55µìœÜ¼¼¼ü‚¢â’R@BbRrrrJjZzFfH ,<"2**:**&6.$àãëç`h, ptrvqus÷ðäöRñ ˜š™KXXZYÛØÚÙ;€44µ´utõô ŒM@Ò2²rò ŠJÊ*ªjê— ‹ˆŠ‰KHJAËÁÉÅÍÃËÇð 3 +;Šw™  Æx+^·v˜IEND®B`‚IssueTrackerProduct/www/icons/zpt.gif0000644000175000017500000000054411012074372020124 0ustar peterbepeterbeGIF89a¥ÿÿÿÿÿþüýýúûü÷ùúõøùóöøòõ÷ðôöëñóåíïÞèëÜæêÙäè×âæÒßãÐÝãÍÜáËÚßÉÙßÈØÝÆ×ÜÄÖÜÃÔÚÂÓÙÁÓÙÀÒÙ½ÑØ½Ð×¼Ï×»ÏÖ»ÎÕ¹ÍÔ¶ÌÔ¶ËÓµËÒ´ÊѲÈбÇаÇϯÆÎ®ÄέÄÍ«ÃÌ©Â˨ÁʦÀɤ¾È¢½Ç »ÅžºÄ›¿Ì˜¶Á®ºFgsÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ!ù ?,ÀŸpH,kÈ$ÒfüÕ€€€px2‹OÀ €Hd¯6 l3„52Óæh»¶EcFgÞn†ã¡ÌÖxm6?l6ˆŠx6 $ˆw!%)‘p#'+.‘bda(,/1ˆC5"&*-024±B5tÀÁ½‰jÅa?A;IssueTrackerProduct/www/icons/ppt.gif0000644000175000017500000000021311012074372020103 0ustar peterbepeterbeGIF89a¢€€€€€ÀÀÀÿÿÿÿÿÿÿ!ù,Ph ܮЀB+eŠÍw¸`|VEPC:œVB\÷,nÑùëÂ2šÍÒ£ðp¤_L0«ù<Åæ©D2…-èn™ÌfË+wAÝvÃèØX¢j»;IssueTrackerProduct/www/icons/pdf.gif0000644000175000017500000000202411012074372020053 0ustar peterbepeterbeGIF89a÷ÿúõîéÛ‹«ÿ ÿ ÿ ÿ ÿÿÿÛ×ÿ"%ÿ(+û/3ÿ25ÿ7:ÿ8;ÿ>@ÿBEÿIMçIJéLMÿTWÿbdÓRSâ^`ÿkmÿuwþ}€ápqÛ„†åæêª«Óeièšê¼¾Ø¶¹ïÚÜëÓÖïâåãâãÃÃ˾¾Æˆˆ²²¸¼¼ÂººÀ…uux¨¨¬ŠŠssurrtŒŒŽïðòéíñäíððÿÿÝ<;çHHíSSßONã[[åqqê}|骪èÛÛÜÛÛÿÿÿûûû÷÷÷öööõõõôôôóóóòòòñññîîîíííìììëëëêêêéééèèèçççæææåååäääâââáááàààßßßÞÞÞÝÝÝÜÜÜÛÛÛÚÚÚÙÙÙØØØÕÕÕÔÔÔÓÓÓÒÒÒÑÑÑÐÐÐÎÎÎÍÍÍÌÌÌËËËÈÈÈÇÇÇÆÆÆÅÅÅÄÄÄÃÃÃÂÂÂÀÀÀ½½½¼¼¼»»»···µµµ³³³²²²±±±°°°¯¯¯¬¬¬AAA&&&ÿÿÿ!ùŒ,ñ D#¨!ƒ† â"°!#=1dÔ°A#Ç 2†Â¢ƒ† .T á†2ˆ4а Áƒ8ÀÃÉÉCX¼2n‡& _ŽÛgßà‹‡â&&‰
hhøÐÅ ̨QcÆ8BLááA‰9n€°àÂC0 <¸!ñ†#f ô°€‚0o¬èPÁ„†$ˆˆsE‚P<å ÁE ˜9^$0áƒÖŽ)`¾8 `‚ gº º¶­‹pŸÊ(Öîݼ]ì`@öÅ\¼qW’ˆp†áˆõBñÀ„ #Ph6+YÅ‹Ï.Rh„†Ó¨3¨^m! ;IssueTrackerProduct/www/icons/html.gif0000644000175000017500000000203211012074372020245 0ustar peterbepeterbeGIF89a÷ÿÿÿJAB¨¡ªÍÊÎSOVi]s‘‘“Y[w‹Ž¸•™Ä“–Áª³õ–œÆ—žÉ‹‘¶“–©V[v™¡ÌÌÕÿ‰´o|®—¤Òs‚µš¨Ö˜¦Óvˆ»­ÙºÉôº½Å×Ùßz½Í󜣵}•È«Áð¯Ãð±ÅñµÈò·ÉòºÌ󆑪`hyÁÅÎ¥½î¨¿ïŽ” ·»Ãµ¹Á´¸Àbx¡œÏZct[duqzŠ~‡—ª°»ÂÆÍ„£Öƒ¢Õgpfo~—žªÌÏÔ÷úÿˆ«Þ‡©Üt‡¤†—±ìóýðöÿìîñDl duëñùøûÿàãç®Ðú±Òù´Ôú¶Õû´Ñô»ØúÂÜûÉàûËáüÌâüÐäüÍà÷ÔæüØèüÚêýÝëüåðþçñýëóýïöÿ±ÓúµÕú¸×û¾Úú¿Ûú­ÆáÄÞûÈàûÏäüÔçüàîýêóýîöÿíõþïöþñ÷þÈàøëôýîöþíõýñøÿòøþôùþ÷ûÿûýÿB“àw´êÌâ÷Õæöìõý]•D¦÷îöýñøþsÇ>Ugõúþ`t‚'˜ßw½è‰Ò4l‰Kdp„°¾ûþÿˆ›x}b}xYÓ¬Oð¿L»˜Fåª9ñ³Hê£/™w@x?üÃtý©=ú'à…+ç®r¼—rý€ù‚½n*é‹:ò–J¾f#·vI«Y#Ÿ`:åT~]OÂ;Ð;—^V·©§”ÿÿÿ!ù¯,÷_ H0ˆ!J^)\($@£%…Í)"dOLæÁsŽ›/^ÒscÅ…B€Ôi¦M @&0 àÃJ;FŒ¼ äGÎ-Y8ØÈ’)d t)Ä*^ dÓ*S§ˆIÂÔ(»xÒ4ÊP¢CŽ,ñ9ñUáŽ5¥6Z¤hÐ#JqL´}5ƒ +P¢LúiR™{Eh êS¨Tœ*Bbï5X ¸Z…Š”3PFìÕÆ "" ‰"&L‹½ª 93…Œ”1Oœ4aQ†Â 0d° ÁA $àÑAá=jИN½F?^;IssueTrackerProduct/www/icons/xls.gif0000644000175000017500000000061211012074372020111 0ustar peterbepeterbeGIF89a¥“¶HŠ6§»Ø`~\ìòýl­fY—KɨɍçãìüÓâú4Q–¥º²ÁÖ–Ä‘h£c8n"ôøþ—µáÞèÞÊÜø4g§¶Ì={!ÙæûÝéû]¡V7Ke®Ö6Jd÷÷÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ!ù ,§@°A$ ,H‹ÐS(<4C p¹DŒ°ðéz»•+£¡-8†Ï! c7ðÂÄQ4I…Ã6jn iƒz|… –˜_ •˜__¢  °¹¹ ¼ µ K £ ÈÂÄ •Ê•ÍÃB ØØ ÝÎÖ·âååÕêëìëA;IssueTrackerProduct/www/icons/py.png0000644000175000017500000000133711012074372017757 0ustar peterbepeterbe‰PNG  IHDRa~etIMEÔ &,¡ÃŠ pHYs  ÒÝ~ügAMA± üaŒPLTEûûûXXX000ÆÁ¶Ä½¬Ã¸£Ÿ¢t³2‰¯"…­$kw;ĬŠÞÞÞôíáòèØðäÐìÝÆyAl †«"_x)À©ˆ÷ñéôíâòèÙðäÑx‘>‚¢}£"Un¼«‚‚‚ØØÖÔÑÍÒÎÆ‹’kzŒFw˜r• xœ!Sk DXj‡$f…q&Xqn‹ a"n’!·$pŽYf=“­FrŽ%j„#•ªM—¦[|’7™¼/ol"—½$p’yž!wv\pŠ3›ºG¬Ã^ÂÕv¹È{—@ƒš5³Íd¹Íh–¯T‹­"r”!z—*ìÛ¼®—k‹&r‡:¾ÔqŸ»JÊÞ{€"vc•µ4°Äm¨ÊAvš!’•€ôðì\|d€!>Žª7®­;Ÿš¤ÁJ¥½T¡ÁB¢¾O‡¡PçàÙ¾¶¯dv>y™,ºÎkЍ/ªÁb‰§@©¸‰öõõâàÞ¼·±‰„ššš¤°¥¯Žøøøþþþ‰‰‰pppkkkBBBÿÿÿüüüÎÎΙ™™êêêmmm9?Š÷tRNS@æØfÉIDATxÚc`F`€F(fldDª‚¦æ:„D¨¾º¡®‘ *TÊSV^^QYU]S ÊÊÎÉÍË/(,*. ÅÆÅ'$&%§¤¦¥Çdd‚„ƒ‚CBÃ|Â#"£¢cüABŽNÎ.®nîž^Þ>¾~`!S3s K+k[;{9ˆñšZÚ:ºzúz†FÆr0å•”UTÕÔ5àBŒ¼gàcedˆôveaø÷ïCoÿD†·@ƒù¤ee„…„Àr¬ÌLÚ‚wA€‚/{öѸ·:ŒP  D¹“Çé 9cŒ¥<‰Û],ÛÊ[+J)„h­™§‘Ý:œ”úñ·Fˆièˆåþã' OŸ=g¸ÿà>ÃËgO¾]òíëg é, fÏ`P—ù•á5Ð'/ß``çäføúó7ïß>}üø €Xî~A^>ŽŸÿÞûÃðýËg†¯_>= –7oÞ1¤Ç„1X›38rœáå«— —._ah®­bð‹Ëkfá`øÇÊÁðèô¯¾‚cëóû7@—}?@,Fºš ·î?bH,©cøöå ÿß?˜þÿg¸òø ¿¨'/;Ã&†ß¿ÿ£ï'Ãg 7?¿Œâ?;ˆeÿé‹ Ï^¾b``çjþÇð¨YHH˜!/)†ÁÊP,,, _Qý÷߆–鋞=:ÿë·')A®§ˆ… ˜0ظ¸þ³~f``úÊðÿ?#ƒ¨¤ òÇÀüýòû'†ß¿~ÓÁ¿½lll Ä 4FF&†ÿAQó謿 ||üÀ€úË€€NGïßß¿™™î‚\@,‚ ‰ 4Gaq\ ƒŽª\seß\†‡Ï^0üþó hÈ/fÆ—ÌÌÌ Ä6ñÏo°fPò: Mm3—b¸¤¤d)Ç_FFF€‚𢙀 øœ˜1 ide. dbf‰ #++; ;'ÃoŽŸ ,ÿÿ2üþÇÈP˜É`¡¯ð?0n=xÂÐ2u8ŸüýóKˆÄòñëw†oÀ¤ùd*0Ë2cåý§ 3×ìcdðÐÿ™iƒ˜”Øòóó3㘧Z¸ÇaIEND®B`‚IssueTrackerProduct/www/icons/music.gif0000644000175000017500000000120511012074372020422 0ustar peterbepeterbeGIF89aÆm/s-0t.aaa8„4hhhNGRŠJRŒJRJTŒLTKTŽK7«#I¢0R¡DN§C‚‚‚‡‡‡ˆˆˆ‰‰‰f¥\j£`ŒŒŒl£j}˜yl¤ka¬a™{c´b———˜˜˜™™™   £££¤¤¤¥¥¥¦¦¦tÊn¨¨¨›³™©©©œ´šªªª«««¯¯¯–۰°—Ʊ±±²²²³³³´´´Ýl¶¶¶­½¬···®¾®¹¹¹ººº»»»¾¾¾¿¿¿ÀÀÀÁÁÁÆÎÅÃÒÂÌÌÌÎÎÎÖÖÖ×××ÒÛÐØØØÙÙÙÕÜÔÛÛÛÜÜÜÝÝÝÞÞÞßßßàààÜäÙáááâââãããääääåäçççäêãéééèìçìììïïïîðîðððòòòóóóòôòòöìôôôööö÷÷÷ùùùùú÷ùúúúúúûûûüüüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ!þIssueTrackerProduct.com!ù ,Ç€0(($ V‚‚0LLFDA&@»Ýÿäêò¯³»W`q‡©Üÿÿÿÿÿÿ!ù•,é+UšD°` F“ \8’Ç”&5!#ÊÂIà‰³`A  Šöœ´†’Fˆ$@€ÀB6~FιòdŒ#4 ÅÄŽ‘4rÜГ„‹ 1 YÁd¤}¦,Úe$Eiè@^dø¦­€H`c™;]òxyCm%g° (‚A‚‹Dˆê𱫣ŒÎ4@.(RH„»!Rì0 <4´°›BRÈqÓÀ»U] ýèDi) l­DEE þ,¡Àf„>àd àIÈ“x €A@;IssueTrackerProduct/www/icons/ini.gif0000644000175000017500000000206111012074372020062 0ustar peterbepeterbeGIF89a÷ÓöÿÿªªªÝíùïÿÿòÿÿÿÿÿU_p‘™¤sx€Í½šöö÷v~Œ¹ÝôôõöùüþêìïÓòúöõõR\m×½¡õÂRÕ­@îÿÿÔÔ丮Yëõô˜È탃„ëÿÿûÐuøýúÕÿÿ¯´¼¸²´ËÈËÇðúÝðúççëûýþÏòúáíùÚð÷àèêÄíù¹áõìéÔñÇeä°>g^GÆ‹Œ’¸˜Gò¾O€„‰p{ˆØÉ­ìÿÿª0üú÷ãä箕k„‚˜ÝÿÿÏ¥EÛ§(œ¢¬âÿÿнéáááĶ`½½Ô’—£´µÓ¸œL‡ŽšÇÇÇËÏÔÞíùÍð÷\HKtm£Êð÷Íç÷bk{êÿÿåÿÿçåæðÿÿâð÷ÔÕÙÅðúÄŽ êóû¸´Î¶ÙòÆÐÕfo¦n!´²®óÿÿòìÛ¨‰4äð÷ÛÛß̳k¨­¶Ù¦ØðúùùúÁ³W—’–î»EËœ.¡–¥ÙóûÝüþêýþèµ8ÖÏœ½áõÔð÷™”ššXµØòÉÀ€·Ûó¿ÁÆëõûáÍ ½ÅÊ¿Þôäß¹áâæàíùÙÿÿæÿÿÞðúõÿÿÙ¥¡`ëìîâãçúüþé¶:æäåÉÈÌôÿÿÒð÷ÏðúØð÷Ðð÷áèìÌç÷ðÅcßð÷bXEwv{ÖÍury_hx··Õ‹yj¡¥®Ï«T ¦±”…‡¦®¶Ç»fÇíùÝкá¨AÖíùig`±¿ÐÑž ›o,Ê‘’—¢ùã°åäã²ÕñF9A¸¦~µ—*ÏÖìÚíùÏÐêûûûÝð÷W>+ùÿÿ÷öõ•Na`i¡x*§m@[A<—¦LOZ³Ûóªm“˜£îèà`izò¾Pƒ‰•HReÍìøóôô|Œ³´Òÿÿÿ!ùÓ,ÿ§M³Ó‚Ï 6ŠœÂ °á´ ¡Ê”º…G[IzøaÔ.4ºnt!“J‡2QɱˆÄ "…P8*Pà‚fY  ”†…€?\LÈTƒÕD~M“¦iͪ\M¨Ê†šbf-å…c€$(¸° –« K–¦¸ÓG d4ÞŒÊc‹° K+iâ…Öf/êQ‡§¥xXc€éË¢aÂèyeÌÍÒI'(=Û#¥•`½j=ñ$b©¥¨ È¡s%‘¯XÁò„€´Ô‰–sªP±à³X´HV",òÁ!‡8 sDÆ\Ê "©6>9›’¦Ä.H-“`Ì%A@0ÙÑ@`@;IssueTrackerProduct/www/icons/gif.gif0000644000175000017500000000206011012074372020047 0ustar peterbepeterbeGIF89a÷›þÿÿÿüý†‡ÿûÿÿÿýƒƒÿûù… ûÿÿýþÿ÷ÿ÷ÿþýùÿûÿýþÿüÿ…‡ÿøýÿúýõÿöõÿù‡ x|ÿüø…ˆÿþøüþûöÿÿ„„ÿýú…ƒ…þþþ ÿýñ…÷ýó ‚ûÿúŠþýë ûûû ‚ÿÿñƒƒ„ ‰€ÿýû‚üþÿ…‡}ùÿÿ€„ùÿöÿþùüÿûöþÿ„ † ~ ‚‹úÿÿÿùðÿùÿ„ÿõÿ}…ýþøÿÿûñÿûþüÿÿÿó† ‡ ÈÈÈüýÿ„ } þÿúÿþúÿúÿ‹ýýóõÿÿÿ÷ÿ‘ÿþÿÿýÿÿÿÿÿÿÿ!ù›,ÿ7 Ô€‰ ¦š–Ð$°!&èÈD1A‰nJpÉÒKþP8IL }x`gУ8e:F¸„É0 `À! 0$ ‚ "…‘ Ȃ(wxÀ¡i–I$YPÀR–Œ\؃‰€6u °AÆÃSv¸i š…É‚ ‹ 8Œyƒ O\^8Q%C ¼¬¹ Å 0Vž@1Ñ™K’àÇG•Ä̉Ñh•JFØ0CAATð¹RaÈ MBÒ0% :fhs@K‹9bApIQ 0S†N ÐÃ(Á€ ?LÀ„) ;IssueTrackerProduct/www/icons/xml.gif0000644000175000017500000000034511012074372020106 0ustar peterbepeterbeGIF89aÄ€€ÀÀÀ3fÿ3™3™ÿ3ÿÿ3Ìÿÿ€¦Êð†††ñññ™3f™ÿÿÿÿÿÿ!ù,b ¤Œd9B¨ø¬l(©Ò²Ë³¬Ú|];@‡j‘¨[ñ!”%À£@@<ÅæÓ(ˆ¶ùØ ƒƒ» kˆ¸ò·óõzî†~¿§Ëîv>y||~+€n€wBAŽA!;IssueTrackerProduct/www/icons/mp3.gif0000644000175000017500000000111611012074372020002 0ustar peterbepeterbeGIF89aæCÉêÿôôôÌëÿë÷ÿÆéÿÜñÿîùÿØðÿßóÿòúÿÂèÿ¼ãý”””Þòÿ¾é¼è’ÀêµÝú•Âë¿æÿ—Äì°ÙøÄÄÄõûÿÙðÿºáü‹ºç·ßûh˜Ô Ëñ©ÔõŽÒþ¢ÎòœÇï™Åí5qÁžÉð¼åÿ/}Ïg¿ûžÉïøüÿ³Ûù›Çîq°èÛñÿy±æA”à²Ûù«Õö™Æî®×÷¤Ðó§ÒôÆ÷®Ø÷ˆˆˆÏìÿèöÿÜÜÜâôÿÒîÿåõÿÕïÿpppÐÐÐÿÿÿ!ùC,«€CC *7';‚ ) :>6,Ž“•—<8; «­?$AB0ª<@Á?=!»¡@@=92Æ‚3« @?@9ÑC1ÀÁÍÁÝÕÃÚÝ5-éÎì Ý4òë Ý ÔÑã7¡„†n.P¬AA4p趃A‹12%(À! C†l4$;IssueTrackerProduct/www/icons/zip.gif0000644000175000017500000000053311012074372020107 0ustar peterbepeterbeGIF89aÕ/Ó¯G¥”_ùé¼Ó°Góè¿°“;Ñ»}Æ®gÝÅyöêÂùêÁùê¿¡ƒ,ðá¶øêÂÛÉË©DêÛ«ãÓŸhi¼›<Ñ®F޲µ”9¶–9ΫEȦCªŒ4¤…/Àž>±’7Å¢A£†0Ò¯GÆ£Aá@Ù½h¬Œ3wx•¿=ºš;Ù¼g³´Òþ÷ÐgdTøð¿¢Œ?ÿÿÿ!ù/,xÀ—pH©@q©šXLC€tP|N(Œg*¼ÁBáHÉÃp`¸Þð¸üݪÛïw:k¿Ú³ú{ut+}„…,‚.-…†Ž‰-z„Ž€z|~ŠxœvsŸq/acegikmB!"#%^KF$JBA;IssueTrackerProduct/www/icons/zope.gif0000644000175000017500000000201311012074372020255 0ustar peterbepeterbeGIF89a÷œ*Je ­­¯Sx ±³µU{¥Bf@k˜]v–¿óóó}•¸ýýþ !6.Y~/9H;a†Y]`‚‚‚.;Gv˜¾+Ic£§dеNh‰Fj’-9/K+U} -I2H2R©¿ábfo',6$)0Q]uZ~§-8B 6T·º»úúû~ŸÉeŒ¸.IgX^d W­ g„¨ 7AN4e”-B *éêêåçéooramy,Ea"Hj &2œœ­ÁãFlºÇØþþþ–›žahn€‚‡XxœwšÅ¤¬NnŽTx¢ŠŽ—Jqœö÷ø^…¯9[€€€ÀÀÀÿÿÿÿÿÿ!ùœ,è9 ÄD° A9½ÈÈŒGri´ a‹BSÎÌé"è E„hªÈI›t"¹â6Éì3„ Ž,àQ¡Gš2m2ÒÄD(ÖlHÃA7͸t‚Èž/›~Œá¢ Í…¨„ ˜q²i Cl9ð&jw6É@B Z(•‰:‰L 7ªèÐQQ¢ú 朤„(g†¨;$ Y’ÇÇ‚$’ 0’%*§JŒ:à9A ^°d2͉Ÿ%”XY$ ‡¦ßBbÊD¼¸ñàe*_®< ;IssueTrackerProduct/www/icons/mpg.gif0000644000175000017500000000203311012074372020065 0ustar peterbepeterbeGIF89a÷ùüÿjk›ííí¿¿¿öóíÖÖÖžžžFÄîœ'}ß_µùïëöǧt€²ÿF"RRƒéÃ$óùÿÿýénnnøüÿ¾ÊÿêÆ/ö¥’²™©¯ÍíQm´ëÐ`¥¾¨acq™±'0ŽÆ¡ ÌåÿÙÙØoooìõÿeºþ`Ë•Âõ~ºÊÿõ›‰ÿ“vëÒdðëøXY‰÷X/”»cVV‡z~§¸Ï“cci’’ÿ}[B•äQx°Íÿóøÿd¡8N¡¤ÉìÉåÿüR'¸ê_aÓá½UV†ÈÔâ·ÌÿòË9ëöÿp2zðZwäòÿòòñ2qÓÈ¢—¹Ïÿ€_êÈ6sÀ½Êÿdºþ¿ßÿ:´qqqÂÉÙ„£Ö\’f{a…ªÚ[ƒˆ«Þ”‡3ÊØÿj»ÿÛíÿëÄ!Óéÿif^MÆŒz+[\‹ùT,ô¯›………&&&tx£654=X ­Íÿõèä«Íÿ\\ŒoržÖÜâq£ÈÚÚÜ©Îÿó®›'0Œ{+Š·Ý÷桎Æÿ¦Õÿqu˜üI}«°Úÿö©–ÖÕÒtm]’½ð×Úãµè‹¯í}­%hj—ݲˆÄâÿfh”~½h¼þ´ÌÿªÚZwÀþ‡©ÜýþÿÊÊÊmmmÿÿÿÿÿÿ!ù,ø; ÄD°`. ÈE&N#6È3`!CE™2jÌ ‚ŽŠ e@IRÀp28 ¡G„ 4ܼÀƃ*>–H¤„„8›:2…$K“º4ùcȉŠ;›èÄÈÑžEíðr© ’G›øjA`‚ l€‘CæL¥›0$PÐöM‰ 0 ‚ÇšM_˜ð³"Å" I\‰4gƦ-'d0€ˆ“ĘÀRçÁ¦'Ò@itÐ*Q<³Çt'=?²8`Á ˆD’ˆØa$i'ƒú|ð£͈MС×`( €¦ëس;IssueTrackerProduct/www/icons/rar.png0000644000175000017500000000053511012074372020112 0ustar peterbepeterbe‰PNG  IHDRãqÜötIMEÔ &,¡ÃŠ pHYs ð ðB¬4˜gAMA± üafPLTEÿÿÿ€€€ÀÜÀ@@@€€ @@`ÀÀ€@ÀÀÀÿûð``€ÀÀ`ÀÀ@ÀÀ à€à€  À  ¤@€@@`À`€¦Êð@ À  À` ` @ @€@à Àà€Àà`À£©PzIDATxÚmÌá‚ †áå’¨)j d˜Âýßd¸ ƒ§žŸ;û^pG÷q¬–zÍÜi%Ú6ï}ÁÀ"_Iüˆð\‘ÓÑ'ªí”iŽZzDJÍóSst’ÆËtŠ“¿Qш/Ž6„m«T×÷ƒàèE^3‘¢Åä'º+ÕÈQ )}2}AIEND®B`‚IssueTrackerProduct/www/icons/reg.gif0000644000175000017500000000020711012074372020060 0ustar peterbepeterbeGIF89a¢€€ÀÀÀ€€€ÿÿÿÿÿÿÿÿ!ù,Lh*ܮЈB+ "JkCÉÐÄQDI„ÓƒK¬ÒP¯¼ 6€Ë³J€0‰™pÁ Q–Cîz=§{“Êãq4 h‹€xLî*´èR;IssueTrackerProduct/www/icons/java.png0000644000175000017500000000164411012074372020251 0ustar peterbepeterbe‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<6IDATxÚbüÿÿ?2¸xñª8sæ:Cr²'#ˆ-i™Ä@,è þÿÿÇ ¦&ÅÀÅÅæÿúõŸáÏŸ _¾|c`bb‹Í»ý?Èÿ1Š7o>àüóç7;;\Œ™™h(fb€9ÖÜ\l#ã?F€‚´A¨1æû÷?`ÅÈ€‘‘,r È 0Ð’û €à*YYYÒe15ƒÌù^^V **b ZZò`>@1!üÎpH2üþý‹áÁƒ§ >cxûöÔvF ͦ¿|ùÎpãÆEE °>€‚ÀÁÁ¶X^^*‡……——hÈ3†cÇ.0ܾýî’¿Aüe––exÿþ X €àa°jÕN…?~ 1|ýúáÇÏ@ƒ¸DEÅ~þü ¶ùçÏ?Àfexôè>0 ÀúˆÙÄÄQáû÷_ë…„¸&ÈË‹D°³3þfæexùòÐÄÅ…ÁŽeb‚È·oïa Ápùò}€bÑÓS[÷Ö%ƒû‡–0üÿpŸá¿6ÃÙ‹W>üçc‰Œe““gX½zCTTÿÿ`A ŸO`@±pr²lŸÓÀ úã&0î\¾Épëág†ËÏ3lزƒ¡¨¸˜ÁÔÔ†aÇŽÀ˜bc°·7g¸v탿¿;Ø€búû÷ƒ¢ŽÃÍW¿®½øÍðèí7†wßþ1üøý˜¹€.cÐÑQepu5axñâÐ[¯Z0¼zõl@±€+¹¬›Á%8…áØÞ- /^¿ePÿü“AQU UŠ Ïž=¦ºu vÀ”zŸ!"ŸaÛ¶õ ––æ`ˆÙÕÕß@JJXƒ“›—AÏÄšAPL†AIEï¯_¿û[UU…áðáË :ÀD$ÏÀÉÉÌ ¤¤ÌxwˆY]Ýì&0t#xy9@€áÎ;À8~Ïðùóg†?½`‘‘g òzE”{……År?îÝ{Ê@Œ œœ6QQþzþÆ·oßÓÁ`Úgú×%s¡€1i.Qœ¿ IEND®B`‚IssueTrackerProduct/www/icons/sxc.png0000644000175000017500000000210011012074372020111 0ustar peterbepeterbe‰PNG  IHDRóÿagAMAÖØÔOX2tEXtSoftwareAdobe ImageReadyqÉe<ÒIDATxÚA¾ÿh„‘ÿðòôíñòùõôúöó ïAEàÿþþýýÿ ÿþ÷õøøú@A¾ÿSD=ö÷øñóòîíîìèæ 'üýó72E Ï×¹ðø× üúëíâqˆEYF’áç¯ß ï>¼gàcedˆôveaø´‰‰‰aÒ´v†wï^2 ó3((É2ÈÊ«1ñ 1ÝÄÀø[ A¾ÿæêíïòòççè÷óñìéë¿Á¼ó ù?>UêðÚñ÷Ýÿëüûòôî A¾ÿßãäåæçßàßéæåôðïöõó öœ‚ $ùù÷îñæÛé± ÿýööóñïôóˆå΃‡ ÷îÞe0ÔTc˜ÖVv62Èk0a‘äbæ`á``üËÊð÷;Ç7ÿ>øù €XÞ¼yÇÆ`mjÌpàÈq†—¯^2\º|…¡¹¶’!·U•AL–AHâÐE, ìL ?þmþÄÀðæõ_`Lü9@,Fºš ·î?bH,©cøöå ÿß?X˜2üÊ ¯ÉÎ %%ÆÀËËÊÀÄÌ Øÿ /ÞÞeøøñÃûÀ(þýo@±ì?}‘áÙËW@Óy€š‚ÿÿ3\ر™••…áÃïó ²BvÀ€db`d„xmîA}`à=Fý¿'öJÓO ;7/Ûæo ÀdÃP5ß’ÁÊéC’é[†y§…ááâs0I0üþñ€áÏŸ»ÙØØˆ‰™…è<&†@›þþcøÿ÷/ƒ”ìO†!š@4 ƒ§ ï_ @k˜î°²²2Ë? Ì? F0 L< äÛõnkŠÅ[”XyþúÃïŸÿ2¾¥€bùûçЀ?`›ÿƒ¬ùÿ¡m0ÝÿaÈqzÇ0eŸ\s¶ãk†¯ß?2üýóŸ•™í###@±üºçÏïß`C@šÿáû×L ììÿÁš~~ÿ7€‘‘™¬ì?#Ã*  –@~3Ó¨'A¦–…a”ó‹½ß£xáÓ×—  ´ÆÆÊ¨ D€bùŒº_À a`ag`Æ#0@#+|’“¹$øL˜™Ø‡ è® ß~¾aàúÉðhÂÏßÍøùù ½„¼ÐƒJîIEND®B`‚IssueTrackerProduct/www/icons/vsd.gif0000644000175000017500000000021011012074372020071 0ustar peterbepeterbeGIF89aò„„„ÆÆÆÿÿÿÿÿÿÿ!ù,MX Ü®°B+BÍû%Ù`"I ¨ rÖ¼D ’›iñÌV%.—@Š özD¸\‰'ŒQŠ#á˧êX9È$Z‘i¹ÝÌbJ&‹%©´:;IssueTrackerProduct/www/icons/dtml.gif0000644000175000017500000000161111012074372020243 0ustar peterbepeterbeGIF89a÷ÿÿPPP€€€ÀÀÀÿ€@ @€€€@@ÿÿÿÿ€€€@€€ÿÿÿ!ù,f@° AxÀ°áƒ…*tØpáÄ)fŒ8"à A"ÌH‘€É#øÈðdÊ+U¶DÙ1¦Ã•h*´I1@Δ:ÑcC¦LJêaÈ«X;IssueTrackerProduct/www/icons/tex.png0000644000175000017500000000063311012074372020125 0ustar peterbepeterbe‰PNG  IHDR(-StIMEÔ &,¡ÃŠ pHYs  ÒÝ~ügAMA± üaPLTEûûû””­ÆÆÕððôèèîææíääëààéßßèÜÜåÚÚä××âóóöççîååìááéÛÛåôô÷gpÀããê÷÷ùééïÙÙãÖÖáîîòÔÔàÒÒÞòòöêêðÑÑÝÎÎÛõõøûûüÐÐÜÄÄÓÂÂÑññõÍÍÚ¿¿ÐøøúííòÉÉ×ÞÞçw¯ÐtRNS@æØfIDATxÚMÍé‚ †a@¶Üý¤¤´´}¹ÿ lìúοçÌc”qÓ³ñÃÇD!“WeÄ›¯Àkén¼yeêûc>)ÚóåÊ~¹1 @Ï‚F éý€ÇÓ|2ÕÖèö‡¾l-Ò¬‹°U»h[8HdáÛ†”n6érY”º²u„DJ¥Í²ÁW}é ˜aŒúçIEND®B`‚IssueTrackerProduct/www/icons/sxi.png0000644000175000017500000000202311012074372020123 0ustar peterbepeterbe‰PNG  IHDRóÿagAMAÖØÔOX2tEXtSoftwareAdobe ImageReadyqÉe<¥IDATxÚA¾ÿh„‘ÿðòôíñòùõôúöóüít×ýþþüþÿ  ýòû÷ùú@A¾ÿ»ÈÎÿö÷øñóòîíîìèæ0÷ò,=G ߯¶çÔ øÿðç᱈EYF’áç¯ß ï>¼gàcedˆôveaø´‰‰‰aV_+Ç7/Dø”äee4Ø……X™X~±òhA¾ÿæêíïòòççè÷óñìéëÁÀ¼ùò÷:BU÷äØÿéÚû÷÷ÿ÷ïî A¾ÿßãäåæçßàßéæåôðïöõó󂜣 $úøöôëåäˬýþöõùóóõóˆå΃‡ ÷îÞe0ÔTc˜ÖVv62˜¦Ã +ÈÉ ÎËÆ ÎÇÎÀÊÁÊðí3Ã§ïŒ Ÿ¾þz@,oÞ¼cH c°65f8pä8ÃËW/.]¾ÂÐ\[ɰ0@šAS€•A†õƒÀ&ŽoŒ @ü¨ùý†Ï?þ #]M†[÷1$–Ô1|ûò…áßï \ÿ~0Ø]ìd0ãgdçdd`bd`ùËÀð÷ç_ _@š?10üþû@±ì?}‘áÙËW ì<@ÍÿþþÿÏi"Ç`Ö¾‡ãÉYV-'°w˜ÿÿcøßîÂðùôa†_ž0$-> @, øeçæe``Ëüá?0‘¤¦¦1~¸Íðš'Xãhxü/;Ê Tµá‡301ìæbcc &f éLÌL ÿ€6“Ãÿ¿äLœþvZÅPð·Vk°A?€Þ¦’;¬¬¬ Äòï0Z€Á40ñübF†_ÿ0DŒbÀÆ—ÌÀ4@,ÿüðló !@bËL@ÁifÖŒÀ<@L~ýbøóû7ØæÿPsæofø BÆü…›aÂÂ" ¢ˆåЀÀ̶ä@ © ’Ìæž`YòÕ @5§3T€AÔϨÏ D€bùŒº_ vf`Œ0e?yÆÊ 2 Eÿ_`H ÿz—ä¢/fÜüü :ì~T7xpIEND®B`‚IssueTrackerProduct/www/issuetracker_pop3account.gif0000644000175000017500000000107211012074372023213 0ustar peterbepeterbeGIF89aæ°{€áÐØ0./0-/ëæô$$&nnr  ¤ÁÁÄûûü! ¢§™-.+|q“˜‡ýÿÑþÿÐÿÿÈÿÿÉÿÿÌÿÿÍúúÉ÷÷ÆõõÅÿÿÏþþÎÄÄŸÿÿÐÿÿÑÿÿÕÿÿ×Ì̬ÿÿßttgÿÿõÿÿùÿÿûîîì1/ .*øÙ™ÿᥠ:97ýÛŸÿÞ¢ñϘðΗòÒ›*íÈðΛ.*%õ‡'Ì™fÿÌ™10/# '##,(%# "! /*'ÎÄÄ311ÿÿÿJJJÿÿÿ!ùL,—€LKƒ„…L‡‡K‹ŒŒt¸;IssueTrackerProduct/www/issuethreaddraft.gif0000644000175000017500000000021311012074372021526 0ustar peterbepeterbeGIF89aãÿÿÿïïïÏÏÏÀÀÀ»»»ªªªŠŠŠfffEEE111ÿÿÿÿÿÿÿÿÿÿÿÿ!ù,8ðÉI+]Ëj €¾K×}R(Ž›y&Û)"„ŹGàUª(Ø÷4‹ ƒ÷`Ž2`†dÂJç3$.I;IssueTrackerProduct/www/issuereportscript.gif0000644000175000017500000000041111012074372021776 0ustar peterbepeterbeGIF89a„  zÿ €<<< € €€€€€ žž<ÿ ÿ úúú!þ(c) IssueTrackerProduct !ù,jà'`ˆh:2€ÒœjJ,Ê`Ʋ¢|ßC§@ŽHHàÓ"“È%‚è@X¬øp8JFèX ׬`|8#aèòp‚!êuàDžãI{# OyJ#gOb}@ bcŠV>>!;IssueTrackerProduct/www/emailicon.gif0000644000175000017500000000206011012074372020127 0ustar peterbepeterbeGIF89a窫iQ³mT´mTµpV¶pV·sX¸tXºvZºwZ¼z\¾}^¿}_À€`ÁaƒbÄcćeŇeÆŠg…˜ÖÈiÇlËk   Í“m®™¢¡¡¡Ñ“l¢¢¢Ó™rÒšr“¨ÝÒœwÔžtË¡Š™«ÞÍ¢‡Ô¡zÖ¡vؤx£±ÔÚ§zÙ§ ²á̪™Ü«|Ù­ŠÞ¬‚߮ܮ‰Ý®‹ß°ƒà°†á±¬¼åᴅʹ­àµŽäµŠáµ“ß·”â¶’¸À×㹕ÆÀÀ溊ổÁÄÉ輌录㼠·Çç»ÇÞå¾àÀ¢±Êíæ¿›æÁ™ÆÉÓËÉÎæÄ¤éÄžàŲÁÎ×ÛÇºîÆŒ¿ÌìåȧÔËÉÚÌÆÁÕâðÌ–äίñÍ’ñÍ”ïÍ›ÚÐÆòИîÏ«íÑ­çÓµïÒ§ãÔ¿òÓìѹëÓ¶ëÒ½óÔžçÕºòÔ¢ôפôØ¥íÙ¾ëÚ¿ôÙ¬ôÚ¬ôÚ­ëÛÂÂåÿõÛªîÛÂõÝ­õÝ®ìÞÆÙàóìÞÖòÞÊîàËòßÉÌêÿ÷â·Üæöøä¹ðäÐñäÎøèÀ÷çÊÖîÿúëÅ÷êÖúìÅùìÍúíÈ÷í×àòÿúîÏ÷ìãûïÏûðÑûñÒûñÕüóÐúòáêöÿûôÞëöÿüõÛúóíñöüþ÷Ûõ÷üþúÜþúàõúÿÿûäÿûçþýåÿüëÿýëýþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ!þCreated by Peter!ù ÿ,ùÿ è§ÈŒj*¥ Cp„¸X(p Ž4…þì©çJ ) xtÄÇ0fØáBB„À HTÉÅÇŠ#>LÈPáß *d¦• R¢@vP(‰ ¡¦$’†n2ŠT 9.À8¥iGŸ©GíL‰!çNƒ -J‘â$# @†¨ sÇÍ‚ )H‰úD©ƒ‘GP6Ì©»&Á…{+õ@ƒÊT¨1/Ú¬ ƒà‚‰O‘~jê’¢24¶t1p!D#&‹8_bD(ÏU\ð ‘¥IŽ ÒóæŒ—*D\Á 0Pà@høÇAƒõëØ­sø;IssueTrackerProduct/www/paperclip.gif0000644000175000017500000000023111012074372020144 0ustar peterbepeterbeGIF89a ãÿÿÿVVVÌÌÌìììfffCCCóóó©©©™™™¾¾¾åååààà}}}ŠŠŠrrrÿÿÿ!ù, FðÉIk=ØNô€âVó(Ý"RÌ“4I—N„š2CâLÁsÄN}K€"0 Ôb”á#°8ä( Ð|R †…áPÐGWó(˜É;IssueTrackerProduct/TODO.txt0000644000175000017500000000135311012074372016166 0ustar peterbepeterbeShowSpambotPrevention into Advanced tab of Properties and complete work in AddIssue on it. ---- More work on the tell-a-friend feature. Especially making sure the error/success messages are shown up properly after. The URL might need better handling of the #i anchor link. Is saveEmailfriends() working properly? ---- IssueTracker Manager and IssueTracker User should automatically have the IssueTracker Access permission. Hmm... how does one do that? ---- Additional options at the bottom of Add Issue. These should be shown below the submit&preview buttons. The additional options could very well be: - Confidential option - Hide me option - Version number input - First status These should all be disabled by default. IssueTrackerProduct/standards/0000755000175000017500000000000011012074373016642 5ustar peterbepeterbeIssueTrackerProduct/standards/favicon.ico0000644000175000017500000000157611012074372020773 0ustar peterbepeterbeh( HHøÿûöÿùúÿùùÿúóùôúÿÿfju=CV/3P`e†³¸ÙÄÈäÊÎáúýÿúüüüþøëöîøÿûúÿùöþ÷øþùµº»AEP:“˜¹ÅÊë§«Ç£§¹òõýýÿÿ÷ùóøÿüøÿûðùï÷þ÷ûÿü…Š‹49B JNgÈËêÐÓò¨«Ç¥§¹øúÿÿÿÿýý÷ëõïñøóûÿûôûô®³±,01¢¦¿¸¼Ù·»Ø·»Ô·¹Ëûýÿúúúÿÿùòùôúÿüûÿû³º³ qv…¸¼Ô…¡³·ÓÃÇß½ÀÏûýÿúúúÿÿüùÿûõü÷ûÿü¸¾¹48935?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(J(){7(1e.19)L w=1e.19;L E=1e.19=J(a,b){K 1D E.2m.4Y(a,b)};7(1e.$)L D=1e.$;1e.$=E;L u=/^[^<]*(<(.|\\s)+>)[^>]*$|^#(\\w+)$/;L G=/^.[^:#\\[\\.]*$/;E.1i=E.2m={4Y:J(d,b){d=d||T;7(d.15){6[0]=d;6.M=1;K 6}N 7(1v d=="25"){L c=u.39(d);7(c&&(c[1]||!b)){7(c[1])d=E.5c([c[1]],b);N{L a=T.5N(c[3]);7(a)7(a.2s!=c[3])K E().2r(d);N{6[0]=a;6.M=1;K 6}N d=[]}}N K 1D E(b).2r(d)}N 7(E.1q(d))K 1D E(T)[E.1i.21?"21":"43"](d);K 6.6G(d.1n==1N&&d||(d.5j||d.M&&d!=1e&&!d.15&&d[0]!=10&&d[0].15)&&E.2H(d)||[d])},5j:"1.2.2",82:J(){K 6.M},M:0,22:J(a){K a==10?E.2H(6):6[a]},2E:J(b){L a=E(b);a.56=6;K a},6G:J(a){6.M=0;1N.2m.1h.1j(6,a);K 6},V:J(a,b){K E.V(6,a,b)},5E:J(b){L a=-1;6.V(J(i){7(6==b)a=i});K a},1K:J(c,a,b){L d=c;7(c.1n==4d)7(a==10)K 6.M&&E[b||"1K"](6[0],c)||10;N{d={};d[c]=a}K 6.V(J(i){P(c 1r d)E.1K(b?6.Y:6,c,E.1l(6,d[c],b,i,c))})},1m:J(b,a){7((b==\'29\'||b==\'1P\')&&2M(a)<0)a=10;K 6.1K(b,a,"2q")},1t:J(b){7(1v b!="4D"&&b!=W)K 6.4B().3t((6[0]&&6[0].2u||T).5v(b));L a="";E.V(b||6,J(){E.V(6.3p,J(){7(6.15!=8)a+=6.15!=1?6.6M:E.1i.1t([6])})});K a},5r:J(b){7(6[0])E(b,6[0].2u).5J().3n(6[0]).2a(J(){L a=6;2e(a.1B)a=a.1B;K a}).3t(6);K 6},8t:J(a){K 6.V(J(){E(6).6C().5r(a)})},8m:J(a){K 6.V(J(){E(6).5r(a)})},3t:J(){K 6.3P(1a,R,S,J(a){7(6.15==1)6.3k(a)})},6s:J(){K 6.3P(1a,R,R,J(a){7(6.15==1)6.3n(a,6.1B)})},6o:J(){K 6.3P(1a,S,S,J(a){6.1b.3n(a,6)})},5a:J(){K 6.3P(1a,S,R,J(a){6.1b.3n(a,6.2J)})},3h:J(){K 6.56||E([])},2r:J(b){L c=E.2a(6,J(a){K E.2r(b,a)});K 6.2E(/[^+>] [^+>]/.17(b)||b.1g("..")>-1?E.57(c):c)},5J:J(e){L f=6.2a(J(){7(E.14.1d&&!E.3W(6)){L a=6.6c(R),5u=T.2R("1u"),4T=T.2R("1u");5u.3k(a);4T.38=5u.38;K 4T.1B}N K 6.6c(R)});L d=f.2r("*").4R().V(J(){7(6[F]!=10)6[F]=W});7(e===R)6.2r("*").4R().V(J(i){7(6.15==3)K;L c=E.Q(6,"2N");P(L a 1r c)P(L b 1r c[a])E.16.1c(d[i],a,c[a][b],c[a][b].Q)});K f},1F:J(b){K 6.2E(E.1q(b)&&E.3x(6,J(a,i){K b.1O(a,i)})||E.3d(b,6))},4I:J(b){7(b.1n==4d)7(G.17(b))K 6.2E(E.3d(b,6,R));N b=E.3d(b,6);L a=b.M&&b[b.M-1]!==10&&!b.15;K 6.1F(J(){K a?E.35(6,b)<0:6!=b})},1c:J(a){K!a?6:6.2E(E.34(6.22(),a.1n==4d?E(a).22():a.M!=10&&(!a.12||E.12(a,"3i"))?a:[a]))},3K:J(a){K a?E.3d(a,6).M>0:S},7g:J(a){K 6.3K("."+a)},5P:J(b){7(b==10){7(6.M){L c=6[0];7(E.12(c,"2y")){L e=c.44,5L=[],11=c.11,30=c.U=="2y-30";7(e<0)K W;P(L i=30?e:0,2b=30?e+1:11.M;i<2b;i++){L d=11[i];7(d.2p){b=E.14.1d&&!d.9s.1C.9o?d.1t:d.1C;7(30)K b;5L.1h(b)}}K 5L}N K(6[0].1C||"").1p(/\\r/g,"")}K 10}K 6.V(J(){7(6.15!=1)K;7(b.1n==1N&&/5w|5y/.17(6.U))6.3o=(E.35(6.1C,b)>=0||E.35(6.37,b)>=0);N 7(E.12(6,"2y")){L a=b.1n==1N?b:[b];E("90",6).V(J(){6.2p=(E.35(6.1C,a)>=0||E.35(6.1t,a)>=0)});7(!a.M)6.44=-1}N 6.1C=b})},3q:J(a){K a==10?(6.M?6[0].38:W):6.4B().3t(a)},6P:J(a){K 6.5a(a).1Y()},6N:J(i){K 6.2V(i,i+1)},2V:J(){K 6.2E(1N.2m.2V.1j(6,1a))},2a:J(b){K 6.2E(E.2a(6,J(a,i){K b.1O(a,i,a)}))},4R:J(){K 6.1c(6.56)},3P:J(g,f,h,d){L e=6.M>1,3m;K 6.V(J(){7(!3m){3m=E.5c(g,6.2u);7(h)3m.8I()}L b=6;7(f&&E.12(6,"1V")&&E.12(3m[0],"4x"))b=6.3V("1S")[0]||6.3k(6.2u.2R("1S"));L c=E([]);E.V(3m,J(){L a=e?E(6).5J(R)[0]:6;7(E.12(a,"1o")){c=c.1c(a)}N{7(a.15==1)c=c.1c(E("1o",a).1Y());d.1O(b,a)}});c.V(6D)})}};E.2m.4Y.2m=E.2m;J 6D(i,a){7(a.3R)E.3Q({1f:a.3R,3l:S,1G:"1o"});N E.5l(a.1t||a.6A||a.38||"");7(a.1b)a.1b.2X(a)}E.1s=E.1i.1s=J(){L b=1a[0]||{},i=1,M=1a.M,5i=S,11;7(b.1n==8f){5i=b;b=1a[1]||{};i=2}7(1v b!="4D"&&1v b!="J")b={};7(M==1){b=6;i=0}P(;i-1}},6e:J(b,c,a){L e={};P(L d 1r c){e[d]=b.Y[d];b.Y[d]=c[d]}a.1O(b);P(L d 1r c)b.Y[d]=e[d]},1m:J(d,e,c){7(e=="29"||e=="1P"){L b,3S={3C:"4Z",4X:"23",18:"3u"},3r=e=="29"?["7P","7M"]:["7L","7K"];J 4S(){b=e=="29"?d.7J:d.7I;L a=0,3a=0;E.V(3r,J(){a+=2M(E.2q(d,"7H"+6,R))||0;3a+=2M(E.2q(d,"3a"+6+"62",R))||0});b-=1Z.7E(a+3a)}7(E(d).3K(":4b"))4S();N E.6e(d,3S,4S);K 1Z.2b(0,b)}K E.2q(d,e,c)},2q:J(e,k,j){L d;J 3y(b){7(!E.14.26)K S;L a=T.4a.4L(b,W);K!a||a.4K("3y")==""}7(k=="1y"&&E.14.1d){d=E.1K(e.Y,"1y");K d==""?"1":d}7(E.14.2B&&k=="18"){L c=e.Y.18;e.Y.18="3u";e.Y.18=c}7(k.1E(/4c/i))k=y;7(!j&&e.Y&&e.Y[k])d=e.Y[k];N 7(T.4a&&T.4a.4L){7(k.1E(/4c/i))k="4c";k=k.1p(/([A-Z])/g,"-$1").2w();L h=T.4a.4L(e,W);7(h&&!3y(e))d=h.4K(k);N{L f=[],2L=[];P(L a=e;a&&3y(a);a=a.1b)2L.4U(a);P(L i=0;i<2L.M;i++)7(3y(2L[i])){f[i]=2L[i].Y.18;2L[i].Y.18="3u"}d=k=="18"&&f[2L.M-1]!=W?"2D":(h&&h.4K(k))||"";P(L i=0;i]*?)\\/>/g,J(b,a,c){K c.1E(/^(7k|7h|5Q|7f|48|5O|a3|3v|9Y|9W|9T)$/i)?b:a+">"});L f=E.3f(d).2w(),1u=h.2R("1u");L e=!f.1g("<9R")&&[1,"<2y 78=\'78\'>",""]||!f.1g("<9O")&&[1,"<77>",""]||f.1E(/^<(9K|1S|9I|9F|9A)/)&&[1,"<1V>",""]||!f.1g("<4x")&&[2,"<1V><1S>",""]||(!f.1g("<9y")||!f.1g("<9v"))&&[3,"<1V><1S><4x>",""]||!f.1g("<5Q")&&[2,"<1V><1S><76>",""]||E.14.1d&&[1,"1u<1u>",""]||[0,"",""];1u.38=e[1]+d+e[2];2e(e[0]--)1u=1u.5D;7(E.14.1d){L g=!f.1g("<1V")&&f.1g("<1S")<0?1u.1B&&1u.1B.3p:e[1]=="<1V>"&&f.1g("<1S")<0?1u.3p:[];P(L j=g.M-1;j>=0;--j)7(E.12(g[j],"1S")&&!g[j].3p.M)g[j].1b.2X(g[j]);7(/^\\s/.17(d))1u.3n(h.5v(d.1E(/^\\s*/)[0]),1u.1B)}d=E.2H(1u.3p)}7(d.M===0&&(!E.12(d,"3i")&&!E.12(d,"2y")))K;7(d[0]==10||E.12(d,"3i")||d.11)k.1h(d);N k=E.34(k,d)});K k},1K:J(d,e,c){7(!d||d.15==3||d.15==8)K 10;L f=E.3W(d)?{}:E.3S;7(e=="2p"&&E.14.26)d.1b.44;7(f[e]){7(c!=10)d[f[e]]=c;K d[f[e]]}N 7(E.14.1d&&e=="Y")K E.1K(d.Y,"9r",c);N 7(c==10&&E.14.1d&&E.12(d,"3i")&&(e=="9q"||e=="9p"))K d.9n(e).6M;N 7(d.28){7(c!=10){7(e=="U"&&E.12(d,"48")&&d.1b)6Z"U 9i 9g\'t 9b 9a";d.99(e,""+c)}7(E.14.1d&&/6T|3R/.17(e)&&!E.3W(d))K d.4z(e,2);K d.4z(e)}N{7(e=="1y"&&E.14.1d){7(c!=10){d.6q=1;d.1F=(d.1F||"").1p(/6W\\([^)]*\\)/,"")+(2M(c).3D()=="93"?"":"6W(1y="+c*6S+")")}K d.1F&&d.1F.1g("1y=")>=0?(2M(d.1F.1E(/1y=([^)]*)/)[1])/6S).3D():""}e=e.1p(/-([a-z])/92,J(a,b){K b.2F()});7(c!=10)d[e]=c;K d[e]}},3f:J(a){K(a||"").1p(/^\\s+|\\s+$/g,"")},2H:J(b){L a=[];7(1v b!="91")P(L i=0,M=b.M;i*",6).1Y();2e(6.1B)6.2X(6.1B)}},J(a,b){E.1i[a]=J(){K 6.V(b,1a)}});E.V(["8e","62"],J(i,c){L b=c.2w();E.1i[b]=J(a){K 6[0]==1e?E.14.2B&&T.1k["5h"+c]||E.14.26&&1e["8d"+c]||T.6I=="6H"&&T.1I["5h"+c]||T.1k["5h"+c]:6[0]==T?1Z.2b(1Z.2b(T.1k["5g"+c],T.1I["5g"+c]),1Z.2b(T.1k["5f"+c],T.1I["5f"+c])):a==10?(6.M?E.1m(6[0],b):W):6.1m(b,a.1n==4d?a:a+"2P")}});L C=E.14.26&&4t(E.14.5n)<8c?"(?:[\\\\w*4s-]|\\\\\\\\.)":"(?:[\\\\w\\8b-\\8a*4s-]|\\\\\\\\.)",6w=1D 4r("^>\\\\s*("+C+"+)"),6v=1D 4r("^("+C+"+)(#)("+C+"+)"),6u=1D 4r("^([#.]?)("+C+"*)");E.1s({5d:{"":"m[2]==\'*\'||19.12(a,m[2])","#":"a.4z(\'2s\')==m[2]",":":{88:"im[3]-0",31:"m[3]-0==i",6N:"m[3]-0==i",3j:"i==0",3M:"i==r.M-1",6r:"i%2==0",6p:"i%2","3j-4m":"a.1b.3V(\'*\')[0]==a","3M-4m":"19.31(a.1b.5D,1,\'4v\')==a","84-4m":"!19.31(a.1b.5D,2,\'4v\')",6E:"a.1B",4B:"!a.1B",83:"(a.6A||a.80||19(a).1t()||\'\').1g(m[3])>=0",4b:\'"23"!=a.U&&19.1m(a,"18")!="2D"&&19.1m(a,"4X")!="23"\',23:\'"23"==a.U||19.1m(a,"18")=="2D"||19.1m(a,"4X")=="23"\',7Y:"!a.2W",2W:"a.2W",3o:"a.3o",2p:"a.2p||19.1K(a,\'2p\')",1t:"\'1t\'==a.U",5w:"\'5w\'==a.U",5y:"\'5y\'==a.U",5b:"\'5b\'==a.U",3J:"\'3J\'==a.U",59:"\'59\'==a.U",6n:"\'6n\'==a.U",6m:"\'6m\'==a.U",2G:\'"2G"==a.U||19.12(a,"2G")\',48:"/48|2y|6l|2G/i.17(a.12)",3E:"19.2r(m[3],a).M",7X:"/h\\\\d/i.17(a.12)",7W:"19.3x(19.3I,J(1i){K a==1i.O;}).M"}},6j:[/^(\\[) *@?([\\w-]+) *([!*$^~=]*) *(\'?"?)(.*?)\\4 *\\]/,/^(:)([\\w-]+)\\("?\'?(.*?(\\(.*?\\))?[^(]*?)"?\'?\\)/,1D 4r("^([:.#]*)("+C+"+)")],3d:J(a,c,b){L d,2o=[];2e(a&&a!=d){d=a;L f=E.1F(a,c,b);a=f.t.1p(/^\\s*,\\s*/,"");2o=b?c=f.r:E.34(2o,f.r)}K 2o},2r:J(t,p){7(1v t!="25")K[t];7(p&&p.15!=1&&p.15!=9)K[];p=p||T;L d=[p],2j=[],3M,12;2e(t&&3M!=t){L r=[];3M=t;t=E.3f(t);L o=S;L g=6w;L m=g.39(t);7(m){12=m[1].2F();P(L i=0;d[i];i++)P(L c=d[i].1B;c;c=c.2J)7(c.15==1&&(12=="*"||c.12.2F()==12))r.1h(c);d=r;t=t.1p(g,"");7(t.1g(" ")==0)6z;o=R}N{g=/^([>+~])\\s*(\\w*)/i;7((m=g.39(t))!=W){r=[];L l={};12=m[2].2F();m=m[1];P(L j=0,3g=d.M;j<3g;j++){L n=m=="~"||m=="+"?d[j].2J:d[j].1B;P(;n;n=n.2J)7(n.15==1){L h=E.Q(n);7(m=="~"&&l[h])1T;7(!12||n.12.2F()==12){7(m=="~")l[h]=R;r.1h(n)}7(m=="+")1T}}d=r;t=E.3f(t.1p(g,""));o=R}}7(t&&!o){7(!t.1g(",")){7(p==d[0])d.4k();2j=E.34(2j,d);r=d=[p];t=" "+t.6i(1,t.M)}N{L k=6v;L m=k.39(t);7(m){m=[0,m[2],m[3],m[1]]}N{k=6u;m=k.39(t)}m[2]=m[2].1p(/\\\\/g,"");L f=d[d.M-1];7(m[1]=="#"&&f&&f.5N&&!E.3W(f)){L q=f.5N(m[2]);7((E.14.1d||E.14.2B)&&q&&1v q.2s=="25"&&q.2s!=m[2])q=E(\'[@2s="\'+m[2]+\'"]\',f)[0];d=r=q&&(!m[3]||E.12(q,m[3]))?[q]:[]}N{P(L i=0;d[i];i++){L a=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];7(a=="*"&&d[i].12.2w()=="4D")a="3v";r=E.34(r,d[i].3V(a))}7(m[1]==".")r=E.58(r,m[2]);7(m[1]=="#"){L e=[];P(L i=0;r[i];i++)7(r[i].4z("2s")==m[2]){e=[r[i]];1T}r=e}d=r}t=t.1p(k,"")}}7(t){L b=E.1F(t,r);d=r=b.r;t=E.3f(b.t)}}7(t)d=[];7(d&&p==d[0])d.4k();2j=E.34(2j,d);K 2j},58:J(r,m,a){m=" "+m+" ";L c=[];P(L i=0;r[i];i++){L b=(" "+r[i].1w+" ").1g(m)>=0;7(!a&&b||a&&!b)c.1h(r[i])}K c},1F:J(t,r,h){L d;2e(t&&t!=d){d=t;L p=E.6j,m;P(L i=0;p[i];i++){m=p[i].39(t);7(m){t=t.7V(m[0].M);m[2]=m[2].1p(/\\\\/g,"");1T}}7(!m)1T;7(m[1]==":"&&m[2]=="4I")r=G.17(m[3])?E.1F(m[3],r,R).r:E(r).4I(m[3]);N 7(m[1]==".")r=E.58(r,m[2],h);N 7(m[1]=="["){L g=[],U=m[3];P(L i=0,3g=r.M;i<3g;i++){L a=r[i],z=a[E.3S[m[2]]||m[2]];7(z==W||/6T|3R|2p/.17(m[2]))z=E.1K(a,m[2])||\'\';7((U==""&&!!z||U=="="&&z==m[5]||U=="!="&&z!=m[5]||U=="^="&&z&&!z.1g(m[5])||U=="$="&&z.6i(z.M-m[5].M)==m[5]||(U=="*="||U=="~=")&&z.1g(m[5])>=0)^h)g.1h(a)}r=g}N 7(m[1]==":"&&m[2]=="31-4m"){L e={},g=[],17=/(-?)(\\d*)n((?:\\+|-)?\\d*)/.39(m[3]=="6r"&&"2n"||m[3]=="6p"&&"2n+1"||!/\\D/.17(m[3])&&"7U+"+m[3]||m[3]),3j=(17[1]+(17[2]||1))-0,d=17[3]-0;P(L i=0,3g=r.M;i<3g;i++){L j=r[i],1b=j.1b,2s=E.Q(1b);7(!e[2s]){L c=1;P(L n=1b.1B;n;n=n.2J)7(n.15==1)n.4p=c++;e[2s]=R}L b=S;7(3j==0){7(j.4p==d)b=R}N 7((j.4p-d)%3j==0&&(j.4p-d)/3j>=0)b=R;7(b^h)g.1h(j)}r=g}N{L f=E.5d[m[1]];7(1v f!="25")f=E.5d[m[1]][m[2]];f=4A("S||J(a,i){K "+f+"}");r=E.3x(r,f,h)}}K{r:r,t:t}},4w:J(b,c){L d=[];L a=b[c];2e(a&&a!=T){7(a.15==1)d.1h(a);a=a[c]}K d},31:J(a,e,c,b){e=e||1;L d=0;P(;a;a=a[c])7(a.15==1&&++d==e)1T;K a},5m:J(n,a){L r=[];P(;n;n=n.2J){7(n.15==1&&(!a||n!=a))r.1h(n)}K r}});E.16={1c:J(f,i,g,e){7(f.15==3||f.15==8)K;7(E.14.1d&&f.54!=10)f=1e;7(!g.2A)g.2A=6.2A++;7(e!=10){L h=g;g=J(){K h.1j(6,1a)};g.Q=e;g.2A=h.2A}L j=E.Q(f,"2N")||E.Q(f,"2N",{}),1x=E.Q(f,"1x")||E.Q(f,"1x",J(){L a;7(1v E=="10"||E.16.52)K a;a=E.16.1x.1j(1a.3G.O,1a);K a});1x.O=f;E.V(i.2d(/\\s+/),J(c,b){L a=b.2d(".");b=a[0];g.U=a[1];L d=j[b];7(!d){d=j[b]={};7(!E.16.2l[b]||E.16.2l[b].4i.1O(f)===S){7(f.3F)f.3F(b,1x,S);N 7(f.6h)f.6h("4h"+b,1x)}}d[g.2A]=g;E.16.2g[b]=R});f=W},2A:1,2g:{},1Y:J(e,h,f){7(e.15==3||e.15==8)K;L i=E.Q(e,"2N"),2f,5E;7(i){7(h==10)P(L g 1r i)6.1Y(e,g);N{7(h.U){f=h.2k;h=h.U}E.V(h.2d(/\\s+/),J(b,a){L c=a.2d(".");a=c[0];7(i[a]){7(f)2T i[a][f.2A];N P(f 1r i[a])7(!c[1]||i[a][f].U==c[1])2T i[a][f];P(2f 1r i[a])1T;7(!2f){7(!E.16.2l[a]||E.16.2l[a].4g.1O(e)===S){7(e.6f)e.6f(a,E.Q(e,"1x"),S);N 7(e.6d)e.6d("4h"+a,E.Q(e,"1x"))}2f=W;2T i[a]}}})}P(2f 1r i)1T;7(!2f){L d=E.Q(e,"1x");7(d)d.O=W;E.3H(e,"2N");E.3H(e,"1x")}}},1U:J(f,b,c,d,g){b=E.2H(b||[]);7(!c){7(6.2g[f])E("*").1c([1e,T]).1U(f,b)}N{7(c.15==3||c.15==8)K 10;L a,2f,1i=E.1q(c[f]||W),16=!b[0]||!b[0].32;7(16)b.4U(6.51({U:f,2K:c}));b[0].U=f;7(E.1q(E.Q(c,"1x")))a=E.Q(c,"1x").1j(c,b);7(!1i&&c["4h"+f]&&c["4h"+f].1j(c,b)===S)a=S;7(16)b.4k();7(g&&E.1q(g)){2f=g.1j(c,a==W?b:b.6Q(a));7(2f!==10)a=2f}7(1i&&d!==S&&a!==S&&!(E.12(c,\'a\')&&f=="50")){6.52=R;1R{c[f]()}1W(e){}}6.52=S}K a},1x:J(c){L a;c=E.16.51(c||1e.16||{});L b=c.U.2d(".");c.U=b[0];L f=E.Q(6,"2N")&&E.Q(6,"2N")[c.U],3B=1N.2m.2V.1O(1a,1);3B.4U(c);P(L j 1r f){L d=f[j];3B[0].2k=d;3B[0].Q=d.Q;7(!b[1]||d.U==b[1]){L e=d.1j(6,3B);7(a!==S)a=e;7(e===S){c.32();c.41()}}}7(E.14.1d)c.2K=c.32=c.41=c.2k=c.Q=W;K a},51:J(c){L a=c;c=E.1s({},a);c.32=J(){7(a.32)a.32();a.7T=S};c.41=J(){7(a.41)a.41();a.7S=R};7(!c.2K)c.2K=c.7R||T;7(c.2K.15==3)c.2K=a.2K.1b;7(!c.4W&&c.4V)c.4W=c.4V==c.2K?c.7Q:c.4V;7(c.6b==W&&c.6a!=W){L b=T.1I,1k=T.1k;c.6b=c.6a+(b&&b.2i||1k&&1k.2i||0)-(b.68||0);c.7O=c.7N+(b&&b.2x||1k&&1k.2x||0)-(b.67||0)}7(!c.3r&&((c.4f||c.4f===0)?c.4f:c.66))c.3r=c.4f||c.66;7(!c.65&&c.64)c.65=c.64;7(!c.3r&&c.2G)c.3r=(c.2G&1?1:(c.2G&2?3:(c.2G&4?2:0)));K c},2l:{21:{4i:J(){5A();K},4g:J(){K}},47:{4i:J(){7(E.14.1d)K S;E(6).2z("4Q",E.16.2l.47.2k);K R},4g:J(){7(E.14.1d)K S;E(6).42("4Q",E.16.2l.47.2k);K R},2k:J(a){7(I(a,6))K R;1a[0].U="47";K E.16.1x.1j(6,1a)}},46:{4i:J(){7(E.14.1d)K S;E(6).2z("4P",E.16.2l.46.2k);K R},4g:J(){7(E.14.1d)K S;E(6).42("4P",E.16.2l.46.2k);K R},2k:J(a){7(I(a,6))K R;1a[0].U="46";K E.16.1x.1j(6,1a)}}}};E.1i.1s({2z:J(c,a,b){K c=="4O"?6.30(c,a,b):6.V(J(){E.16.1c(6,c,b||a,b&&a)})},30:J(d,b,c){K 6.V(J(){E.16.1c(6,d,J(a){E(6).42(a);K(c||b).1j(6,1a)},c&&b)})},42:J(a,b){K 6.V(J(){E.16.1Y(6,a,b)})},1U:J(c,a,b){K 6.V(J(){E.16.1U(c,a,6,R,b)})},63:J(c,a,b){7(6[0])K E.16.1U(c,a,6[0],S,b);K 10},2h:J(){L b=1a;K 6.50(J(a){6.4N=0==6.4N?1:0;a.32();K b[6.4N].1j(6,1a)||S})},7F:J(a,b){K 6.2z(\'47\',a).2z(\'46\',b)},21:J(a){5A();7(E.2Q)a.1O(T,E);N E.3w.1h(J(){K a.1O(6,E)});K 6}});E.1s({2Q:S,3w:[],21:J(){7(!E.2Q){E.2Q=R;7(E.3w){E.V(E.3w,J(){6.1j(T)});E.3w=W}E(T).63("21")}}});L x=S;J 5A(){7(x)K;x=R;7(T.3F&&!E.14.2B)T.3F("61",E.21,S);7(E.14.1d&&1e==3b)(J(){7(E.2Q)K;1R{T.1I.7D("2c")}1W(3e){3z(1a.3G,0);K}E.21()})();7(E.14.2B)T.3F("61",J(){7(E.2Q)K;P(L i=0;i=0){L i=g.2V(e,g.M);g=g.2V(0,e)}c=c||J(){};L f="4J";7(d)7(E.1q(d)){c=d;d=W}N{d=E.3v(d);f="5Z"}L h=6;E.3Q({1f:g,U:f,1G:"3q",Q:d,1z:J(a,b){7(b=="1X"||b=="5Y")h.3q(i?E("<1u/>").3t(a.4e.1p(/<1o(.|\\s)*?\\/1o>/g,"")).2r(i):a.4e);h.V(c,[a.4e,b,a])}});K 6},7q:J(){K E.3v(6.5X())},5X:J(){K 6.2a(J(){K E.12(6,"3i")?E.2H(6.7p):6}).1F(J(){K 6.37&&!6.2W&&(6.3o||/2y|6l/i.17(6.12)||/1t|23|3J/i.17(6.U))}).2a(J(i,c){L b=E(6).5P();K b==W?W:b.1n==1N?E.2a(b,J(a,i){K{37:c.37,1C:a}}):{37:c.37,1C:b}}).22()}});E.V("5W,5V,5U,69,5T,5S".2d(","),J(i,o){E.1i[o]=J(f){K 6.2z(o,f)}});L B=(1D 3O).3N();E.1s({22:J(d,b,a,c){7(E.1q(b)){a=b;b=W}K E.3Q({U:"4J",1f:d,Q:b,1X:a,1G:c})},7o:J(b,a){K E.22(b,W,a,"1o")},7n:J(c,b,a){K E.22(c,b,a,"2O")},7m:J(d,b,a,c){7(E.1q(b)){a=b;b={}}K E.3Q({U:"5Z",1f:d,Q:b,1X:a,1G:c})},7Z:J(a){E.1s(E.4H,a)},4H:{2g:R,U:"4J",2U:0,5R:"49/x-7j-3i-7i",6x:R,3l:R,Q:W,6t:W,3J:W,4n:{3L:"49/3L, 1t/3L",3q:"1t/3q",1o:"1t/4l, 49/4l",2O:"49/2O, 1t/4l",1t:"1t/7e",4o:"*/*"}},4q:{},3Q:J(s){L f,2Y=/=\\?(&|$)/g,1A,Q;s=E.1s(R,s,E.1s(R,{},E.4H,s));7(s.Q&&s.6x&&1v s.Q!="25")s.Q=E.3v(s.Q);7(s.1G=="4u"){7(s.U.2w()=="22"){7(!s.1f.1E(2Y))s.1f+=(s.1f.1E(/\\?/)?"&":"?")+(s.4u||"7d")+"=?"}N 7(!s.Q||!s.Q.1E(2Y))s.Q=(s.Q?s.Q+"&":"")+(s.4u||"7d")+"=?";s.1G="2O"}7(s.1G=="2O"&&(s.Q&&s.Q.1E(2Y)||s.1f.1E(2Y))){f="4u"+B++;7(s.Q)s.Q=(s.Q+"").1p(2Y,"="+f+"$1");s.1f=s.1f.1p(2Y,"="+f+"$1");s.1G="1o";1e[f]=J(a){Q=a;1X();1z();1e[f]=10;1R{2T 1e[f]}1W(e){}7(h)h.2X(g)}}7(s.1G=="1o"&&s.1Q==W)s.1Q=S;7(s.1Q===S&&s.U.2w()=="22"){L i=(1D 3O()).3N();L j=s.1f.1p(/(\\?|&)4s=.*?(&|$)/,"$a2="+i+"$2");s.1f=j+((j==s.1f)?(s.1f.1E(/\\?/)?"&":"?")+"4s="+i:"")}7(s.Q&&s.U.2w()=="22"){s.1f+=(s.1f.1E(/\\?/)?"&":"?")+s.Q;s.Q=W}7(s.2g&&!E.5M++)E.16.1U("5W");7((!s.1f.1g("9Z")||!s.1f.1g("//"))&&(s.1G=="1o"||s.1G=="2O")&&s.U.2w()=="22"){L h=T.3V("6k")[0];L g=T.2R("1o");g.3R=s.1f;7(s.7c)g.9X=s.7c;7(!f){L l=S;g.9V=g.9U=J(){7(!l&&(!6.3c||6.3c=="60"||6.3c=="1z")){l=R;1X();1z();h.2X(g)}}}h.3k(g);K 10}L m=S;L k=1e.7a?1D 7a("9S.9Q"):1D 79();k.9P(s.U,s.1f,s.3l,s.6t,s.3J);1R{7(s.Q)k.4G("9N-9M",s.5R);7(s.5I)k.4G("9L-5H-9J",E.4q[s.1f]||"9H, 9G 9E 9B 5G:5G:5G 9z");k.4G("X-9x-9u","79");k.4G("9t",s.1G&&s.4n[s.1G]?s.4n[s.1G]+", */*":s.4n.4o)}1W(e){}7(s.75)s.75(k);7(s.2g)E.16.1U("5S",[k,s]);L c=J(a){7(!m&&k&&(k.3c==4||a=="2U")){m=R;7(d){74(d);d=W}1A=a=="2U"&&"2U"||!E.73(k)&&"3e"||s.5I&&E.72(k,s.1f)&&"5Y"||"1X";7(1A=="1X"){1R{Q=E.71(k,s.1G)}1W(e){1A="5C"}}7(1A=="1X"){L b;1R{b=k.5B("70-5H")}1W(e){}7(s.5I&&b)E.4q[s.1f]=b;7(!f)1X()}N E.5t(s,k,1A);1z();7(s.3l)k=W}};7(s.3l){L d=54(c,13);7(s.2U>0)3z(J(){7(k){k.9m();7(!m)c("2U")}},s.2U)}1R{k.9l(s.Q)}1W(e){E.5t(s,k,W,e)}7(!s.3l)c();J 1X(){7(s.1X)s.1X(Q,1A);7(s.2g)E.16.1U("5T",[k,s])}J 1z(){7(s.1z)s.1z(k,1A);7(s.2g)E.16.1U("5U",[k,s]);7(s.2g&&!--E.5M)E.16.1U("5V")}K k},5t:J(s,a,b,e){7(s.3e)s.3e(a,b,e);7(s.2g)E.16.1U("69",[a,s,e])},5M:0,73:J(r){1R{K!r.1A&&9k.9j=="5b:"||(r.1A>=6Y&&r.1A<9h)||r.1A==6X||r.1A==9e||E.14.26&&r.1A==10}1W(e){}K S},72:J(a,c){1R{L b=a.5B("70-5H");K a.1A==6X||b==E.4q[c]||E.14.26&&a.1A==10}1W(e){}K S},71:J(r,b){L c=r.5B("9d-U");L d=b=="3L"||!b&&c&&c.1g("3L")>=0;L a=d?r.9c:r.4e;7(d&&a.1I.28=="5C")6Z"5C";7(b=="1o")E.5l(a);7(b=="2O")a=4A("("+a+")");K a},3v:J(a){L s=[];7(a.1n==1N||a.5j)E.V(a,J(){s.1h(3s(6.37)+"="+3s(6.1C))});N P(L j 1r a)7(a[j]&&a[j].1n==1N)E.V(a[j],J(){s.1h(3s(j)+"="+3s(6))});N s.1h(3s(j)+"="+3s(a[j]));K s.6g("&").1p(/%20/g,"+")}});E.1i.1s({1J:J(c,b){K c?6.27({1P:"1J",29:"1J",1y:"1J"},c,b):6.1F(":23").V(J(){6.Y.18=6.5x||"";7(E.1m(6,"18")=="2D"){L a=E("<"+6.28+" />").6B("1k");6.Y.18=a.1m("18");7(6.Y.18=="2D")6.Y.18="3u";a.1Y()}}).3h()},1H:J(b,a){K b?6.27({1P:"1H",29:"1H",1y:"1H"},b,a):6.1F(":4b").V(J(){6.5x=6.5x||E.1m(6,"18");6.Y.18="2D"}).3h()},6U:E.1i.2h,2h:J(a,b){K E.1q(a)&&E.1q(b)?6.6U(a,b):a?6.27({1P:"2h",29:"2h",1y:"2h"},a,b):6.V(J(){E(6)[E(6).3K(":23")?"1J":"1H"]()})},98:J(b,a){K 6.27({1P:"1J"},b,a)},97:J(b,a){K 6.27({1P:"1H"},b,a)},96:J(b,a){K 6.27({1P:"2h"},b,a)},95:J(b,a){K 6.27({1y:"1J"},b,a)},94:J(b,a){K 6.27({1y:"1H"},b,a)},9f:J(c,a,b){K 6.27({1y:a},c,b)},27:J(l,k,j,h){L i=E.6V(k,j,h);K 6[i.2S===S?"V":"2S"](J(){7(6.15!=1)K S;L g=E.1s({},i);L f=E(6).3K(":23"),4y=6;P(L p 1r l){7(l[p]=="1H"&&f||l[p]=="1J"&&!f)K E.1q(g.1z)&&g.1z.1j(6);7(p=="1P"||p=="29"){g.18=E.1m(6,"18");g.36=6.Y.36}}7(g.36!=W)6.Y.36="23";g.40=E.1s({},l);E.V(l,J(c,a){L e=1D E.2v(4y,g,c);7(/2h|1J|1H/.17(a))e[a=="2h"?f?"1J":"1H":a](l);N{L b=a.3D().1E(/^([+-]=)?([\\d+-.]+)(.*)$/),24=e.2o(R)||0;7(b){L d=2M(b[2]),2C=b[3]||"2P";7(2C!="2P"){4y.Y[c]=(d||1)+2C;24=((d||1)/e.2o(R))*24;4y.Y[c]=24+2C}7(b[1])d=((b[1]=="-="?-1:1)*d)+24;e.3Z(24,d,2C)}N e.3Z(24,a,"")}});K R})},2S:J(a,b){7(E.1q(a)||(a&&a.1n==1N)){b=a;a="2v"}7(!a||(1v a=="25"&&!b))K A(6[0],a);K 6.V(J(){7(b.1n==1N)A(6,a,b);N{A(6,a).1h(b);7(A(6,a).M==1)b.1j(6)}})},8Z:J(b,c){L a=E.3I;7(b)6.2S([]);6.V(J(){P(L i=a.M-1;i>=0;i--)7(a[i].O==6){7(c)a[i](R);a.6R(i,1)}});7(!c)6.5z();K 6}});L A=J(b,c,a){7(!b)K 10;c=c||"2v";L q=E.Q(b,c+"2S");7(!q||a)q=E.Q(b,c+"2S",a?E.2H(a):[]);K q};E.1i.5z=J(a){a=a||"2v";K 6.V(J(){L q=A(6,a);q.4k();7(q.M)q[0].1j(6)})};E.1s({6V:J(b,a,c){L d=b&&b.1n==8Y?b:{1z:c||!c&&a||E.1q(b)&&b,2t:b,3Y:c&&a||a&&a.1n!=8W&&a};d.2t=(d.2t&&d.2t.1n==53?d.2t:{9w:8U,8T:6Y}[d.2t])||8S;d.5o=d.1z;d.1z=J(){7(d.2S!==S)E(6).5z();7(E.1q(d.5o))d.5o.1j(6)};K d},3Y:{6O:J(p,n,b,a){K b+a*p},5F:J(p,n,b,a){K((-1Z.9C(p*1Z.9D)/2)+0.5)*a+b}},3I:[],3T:W,2v:J(b,c,a){6.11=c;6.O=b;6.1l=a;7(!c.3U)c.3U={}}});E.2v.2m={4C:J(){7(6.11.33)6.11.33.1j(6.O,[6.2I,6]);(E.2v.33[6.1l]||E.2v.33.4o)(6);7(6.1l=="1P"||6.1l=="29")6.O.Y.18="3u"},2o:J(a){7(6.O[6.1l]!=W&&6.O.Y[6.1l]==W)K 6.O[6.1l];L r=2M(E.1m(6.O,6.1l,a));K r&&r>-8N?r:2M(E.2q(6.O,6.1l))||0},3Z:J(c,b,d){6.5s=(1D 3O()).3N();6.24=c;6.3h=b;6.2C=d||6.2C||"2P";6.2I=6.24;6.4E=6.4F=0;6.4C();L e=6;J t(a){K e.33(a)}t.O=6.O;E.3I.1h(t);7(E.3T==W){E.3T=54(J(){L a=E.3I;P(L i=0;i6.11.2t+6.5s){6.2I=6.3h;6.4E=6.4F=1;6.4C();6.11.40[6.1l]=R;L b=R;P(L i 1r 6.11.40)7(6.11.40[i]!==R)b=S;7(b){7(6.11.18!=W){6.O.Y.36=6.11.36;6.O.Y.18=6.11.18;7(E.1m(6.O,"18")=="2D")6.O.Y.18="3u"}7(6.11.1H)6.O.Y.18="2D";7(6.11.1H||6.11.1J)P(L p 1r 6.11.40)E.1K(6.O.Y,p,6.11.3U[p])}7(b&&E.1q(6.11.1z))6.11.1z.1j(6.O);K S}N{L n=t-6.5s;6.4F=n/6.11.2t;6.4E=E.3Y[6.11.3Y||(E.3Y.5F?"5F":"6O")](6.4F,n,0,1,6.11.2t);6.2I=6.24+((6.3h-6.24)*6.4E);6.4C()}K R}};E.2v.33={2i:J(a){a.O.2i=a.2I},2x:J(a){a.O.2x=a.2I},1y:J(a){E.1K(a.O.Y,"1y",a.2I)},4o:J(a){a.O.Y[a.1l]=a.2I+a.2C}};E.1i.5f=J(){L b=0,3b=0,O=6[0],5q;7(O)8K(E.14){L d=O.1b,45=O,1M=O.1M,1L=O.2u,5p=26&&4t(5n)<8H,2Z=E.1m(O,"3C")=="2Z";7(O.7b){L c=O.7b();1c(c.2c+1Z.2b(1L.1I.2i,1L.1k.2i),c.3b+1Z.2b(1L.1I.2x,1L.1k.2x));1c(-1L.1I.68,-1L.1I.67)}N{1c(O.5k,O.5K);2e(1M){1c(1M.5k,1M.5K);7(3X&&!/^t(8F|d|h)$/i.17(1M.28)||26&&!5p)3a(1M);7(!2Z&&E.1m(1M,"3C")=="2Z")2Z=R;45=/^1k$/i.17(1M.28)?45:1M;1M=1M.1M}2e(d&&d.28&&!/^1k|3q$/i.17(d.28)){7(!/^a0|1V.*$/i.17(E.1m(d,"18")))1c(-d.2i,-d.2x);7(3X&&E.1m(d,"36")!="4b")3a(d);d=d.1b}7((5p&&(2Z||E.1m(45,"3C")=="4Z"))||(3X&&E.1m(45,"3C")!="4Z"))1c(-1L.1k.5k,-1L.1k.5K);7(2Z)1c(1Z.2b(1L.1I.2i,1L.1k.2i),1Z.2b(1L.1I.2x,1L.1k.2x))}5q={3b:3b,2c:b}}J 3a(a){1c(E.2q(a,"a1",R),E.2q(a,"8D",R))}J 1c(l,t){b+=4t(l)||0;3b+=4t(t)||0}K 5q}})();',62,624,'||||||this|if||||||||||||||||||||||||||||||||||||||function|return|var|length|else|elem|for|data|true|false|document|type|each|null||style||undefined|options|nodeName||browser|nodeType|event|test|display|jQuery|arguments|parentNode|add|msie|window|url|indexOf|push|fn|apply|body|prop|css|constructor|script|replace|isFunction|in|extend|text|div|typeof|className|handle|opacity|complete|status|firstChild|value|new|match|filter|dataType|hide|documentElement|show|attr|doc|offsetParent|Array|call|height|cache|try|tbody|break|trigger|table|catch|success|remove|Math||ready|get|hidden|start|string|safari|animate|tagName|width|map|max|left|split|while|ret|global|toggle|scrollLeft|done|handler|special|prototype||cur|selected|curCSS|find|id|duration|ownerDocument|fx|toLowerCase|scrollTop|select|bind|guid|opera|unit|none|pushStack|toUpperCase|button|makeArray|now|nextSibling|target|stack|parseFloat|events|json|px|isReady|createElement|queue|delete|timeout|slice|disabled|removeChild|jsre|fixed|one|nth|preventDefault|step|merge|inArray|overflow|name|innerHTML|exec|border|top|readyState|multiFilter|error|trim|rl|end|form|first|appendChild|async|elems|insertBefore|checked|childNodes|html|which|encodeURIComponent|append|block|param|readyList|grep|color|setTimeout|runtimeStyle|args|position|toString|has|addEventListener|callee|removeData|timers|password|is|xml|last|getTime|Date|domManip|ajax|src|props|timerId|orig|getElementsByTagName|isXMLDoc|mozilla|easing|custom|curAnim|stopPropagation|unbind|load|selectedIndex|offsetChild|mouseleave|mouseenter|input|application|defaultView|visible|float|String|responseText|charCode|teardown|on|setup|currentStyle|shift|javascript|child|accepts|_default|nodeIndex|lastModified|RegExp|_|parseInt|jsonp|previousSibling|dir|tr|self|getAttribute|eval|empty|update|object|pos|state|setRequestHeader|ajaxSettings|not|GET|getPropertyValue|getComputedStyle|styleSheets|lastToggle|unload|mouseout|mouseover|andSelf|getWH|container2|unshift|fromElement|relatedTarget|visibility|init|absolute|click|fix|triggered|Number|setInterval|removeAttribute|prevObject|unique|classFilter|submit|after|file|clean|expr|windowData|offset|scroll|client|deep|jquery|offsetLeft|globalEval|sibling|version|old|safari2|results|wrapAll|startTime|handleError|container|createTextNode|radio|oldblock|checkbox|dequeue|bindReady|getResponseHeader|parsererror|lastChild|index|swing|00|Modified|ifModified|clone|offsetTop|values|active|getElementById|link|val|col|contentType|ajaxSend|ajaxSuccess|ajaxComplete|ajaxStop|ajaxStart|serializeArray|notmodified|POST|loaded|DOMContentLoaded|Width|triggerHandler|ctrlKey|metaKey|keyCode|clientTop|clientLeft|ajaxError|clientX|pageX|cloneNode|detachEvent|swap|removeEventListener|join|attachEvent|substr|parse|head|textarea|reset|image|before|odd|zoom|even|prepend|username|quickClass|quickID|quickChild|processData|uuid|continue|textContent|appendTo|contents|evalScript|parent|defaultValue|setArray|CSS1Compat|compatMode|cssFloat|styleFloat|webkit|nodeValue|eq|linear|replaceWith|concat|splice|100|href|_toggle|speed|alpha|304|200|throw|Last|httpData|httpNotModified|httpSuccess|clearInterval|beforeSend|colgroup|fieldset|multiple|XMLHttpRequest|ActiveXObject|getBoundingClientRect|scriptCharset|callback|plain|img|hasClass|br|urlencoded|www|abbr|pixelLeft|post|getJSON|getScript|elements|serialize|keypress|keydown|change|mouseup|mousedown|dblclick|resize|focus|blur|stylesheet|rel|mousemove|doScroll|round|hover|keyup|padding|offsetHeight|offsetWidth|Bottom|Top|Right|clientY|pageY|Left|toElement|srcElement|cancelBubble|returnValue|0n|substring|animated|header|enabled|ajaxSetup|innerText|noConflict|size|contains|only|line|gt|weight|lt|font|uFFFF|u0128|417|inner|Height|Boolean|toggleClass|removeClass|addClass|removeAttr|replaceAll|insertAfter|wrap|prependTo|contentWindow|contentDocument|iframe|children|siblings|wrapInner|prevAll|nextAll|prev|next|parents|maxLength|maxlength|readOnly|readonly|borderTopWidth|class|able|htmlFor|522|reverse|boxModel|with|1px|compatible|10000|ie|ra|it|rv|400|fast|600|userAgent|Function|navigator|Object|stop|option|array|ig|NaN|fadeOut|fadeIn|slideToggle|slideUp|slideDown|setAttribute|changed|be|responseXML|content|1223|fadeTo|can|300|property|protocol|location|send|abort|getAttributeNode|specified|method|action|cssText|attributes|Accept|With|th|slow|Requested|td|GMT|cap|1970|cos|PI|Jan|colg|01|Thu|tfoot|Since|thead|If|Type|Content|leg|open|XMLHTTP|opt|Microsoft|embed|onreadystatechange|onload|area|charset|hr|http|inline|borderLeftWidth|1_|meta'.split('|'),0,{}))IssueTrackerProduct/js/core.js0000644000175000017500000001033611012074373016564 0ustar peterbepeterbe$.id=function(id){return document.getElementById(id)}; function econvert(s) { return s.replace(/%7E/g,'~').replace(/%28/g,'(').replace(/%29/g,')').replace(/%20/g,' ').replace(/_dot_| dot |_\._|\(\.\)/gi, '.').replace(/_at_|~at~/gi, '@'); } function fixEncodedLinks() { $("a.aeh", $("#main")).each(function() { this.href = econvert(this.href); this.innerHTML = econvert(this.innerHTML); }); } function _getNoLines(element) { var hardlines = element.value.split('\n'); var total = hardlines.length; for (var i=0, len=hardlines.length; i parseInt(this.rows)) this.rows = '' + Math.min(_getNoLines(this) + 2, 50); }); // When a user enters new lines, if they have entered more // lines than the textarea has rows, then double the textareas rows $('textarea.autoexpanding').bind('keyup', function() { if (_getNoLines(this) > parseInt(this.rows)) this.rows = '' + Math.min(_getNoLines(this) + 2, 50); }); // Unless we can find a reason not to, hide all the fileattachment input TRs if (!($('tr.fileattachment-error').size() || $('tr.fileattachment input[type="checkbox"]').size())) { hideFileAttachments(); } else { $('tr.fileattachment a.fileattachment-tip').hide(); } $('input[type="file"]').change(function() { if (this.value) $('tr.fileattachment a.fileattachment-tip').hide(); else $('tr.fileattachment a.fileattachment-tip:hidden').show(); }); }); function G(p) { location.href=p; } function checkCaptchaValue(elm, msg, maxlength) { var v = $.trim(elm.value); if (v) { if (v.search(/\D/)>-1) alert(msg); //alert(v); //alert(typeof v); v = v.replace(/\D/g,''); if (v.length >= maxlength) v = v.substring(0, maxlength); //elm.value = v; } return v; } function checkCaptchaValue(v, msg, maxlength) { v = $.trim(""+v); if (v) { if (v.search(/\D/)>-1) alert(msg); v = v.replace(/\D/g,''); if (v.length >= maxlength) v = v.substring(0, maxlength); } return v; } $.fn.fastSerialize = function() { var a = []; $('input,textarea,select,button', this).each(function() { var n = this.name; var t = this.type; if ( !n || this.disabled || t == 'reset' || (t == 'checkbox' || t == 'radio') && !this.checked || (t == 'submit' || t == 'image' || t == 'button') && this.form.clicked != this || this.tagName.toLowerCase() == 'select' && this.selectedIndex == -1) return; if (t == 'image' && this.form.clicked_x) return a.push( {name: n+'_x', value: this.form.clicked_x}, {name: n+'_y', value: this.form.clicked_y} ); if (t == 'select-multiple') { $('option:selected', this).each( function() { a.push({name: n, value: this.value}); }); return; } a.push({name: n, value: this.value}); }); return a; }; function showAJAXProblemWarning(url) { if ($('#ajax-problem-warning').size()) return; // show a nice message that the AJAX call didn't work container = $('
') .addClass('problem-warning-message') .append($('').addClass('close').click(function() { $('#ajax-problem-warning').remove(); return false; }).text('close')) .append($('

') .addClass('error').text('Currently having network connection problems.')) $('#main').append(container); window.setTimeout(function() { $('#ajax-problem-warning').fadeOut().remove(); }, 60*1000); }IssueTrackerProduct/js/jquery-latest.min.js0000644000175000017500000014666011012074373021241 0ustar peterbepeterbe/* * jQuery 1.2.2 - New Wave Javascript * * Copyright (c) 2007 John Resig (jquery.com) * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. * * $Date: 2008-01-14 17:56:07 -0500 (Mon, 14 Jan 2008) $ * $Rev: 4454 $ */ (function(){if(window.jQuery)var _jQuery=window.jQuery;var jQuery=window.jQuery=function(selector,context){return new jQuery.prototype.init(selector,context);};if(window.$)var _$=window.$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;var isSimple=/^.[^:#\[\.]*$/;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}else if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem)if(elem.id!=match[3])return jQuery().find(selector);else{this[0]=elem;this.length=1;return this;}else selector=[];}}else return new jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return new jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(selector.constructor==Array&&selector||(selector.jquery||selector.length&&selector!=window&&!selector.nodeType&&selector[0]!=undefined&&selector[0].nodeType)&&jQuery.makeArray(selector)||[selector]);},jquery:"1.2.2",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;this.each(function(i){if(this==elem)ret=i;});return ret;},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value==undefined)return this.length&&jQuery[type||"attr"](this[0],name)||undefined;else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div"),container2=document.createElement("div");container.appendChild(clone);container2.innerHTML=container.innerHTML;return container2.firstChild;}else return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return!selector?this:this.pushStack(jQuery.merge(this.get(),selector.constructor==String?jQuery(selector).get():selector.length!=undefined&&(!selector.nodeName||jQuery.nodeName(selector,"form"))?selector:[selector]));},is:function(selector){return selector?jQuery.multiFilter(selector,this).length>0:false;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=value.constructor==Array?value:[value];jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else this.value=value;});},html:function(value){return value==undefined?(this.length?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script")){scripts=scripts.add(elem);}else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.prototype.init.prototype=jQuery.prototype;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==1){target=this;i=0;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret;function color(elem){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=elem.style.display;elem.style.display="block";elem.style.display=save;}if(name.match(/float/i))name=styleFloat;if(!force&&elem.style&&elem.style[name])ret=elem.style[name];else if(document.defaultView&&document.defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var getComputedStyle=document.defaultView.getComputedStyle(elem,null);if(getComputedStyle&&!color(elem))ret=getComputedStyle.getPropertyValue(name);else{var swap=[],stack=[];for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(var i=0;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!tags.indexOf("",""]||(!tags.indexOf("",""]||!tags.indexOf("",""]||jQuery.browser.msie&&[1,"div

","
"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf(""&&tags.indexOf("=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var fix=jQuery.isXMLDoc(elem)?{}:jQuery.props;if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(fix[name]){if(value!=undefined)elem[fix[name]]=value;return elem[fix[name]];}else if(jQuery.browser.msie&&name=="style")return jQuery.attr(elem.style,"cssText",value);else if(value==undefined&&jQuery.browser.msie&&jQuery.nodeName(elem,"form")&&(name=="action"||name=="method"))return elem.getAttributeNode(name).nodeValue;else if(elem.tagName){if(value!=undefined){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem.setAttribute(name,""+value);}if(jQuery.browser.msie&&/href|src/.test(name)&&!jQuery.isXMLDoc(elem))return elem.getAttribute(name,2);return elem.getAttribute(name);}else{if(name=="opacity"&&jQuery.browser.msie){if(value!=undefined){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseFloat(value).toString()=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100).toString():"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(value!=undefined)elem[name]=value;return elem[name];}},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(typeof array!="array")for(var i=0,length=array.length;i*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":"m[2]=='*'||jQuery.nodeName(a,m[2])","#":"a.getAttribute('id')==m[2]",":":{lt:"im[3]-0",nth:"m[3]-0==i",eq:"m[3]-0==i",first:"i==0",last:"i==r.length-1",even:"i%2==0",odd:"i%2","first-child":"a.parentNode.getElementsByTagName('*')[0]==a","last-child":"jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a","only-child":"!jQuery.nth(a.parentNode.lastChild,2,'previousSibling')",parent:"a.firstChild",empty:"!a.firstChild",contains:"(a.textContent||a.innerText||jQuery(a).text()||'').indexOf(m[3])>=0",visible:'"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',hidden:'"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',enabled:"!a.disabled",disabled:"a.disabled",checked:"a.checked",selected:"a.selected||jQuery.attr(a,'selected')",text:"'text'==a.type",radio:"'radio'==a.type",checkbox:"'checkbox'==a.type",file:"'file'==a.type",password:"'password'==a.type",submit:"'submit'==a.type",image:"'image'==a.type",reset:"'reset'==a.type",button:'"button"==a.type||jQuery.nodeName(a,"button")',input:"/input|select|textarea|button/i.test(a.nodeName)",has:"jQuery.find(m[3],a).length",header:"/h\\d/i.test(a.nodeName)",animated:"jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length"}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false;var re=quickChild;var m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var f=jQuery.expr[m[1]];if(typeof f!="string")f=jQuery.expr[m[1]][m[2]];f=eval("false||function(a,i){return "+f+"}");r=jQuery.grep(r,f,not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[];var cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&(!elem||n!=elem))r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval!=undefined)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=function(){return fn.apply(this,arguments);};handler.data=data;handler.guid=fn.guid;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){var val;if(typeof jQuery=="undefined"||jQuery.event.triggered)return val;val=jQuery.event.handle.apply(arguments.callee.elem,arguments);return val;});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined)for(var type in events)this.remove(elem,type);else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data||[]);if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event)data.unshift(this.fix({type:type,target:elem}));data[0].type=type;if(jQuery.isFunction(jQuery.data(elem,"handle")))val=jQuery.data(elem,"handle").apply(elem,data);if(!fn&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val;event=jQuery.event.fix(event||window.event||{});var parts=event.type.split(".");event.type=parts[0];var handlers=jQuery.data(this,"events")&&jQuery.data(this,"events")[event.type],args=Array.prototype.slice.call(arguments,1);args.unshift(event);for(var j in handlers){var handler=handlers[j];args[0].handler=handler;args[0].data=handler.data;if(!parts[1]||handler.type==parts[1]){var ret=handler.apply(this,args);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}if(jQuery.browser.msie)event.target=event.preventDefault=event.stopPropagation=event.handler=event.data=null;return val;},fix:function(event){var originalEvent=event;event=jQuery.extend({},originalEvent);event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=originalEvent.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;arguments[0].type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;arguments[0].type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){return this.each(function(){jQuery.event.add(this,type,function(event){jQuery(this).unbind(event);return(fn||data).apply(this,arguments);},fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){if(this[0])return jQuery.event.trigger(type,data,this[0],false,fn);return undefined;},toggle:function(){var args=arguments;return this.click(function(event){this.lastToggle=0==this.lastToggle?1:0;event.preventDefault();return args[this.lastToggle].apply(this,arguments)||false;});},hover:function(fnOver,fnOut){return this.bind('mouseenter',fnOver).bind('mouseleave',fnOut);},ready:function(fn){bindReady();if(jQuery.isReady)fn.call(document,jQuery);else jQuery.readyList.push(function(){return fn.call(this,jQuery);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.apply(document);});jQuery.readyList=null;}jQuery(document).triggerHandler("ready");}}});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(document.addEventListener&&!jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);if(jQuery.browser.msie&&window==top)(function(){if(jQuery.isReady)return;try{document.documentElement.doScroll("left");}catch(error){setTimeout(arguments.callee,0);return;}jQuery.ready();})();if(jQuery.browser.opera)document.addEventListener("DOMContentLoaded",function(){if(jQuery.isReady)return;for(var i=0;i=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("
").append(res.responseText.replace(//g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=(new Date).getTime();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){var jsonp,jsre=/=\?(&|$)/g,status,data;s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(s.type.toLowerCase()=="get"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&s.type.toLowerCase()=="get"){var ts=(new Date()).getTime();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&s.type.toLowerCase()=="get"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");if((!s.url.indexOf("http")||!s.url.indexOf("//"))&&(s.dataType=="script"||s.dataType=="json")&&s.type.toLowerCase()=="get"){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xml=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();xml.open(s.type,s.url,s.async,s.username,s.password);try{if(s.data)xml.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xml.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xml.setRequestHeader("X-Requested-With","XMLHttpRequest");xml.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend)s.beforeSend(xml);if(s.global)jQuery.event.trigger("ajaxSend",[xml,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xml&&(xml.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xml)&&"error"||s.ifModified&&jQuery.httpNotModified(xml,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xml,s.dataType);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xml.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else jQuery.handleError(s,xml,status);complete();if(s.async)xml=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xml){xml.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xml.send(s.data);}catch(e){jQuery.handleError(s,xml,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xml,s]);}function complete(){if(s.complete)s.complete(xml,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xml,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xml;},handleError:function(s,xml,status,e){if(s.error)s.error(xml,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xml,s,e]);},active:0,httpSuccess:function(r){try{return!r.status&&location.protocol=="file:"||(r.status>=200&&r.status<300)||r.status==304||r.status==1223||jQuery.browser.safari&&r.status==undefined;}catch(e){}return false;},httpNotModified:function(xml,url){try{var xmlRes=xml.getResponseHeader("Last-Modified");return xml.status==304||xmlRes==jQuery.lastModified[url]||jQuery.browser.safari&&xml.status==undefined;}catch(e){}return false;},httpData:function(r,type){var ct=r.getResponseHeader("content-type");var xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0;var data=xml?r.responseXML:r.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else s.push(encodeURIComponent(j)+"="+encodeURIComponent(a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle(fn,fn2):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall);var hidden=jQuery(this).is(":hidden"),self=this;for(var p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return jQuery.isFunction(opt.complete)&&opt.complete.apply(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.apply(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(!elem)return undefined;type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",array?jQuery.makeArray(array):[]);return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].apply(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:{slow:600,fast:200}[opt.duration])||400;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.apply(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.apply(this.elem,[this.now,this]);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=(new Date()).getTime();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;ithis.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done&&jQuery.isFunction(this.options.complete))this.options.complete.apply(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.fx.step={scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}};jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522,fixed=jQuery.css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&jQuery.css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(jQuery.css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&jQuery.css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||jQuery.css(offsetChild,"position")=="absolute"))||(mozilla&&jQuery.css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l)||0;top+=parseInt(t)||0;}return results;};})();IssueTrackerProduct/Constants.py0000644000175000017500000003135311012074373017212 0ustar peterbepeterbe# IssueTrackerProduct # www.IssueTrackerProduct.com # Peter Bengtsson # # Constants for IssueTrackerProduct # import os def getEnvBool(key, default): """ return an boolean from the environment variables """ value = os.environ.get(key, default) try: value = not not int(value) except ValueError: if str(value).lower().strip() in ['yes','on','t','y']: value = 1 elif str(value).lower().strip() in ['no','off','f','n']: value = 0 else: value = default return value def getEnvInt(key, default): """ return an integer from the environment variables """ value = os.environ.get(key, default) try: return int(value) except ValueError: return default def getEnvStr(key, default): """ return a string from the environment variables """ value = os.environ.get(key, default) return str(value) true = not 0 false = not true try: True = not False except NameError: True = true False = false from I18N import _ # Optimize the output OPTIMIZE = getEnvBool('OPTIMIZE_ISSUETRACKERPRODUCT', True) # Debug # shows some verbose dev data and doesn't actually send emails DEBUG = getEnvBool('DEBUG_ISSUETRACKERPRODUCT', False) UNICODE_ENCODING = getEnvStr('UNICODE_ENCODING_ISSUETRACKERPRODUCT', 'utf-8') # Global variable if you want to disable CheckoutableTemplates # even if it's installed and working. This can be useful to disable # if you're doing development or want to supress all checked out # templates in ZODB. DISABLE_CHECKOUTABLE_TEMPLATES = getEnvBool('DISABLE_CHECKOUTABLE_TEMPLATES', False) # constants ICON_LOCATION = 'misc_/IssueTrackerProduct' ISSUETRACKER_METATYPE = 'Issue Tracker' ISSUE_METATYPE = 'Issue Tracker Issue' ISSUE_DRAFT_METATYPE = 'Issue Tracker Draft Issue' ISSUETHREAD_DRAFT_METATYPE = 'Issue Tracker Draft Issue Thread' NOTIFYABLE_METATYPE = 'Issue Tracker Notifyable' ISSUETHREAD_METATYPE = 'Issue Tracker Issue Thread' NOTIFICATION_META_TYPE = 'Issue Tracker Notification' NOTIFYABLEGROUP_METATYPE = 'Issue Tracker Notifyable Group' NOTIFYABLECONTAINER_METATYPE = 'Issue Tracker Notifyable Container' POP3ACCOUNT_METATYPE = 'Issue Tracker POP3 Account' ACCEPTINGEMAIL_METATYPE = 'Issue Tracker Accepting Email' ISSUEUSERFOLDER_METATYPE = 'Issue Tracker User Folder' ISSUEASSIGNMENT_METATYPE = 'Issue Tracker Assignment' FILTEROPTION_METATYPE = 'Issue Tracker Filter Option' REPORTSCRIPT_METATYPE = 'Issue Tracker Report Script' REPORTS_CONTAINER_METATYPE = 'Report Scripts Container' # properties #DEFAULT_TYPES = ('general', 'announcement', 'idea', 'bug report', # 'feature request','question', # 'usability','other') DEFAULT_TYPES = (_(u'general'), _(u'announcement'), _(u'idea'), _(u'bug report'), _(u'feature request'), _(u'question'), _(u'usability'), _(u'other'), ) DEFAULT_TYPE = DEFAULT_TYPES[0] #DEFAULT_URGENCIES = ('low','normal','high','critical') DEFAULT_URGENCIES = (_(u'low'), _(u'normal'), _(u'high'), _(u'critical')) DEFAULT_ALWAYS_NOTIFY = () DEFAULT_URGENCY = DEFAULT_URGENCIES[1] DEFAULT_SECTIONS_OPTIONS = (_(u'General'), _(u'Homepage'), _(u'Other')) DEFAULT_SECTIONS = [DEFAULT_SECTIONS_OPTIONS[0]] DEFAULT_WHEN_IGNORE_WORD = 'ignored' DEFAULT_DISPLAY_DATE = '%d/%m %Y %H:%M' DEFAULT_SITEMASTER_NAME = 'Issue Tracker' DEFAULT_SITEMASTER_EMAIL = 'noreply@localhost' DEFAULT_MANAGER_ROLES = ['Manager', 'IssueTracker Manager'] DEFAULT_DEFAULT_BATCH_SIZE = getEnvInt('ITP_DEFAULT_BATCH_SIZE', 20) DEFAULT_OUTLOOK_BATCH_SIZE = getEnvInt('ITP_OUTLOOK_BATCH_SIZE', 10) DEFAULT_ALLOW_SHOW_ALL = True DEFAULT_ISSUEPREFIX = '' DEFAULT_NO_FILEATTACHMENTS = getEnvInt('ITP_NO_FILEATTACHMENTS', 3) DEFAULT_NO_FOLLOWUP_FILEATTACHMENTS = getEnvInt('ITP_NO_FOLLOWUP_FILEATTACHMENTS', 3) DEFAULT_STATUSES = (_(u'open'), _(u'taken'), _(u'on hold'), _(u'rejected'), _(u'completed')) DEFAULT_STATUSES_VERBS = (_(u'open'), _(u'take'), _(u'put on hold'), _(u'reject'), _(u'complete')) DEFAULT_DISPLAY_FORMATS = ('plaintext','structuredtext') DEFAULT_DEFAULT_DISPLAY_FORMAT = DEFAULT_DISPLAY_FORMATS[0] DEFAULT_DISPATCH_ON_SUBMIT = True DEFAULT_RANDOMID_LENGTH = getEnvInt('ITP_ID_LENGTH', 3) DEFAULT_ALLOW_ISSUEATTRCHANGE = True DEFAULT_STOP_CACHE = True DEFAULT_ALLOW_SUBSCRIPTION = False DEFAULT_PRIVATE_STATISTICS = False DEFAULT_PRIVATE_REPORTS = True DEFAULT_SAVE_DRAFTS = True DEFAULT_SHOW_CONFIDENTIAL_OPTION = False DEFAULT_SHOW_HIDEME_OPTION = False DEFAULT_SHOW_DOWNLOAD_BUTTON = False DEFAULT_SHOW_ISSUEURL_OPTION = False DEFAULT_ENCODE_EMAILDISPLAY = True DEFAULT_SHOW_ALWAYS_NOTIFY_STATUS = True DEFAULT_IMAGES_IN_MENU = True DEFAULT_USE_ISSUE_ASSIGNMENT = False DEFAULT_DISALLOW_DUPLICATE_ISSUE_SUBJECTS = False DEFAULT_CAN_ADD_NEW_SECTIONS = False DEFAULT_SIGNATURE_TEXT = _('''[title] <[url]>''') SORTORDER_ALTERNATIVES = ('issuedate','modifydate') DEFAULT_SORTORDER = SORTORDER_ALTERNATIVES[0] DEFAULT_SHOW_ID_WITH_TITLE = False DEFAULT_SHOW_CVSEXPORT_LINK = False DEFAULT_SHOW_USE_ACCESSKEYS_OPTION = True DEFAULT_SHOW_REMEMBER_SAVEDFILTER_PERSISTENTLY_OPTION = True DEFAULT_USE_AUTOSAVE = True DEFAULT_USE_ESTIMATED_TIME = False DEFAULT_USE_ACTUAL_TIME = False DEFAULT_INCLUDE_DESCRIPTION_IN_NOTIFICATIONS = False DEFAULT_USE_TELLAFRIEND = True DEFAULT_USE_TELLAFRIEND_FOR_ANONYMOUS = True DEFAULT_SHOW_DATES_CLEVERLY = True DEFAULT_SHOW_SPAMBOT_PREVENTION = False DEFAULT_SPAM_KEYWORDS = ['poker-stadium.com', ' this gets cleaned away FILTERVALUER_MAX_PER_USER = 20 # how many saved filters one person can have FILTERVALUEFOLDER_THRESHOLD_CLEANING = 1000 # when SPAMBAYES_CHECK = 'spam' # if X-Spambayes-Classification=$this delete the inbound email BTREEFOLDER2_ID = 'issues' # if a BTreeFolder2 is used, use this id POSSIBLE_USER_LISTS = ['assignments', 'added', 'followedup', 'subscribed'] MENUICONS_DATA = {'Home':{'src':'home.gif', 'size':'16x16'}, 'AddIssue':{'src':'add.gif', 'size':'16x16', 'alt':'Add Issue'}, 'QuickAddIssue':{'src':'add.gif', 'size':'16x16', 'alt':'Quick Add Issue'}, 'ListIssues':{'src':'list.gif', 'size':'16x16', 'alt':'List Issues'}, 'CompleteList':{'src':'complete.gif', 'size':'16x16', 'alt':'Complete List'}, 'User':{'src':'user.gif', 'size':'16x16'}, 'Login':{'src':'login.gif', 'size':'16x16'}, 'Logout':{'src':'logout.gif', 'size':'16x16'}, } for title, data in MENUICONS_DATA.items(): if not data.has_key('width'): data['width'] = data['size'].split('x')[0] if not data.has_key('height'): data['height'] = data['size'].split('x')[1] if not data['src'].startswith('/'): data['src'] = '/misc_/IssueTrackerProduct/%s'%data['src'] if not data.has_key('alt'): data['alt'] = title MENUICONS_DATA[title] = data IssueTrackerProduct/Assignment.py0000644000175000017500000001511611012074373017345 0ustar peterbepeterbe# IssueTrackerProduct # www.issuetrackerproduct.com # # Peter Bengtsson # License: ZPL # # python # Zope from Globals import InitializeClass from AccessControl import ClassSecurityInfo from zLOG import LOG, ERROR, INFO, PROBLEM, WARNING from DateTime import DateTime # Product from Issue import IssueTrackerIssue from Constants import * from Permissions import * class IssueTrackerIssueAssignment(IssueTrackerIssue): """ Issue Assignment class """ meta_type = ISSUEASSIGNMENT_METATYPE icon = '%s/issueassignment.gif'%ICON_LOCATION _properties=({'id':'acl_assignee', 'type': 'string', 'mode':'w'}, {'id':'fromname', 'type': 'string', 'mode':'w'}, {'id':'email', 'type': 'string', 'mode':'w'}, {'id':'assignmentdate','type': 'date', 'mode':'w'}, {'id':'acl_adder', 'type': 'string', 'mode':'w'}, ) security=ClassSecurityInfo() manage_options = ( {'label':'Properties', 'action':'manage_propertiesForm'}, {'label':'Contents', 'action':'manage_main'}, ) # legacy # All old assignments that have already been sent we can assume # that they have been sent. email_sent = True def __init__(self, id, acl_assignee, state, fromname, email, acl_adder='', email_sent=False): """ create assignment """ self.id = str(id) self.acl_assignee = acl_assignee self.assignmentdate = DateTime() self.fromname = fromname self.email = email if acl_adder is None: acl_adder = '' self.acl_adder = acl_adder assert state in [-1, 0, 1], "Invalid state of assignment" self.state = state # 1=Assigned 0=Reassigned -1=Rejected self.email_sent = bool(email_sent) def getTitle(self): """ return title """ return self.showState() def getAssignmentDate(self): """ return assignmentdate """ return self.assignmentdate def getFromname(self, issueusercheck=1): """ return fromname """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') uf = self.unrestrictedTraverse(ufpath) issueuserobj = uf.data[name] return issueuserobj.getFullname() or self.fromname else: return self.fromname def getEmail(self, issueusercheck=1): """ return email """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') uf = self.unrestrictedTraverse(ufpath) issueuserobj = uf.data[name] return issueuserobj.getEmail() or self.email else: return self.email def getACLAdder(self): """ return acl_adder """ return self.acl_adder def _setACLAdder(self, acl_adder): """ set acl_adder """ self.acl_adder = acl_adder def getACLAssignee(self): """ return acl_assignee """ return self.acl_assignee def _setACLAssignee(self, acl_assignee): """ set acl_assignee """ self.acl_assignee = acl_assignee def getACLAssigneeUser(self): """ return acl_assignee as object from its userfolder """ ufpath, name = self.getACLAssignee().split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) issueuserobj = uf.data[name] return issueuserobj def hasACLAssigneeUser(self): """ return if acl_assignee exists as an object in its userfolder """ ufpath, name = self.getACLAssignee().split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # user folder doesn't exist return False return uf.data.has_key(name) def isYou(self): """ return true if logged in as who is assigned """ issueuser = self.getIssueUser() if issueuser: identifier = issueuser.getIssueUserIdentifier() identifier = ','.join(identifier) acl_assignee = self.getACLAssignee() if identifier == acl_assignee: return True return False def getAssigneeFullname(self): """ return the fullname from the acl_assignee """ try: issueuserobj = self.getACLAssigneeUser() except KeyError: return "" try: return issueuserobj.getFullname() except AttributeError: # if the issueuserobj doesn't have a getFullname() method, # then this object is a plain Zope User Folder instance return issueuserobj.getUserName() def getAssigneeEmail(self): """ return the email from the acl_assignee """ try: issueuserobj = self.getACLAssigneeUser() except KeyError: return "" try: return issueuserobj.getEmail() except AttributeError: # read the comment in the except clause above in getAssigneeFullname() return "" def getState(self): """ return state """ return self.state def showState(self, complete=0): """ return state nicely """ state = self.getState() if state == -1: if complete: return "Rejected by" else: return "Rejected" elif state == 0: if complete: return "Reassigned to" else: return "Reassigned" else: if complete: return "Assigned to" else: return "Assigned" def _setEmailSent(self): self.email_sent = True def isEmailSent(self): return self.email_sent security.declareProtected(VMS, 'assertAllProperties') def assertAllProperties(self): """ make sure assignment has all properties """ props = { # currently nothing } count = 0 for key, default in props.items(): if not self.__dict__.has_key(key): self.__dict__[key] = default count += 1 return count InitializeClass(IssueTrackerIssueAssignment) IssueTrackerProduct/README.txt0000644000175000017500000000116311012074373016356 0ustar peterbepeterbeIssueTrackerProduct - A bug/issue tracker web application for Zope by Peter Bengtsson, mail@peterbe.com, www.peterbe.com License: ZPL For more information about the IssueTrackerProduct please see "http://www.issuetrackerproduct.com":http://www.issuetrackerproduct.com Dependencies Python 2.3.x or greater Zope: Zope 2.7 or later but NOT Zope 3. Any operating system where Python runs Upgrading Once you've upgraded and refreshed the product or restarted Zope, be sure to press the Update Everything button on the Management tab inside the ZMI each IssueTrackerProduct instance. IssueTrackerProduct/CHANGES.txt0000644000175000017500000015466511012074373016511 0ustar peterbepeterbe- 0.9.3 Bug fixed: NameError when checking for emails in and nothing to download. Thanks Luciano (Real#0743) - 0.9.2 Bug fixed: When accepting multipart emails and html2safehtml is not installed the default part to keep is now the text/plain part. Bug fixed: Loading a saved draft followup that didn't have an action would raise an error. Bug fixed: Loading filter options in Konquerer would load the AJAX request 3 times for one click. Bug fixed: Saved filters container was never imploded even if the container had more saved filters in it than allowed. Bug fixed: Saving filters when only the fromname is known would raise an UnmatchableError. - 0.9.1 Bug fixed: Saved cookies with where only the name is known could cause an error on the /User page. Bug fixed: Delete issue form did not work. - 0.9.0 New feature: If you pass the spambot prevention test it's remembered in a cookie for the next 60 days rather than just for the session. New feature: When you've added an issue the URL it redirects to no longer ends with ?NewIssues=Submitted. Cleaner URLs. New feature: Really long URLs in the text that extend beyond the box it's in is truncated shorter. Bug fixed: Having statuses containing unicode characters could break the statistics page. Thanks Rene Bonvin. (Real#0735) New feature: Basic statistics is not private by default from now on. You can still make it private from the Advanced Properties page. New feature: Statuses can now be configured to contain unicode characters. New feature: The persistent list of saved filters is now indexed with a separate ZCatalog to vastly speed up lookups of old saved filters. Bug fixed: Saved filters containing unicode characters could cause a UnicodeDecodeError. Bug fixed: Unicode characters in a filtered name would show incorrectly when loading the filter options. Bug fixed: Search on title with wildcards (e.g. *keyword* matching 'subkeywords') only if no other title searches matched. Bug fixed: Emails with tabs in header values were before incorrectly stripped of this whitespace causing the set of headers to be shortened. Thanks Jesse Perry Bug fixed: getIssueObjects() would break with KeyError if a reference to a join-in issuetracker (see bottom of Advanced properties) is broken. Bug fixed: Reassigned issues didn't say "assigned" on the List Issues. Thanks NiBar (Real#0711) New feature: Added new method called getReportIssues() that takes the id of the report as an only parameter and it returns all the issues that report would find. New feature: Search result highlighting can be switched off with a Javascript-created link similar to Gmail. Bug fixed: Encoded email address in a autorefresh are re-encoded. Bug fixed: Possible UnicodeDecodeError if using normal acl_users user folder with a unicode fromname. New feature: File attachments initially hidden and covered with a Javascript opener. New feature: Change assignment form expands automatically on focus. Bug fixed: Filter switcher ("Do only show"/"Do not show") had a Javascript error on a missing variable called 'label'. New feature: Ability to disable "Show them all" as a batching option. Very useful when there are many issues because showing this page will slow your Zope down. Bug fixed: Name hidden wasn't hidden on home page. New feature: List of issues on home page periodically refreshed with AJAX. Bug fixed: Download of dodgy emails without a To: part now skipped (Real#0700, thanks Jesse Perry) New feature: File attachment links have a rel="nofollow" now. New feature: Ability to hook up pre- and post scripts that are called before and after issues are submitted. http://www.issuetrackerproduct.com/Documentation/How-Tos/pre-post-SubmitIssue Bug fixed: Confidential issues could be seen by logged in people by changing their email address to an email address of someone else. (Real#0696, thanks NiBar) Bug fixed: Keyboard shortcut for comparing issues changed from 'c' to 'g'+'d' since the 'c' cause interference with Ctrl-C for copying. (Real#0697, thanks Jesse Perry) New feature: Skipped notifications when not dispatching on submit. Means that if you let a cronjob send the notifications instead of immediately sending them, some notifications are cancelled if the people who are email have already followed up. (Real#0686) Bug fixed: rss.xml and rdf.xml weren't protected by the View permission. (Real#0695, thanks Jesse Perry) Bug fixed: Changed default encoding from "utf8" to "utf-8" to make sure older IEs gets it right. Bug fixed: Header in CSV file for "ID" replaced by "Issue ID" (Real#0687) Bug fixed: Clairvoyant AJAX checks' interval increases now slowly as the page gets older. Bug fixed: Issues were uncataloged twice when deleting causing a harmless extra message in the event log. - 0.8.3 Bug fixed: Clairvoyant javascript alert on followups would repeat the same message over and over (almost blinking). New feature: Name and email is saved in a cookie when working on a draft if the name/email isn't already set. Change: If in DEBUG mode (environment variable DEBUG_ISSUETRACKERPRODUCT) emails are printed to stdout - 0.8.2 Bug fixed: Error on duplicate in on second followups in issues. Real#0665 and Real#0666 - 0.8.1 Bug fixed: The clarvoyent AJAX feature would show if someone is working a different issue to the one you're looking at. - 0.8.0 New feature: Ability to compare issues (experimental). Only works when using keyboard shortcuts. Key: c New feature: Now you can assign an issue to anybody even after it's been created and even if it didn't already have an assignment. Real#0299, Real#0188, Real#0081 Bug fixed: Sending assignment emails from a user with non-ASCII name would cause an UnicodeDecodeError. New feature: Uses jQuery 1.2.1 Bug fixed: Cancelling a go-to-issue keyboard command caused a Javascript error. New feature: Upgraded to addhrefs 0.9.3 Bug fixed: preParseEmailString would sometimes barf on unicode strings for translation. Bug fixed: UnicodeDecodeError wasn't raised properly in processing inbound email reply and formatflowed_decode() was called with wrong parameter. Thanks Robert Leftwich (Real#0642) Bug fixed: When you reply via email to an issue, no notification was sent to those involved in the issue. Thanks Robert Leftwich. (Real#0413) New feature: When you reach the last line in any textarea input (eg. on adding a new issue) the textarea box automatically expands to add a few extra blank lines. New feature: Expires header set to all static content and set much further into the future for better HTTP caching. New feature: The Tell-a-friend and Subscribe-to-changes has been moved down below the issue threads. Bug fixed: Fixed possible XSS hole. Name and email checking now done when adding issues and followups. Bug fixed: Emails in unicode (non-ascii) characters could lead to email notifications being sent out incorrectly. New feature: The autorefresh interval starts on 3 seconds interval and increases the interval length by 30% each time it incurs. http://www.issuetrackerproduct.com/News/autorefresh-intervals/ Bug fixed: multiple drafts were created sometimes. New feature: fromname, title and description of issues and followups are now stored as unicode! The default encoding is utf8. New feature: If you run Python 2.4 on your Zope you can configure the POP3 to use SSL. Thanks Shane Graber New feature: Possible to disable the 'Tell a friend' feature for anonymous users (eg. spambots for example). Thanks Fred Damberger - 0.7.3 Bug fixed: .svn and CVS folders could be created when you Update Everything. New feature: New version of addhrefs.py version 0.9.2. New feature: QuickAddIssue now shows the spambot prevention if switched on. Real#0343 Bug fixed: Redirecting out on duplicate status change would cause a TypeError. This happens when two people simultaneuously for example Take an issue. Bug fixed: Change user details didn't work when you're not logged in as a CMF user. Bug fixed: ShowIssueData template mixed up ShowIssueURLOption with ShowConfidentialOption. Thanks Peter Eddy. Real#0332 Bug fixed: Edit notifyable error. Real#0320 New feature: Ability to enable a spambot prevention technique to assert that posters are humans. (see Properties->Advanced->Enable spambot prevention) Bug fixed: When using IssueTrackerProduct on top of Plone the authenticated member was selected incorrectly. Thanks Greg Baker (Real#0315) Bug fixed: get_cookie() typo should have been self.get_cookie(). Thanks Greg Baker (Real#0312) Bug fixed: Javascript autorefresh didn't work with URLs ending in 'index_html'. Thanks Maheshg Real#0298 Bug fixed: Odd emails with ISO-8859 encoding that aren't really ISO-8859 encoded could cause a TypeError. Thanks Mirco Attocchi Real#0300. Bug fixed: POP3 email parsing could fail if email didn't have a message-id. New feature: "Select all" button on the Spam protection page. Bug fixed: Bug in saving of spam keywords on duplicate keywords across combinations fixed. Bug fixed: Not possible to change an issues Confidential status through the "change details" form even if Confidential option was enabled. Real#0281 - 0.7.2 Bug fixed: Nasty bug in name/email gets blanked on issue submission even the issuetracker remembers the users name as part of the instance persistency. Thanks Rakesh, Ramy, Pradeep Real#0268 Bug fixed: getPreviewTitle() on followups could get potentially confused if you tried to put on hold. New feature: Spam protection built in. Click Management tab, Spam protection to configure the spam keywords. Only applicable to public issuetrackers really. Bug fixed: ACL accounts where the username == full name couldn't be sent to in the Always notify property. Thanks Ed Leafe Real#0266 Bug fixed: ValueError in pop3 checking with mismatched case in the subjectline. Thanks Eric Real#0265 New feature: Ability to control wether to reveal the new issue URL of issues created via inbound email. Instead of the issue's URL it shows the ID like #0001. (Thanks Bob Gimlich Real#0263) Bug fixed: Emailed in issues with signature had incorrect linebreaks in HTML looking like this: New feature: The URL option on Add Issue is now by default switched off (available on Properties, Advanced) on all new instances. New feature: Now you can control the menu items through the Properties tab. Follow the "Configure the menu" link. Bug fixed: When renaming an issuetracker, ACL users defined within and used in issues, threads and assignments got link broken. Now a new function called _renameOldPaths() takes care of this. Bug fixed: The CSV export includes titles of join-in issuetrackers like on List Issues. (thanks Kosmas Real#0249) New feature: You can search a comma separated list of issue ids in serveral suitable formats like '#0123, 124, 0125, #126'. - 0.7.1 Bug fixed: Searching and finding exactly one matched issue yielded a TypeError about len() that happens just before the redirect to the issue. Bug fixed: getNextActionIssues() got confused when ACL logged in users had submitted their last replies to issues via email and included it wrongly. Bug fixed: timeSince() could return "2 weeks and 0 days" when it should just be "2 weeks". Bug fixed: Id with title in tag (Real#0236) Bug fixed: Badly formatted email replies that don't use > but split the new and old text with "-----Original Message------" can now be splitted correctly. - 0.7.0 New feature: "drafts" folder is (if possible) created as BTreeFolder2 (Real#0228) Bug fixed: Automatic refresh in Opera 8.51 fixed. Bug fixed: Some of the fancy javascript stuff didn't work in IE5 and IE5.5 but now they at least don't give any error messages. New feature: It is now possible to switch off the Tell a Friend feature from the Show Issue page. (Real#0219) Bug fixed: Followups could be autosaved even if they didn't have any text. (Real#0220) Bug fixed: Failed email sendouts failed on the error catching because of a typo. New feature: 'Your next action issues' list on homepage. Disabled by default but can be enabled on the user settings page. Bug fixed: Searching and finding only one issue when filters are on is now more careful. (Real#0072) New feature: When you set up accepting emails for a POP3 account you can also blacklist certain email address like '*@hotmail.com'. You can also whitelist exceptions to the blacklist patterns. New feature: Being able to change issues once added is now governed by a permission called 'Change Issue Tracker Issues'. New feature: You can now reply to issue notifications sent out from an issuetracker if the sitemaster email address is one of the accepting emails in the POP3 setup. This makes it possible to completely discuss an issue without even using a web browser. Parsing of the replies is best done with Formatflowed installed. New feature: POP3 inbound email feature now prevents duplicates and it is now possible to use this without deleting emails from the server. Bug fixed: Typo for 'remember_savedfilter_persistently' cookie key reference which caused excessive LOG() messages. New feature: Autosave for followups enabled. Autosaving is now only done when focus is on the subject or description/comment. New feature: Ability to specify Estimated time and Actual time as attributes on an issue. See Advanced Properties tab if interested. New feature: A much improved searchterm-highlighter that doesn't highlight words like 'a' when their inside a word like th*A*t. New feature: New Advanced option that disallows adding issues with a potentially duplicate subject/title. New feature: keyboardshortcuts.js which allows you to use the keyboard to navigate. Enabled only if selected on the User page. - 0.6.13 New feature: Which used filter used is only remembered in one session, if you're a Issue User you can set it to remember this persistently. Thanks Jan Kokoska for the push (Real#0186) Bug fixed: Applying a filter when inside the Complete List page would redirect back to List Issues. Thanks Benjamin Higgins (Real#1087) Bug fixed: It was not possible to change the "URL" for an issue when using the little "change details" form on the issue display. New feature: CleanOldSavedFilters() (part of Update Everything) can now tidy up the amount of saved filters stored inside an issuetracker. Needed as part of fixing Real#0183. Bug fixed: Subscription form caused invalid XHTML. (Thanks John J Lee) Bug fixed: If you had a Zope object called Reports somewhere in the acquisition path, you could get an AttributeError: getScripts on the Home page. Thanks David Snowsill (Real#0180) Bug fixed: Statistics page didn't work with a completely new issuetracker instance. Thanks Tim Sparkes. Bug fixed: User page now usable with old Konqueror (Real#0158) Bug fixed: CSV export notates issue IDs with # so that Excel/oocalc doesn't try to make the issue ID into an integer. New feature: Introduced new js-core.js javascript page for general functions that all pages can use. (beware those who have StandardHeader.zpt checked out!) Bug fixed: CSS is now alos cached with the more modern 'Cache-Control' header thanks to advise from Dieter Maurer. Bug fixed: When running a report the batching links didn't work Real#0172 New feature: Every time you run a report it remembers globally how many issues where found. Bug fixed: The homepage could say that an issue was from Today or Yesterday because it mathematically was so by arithmatic but did not take midnight into account to show what really was Today and Yesterday. - 0.6.12 New feature: Header background and issue header background gets a gradient shade instead of single coloured. Bug fixed: When moving to storage in a BTreeFolder the internal ID counter was lost and counting of issues was done where there are longer any issues resulting in odd issue IDs. (thanks Jason Powers Real#0167) New feature: "Recently added issues" and "Recently visited issues" have been merged into one list "Recent issues". Bug fixed: Instances using a SecureMailHost had to see the To: and From: headers repeated inside the email body. New feature: File attachments to issues or followups contain illegal characters are stripped to valid ids (Real#0169) New feature: Long filenames are truncated when shown in the issuedescription. Before they could overlap the other info. New feature: An M$ Outlook inspired homepage listing of the latest changes in issues that replaces the list of "5 latest issues". New feature: If you use a Secure MailHost, the secureSend() function is used instead to send emails. Bug fixed: Revisiting saved thread drafts didn't work. Bug fixed: If 'self.subscribers' in Issue objects were tuples, they are now automatically converted to lists. (thanks Oldrich Auda, Real#0163) Bug fixed: User.zpt wasn't checkoutable with CheckoutableTemplates (thanks Kosmas Chatzimichalis) New feature: Adding issues is bound to a different permission name "Add Issue Tracker Issues". It used to "View". This affects those who use the "Issue Tracker Manager" and "Issue Tracker User" roles. New feature: Mentions of other issues with a hash like #0150 becomes a hyperlink. (Real#0150) Bug fixed: 'remember-filterlogic' request variable was only considered for whether to session store the filterlogic, not the filtervaluer. Bug fixed: getEmailFromnameCombos() didn't fetch fromname and email correctly thus disabling POP3 reading. Bug fixed: fromname and email is saved additionally again when you're logged in as Issue User Folder user. Bug fixed: 'svn://' or 'ssh+svn://' URLs were shown with a 'http://' first prefixed. (Real#0154) - 0.6.11 New feature: Experimental Upgrade tab inside the Management tab. Bug fixed: The migration for Zope 2.8.0 on the ZCatalog wasn't backwardscompatible. The migration script is now wrapped in a if statement so that it's only run if it exists. New feature: Press Update Everything to make sure all issues and threads have the latest name and email if users are defined in a Issue User Folder. Bug fixed: _getOthers() was still using self.email instead of self.getEmail() on issues which meant trouble if the submitter of the issue would be a Issue User Folder submitter with no previously submitted issues. New feature: From Advanced Properties you can set scores for each status and calculate a guesstimate overall status progress. Bug fixed: When adding an issue, request variables are now only taken from REQUEST.form which avoids any cookies or session variables. (Thanks Eric Bressler Real#0152) Bug fixed: All references to REQUEST.AUTHENTICATED_USER gone. (Real#0057) Bug fixed: Password reminders could not find SecureMailHost mailhost objects to send emails via when outside an issuetracker. Bug fixed: Bug reporting and error handling used the old address to the Real issuetracker. Now it's using real.issuetrackerproduct.com. Bug fixed: Spam detection on inbound emails are now also checked on lowercase header keys which can be the case on some servers. Bug fixed: Fixed potential bug where default_display_format attribute isn't set on the class instance (Real#0129) Bug fixed: Now works with Zope 2.8.0 (Real#0108, Real#0136, Real#0120). Make sure you press the "Update Everything" button after installation. New feature: By default Save Drafts is now set to True. Bug fixed: Bad optimization of ListIssues|CompleteList could result in AJAX for showing/hiding filter options didn't work. Bug fixed: getFromname and getEmail in Issue class could get KeyError if a user has been moved. New feature: All sourcecode cleaned up with Pyflakes (http://divmod.org/projects/pyflakes) Bug fixed: The "Allow issue attribute change" property now actually does something. You can now post-submission change the issue attributes such as section and urgency. - 0.6.10 Bug fixed: addissue.js had a malformed line which caused incorrect Javascript code if you use slimmer 0.1.16 which could result in autosaving not working on the Add Issue page. New feature: Statistics got a barchart on issues by status. New feature: Drafts are cleaned up on issue submission unless a draft_issue_id is available. (Real#0126) Bug fixed: "Notify the others" on followup form only listed names with a valid email address. (Real#0123) Bug fixed: RSS feed sort order is now the same as the of the Home page. (Real#0124) Bug fixed: RSS feed escaped with CDATA instead of both that and HTML quoted. (Real#0125) - 0.6.9 Bug fixed: If no name or email on the issue and the user, it would say that you're already subscribing to an issue. (Thanks Benjamin Higgins Real#0113) New feature: Deleting drafts from the Add Issue page is done with AJAX. New feature: if you want to use a Secure Mail Host instead of the default Mail Host, it is now recognized as 'SecureMailHost' but 'MailHost' still takes precedence. Bug fixed: Which type of List Issues was remembered but not used if you were logged in as Issue User. (Thanks Marcus Scotti, Real#0110) Bug fixed: Filter on fromname didn't work due to a bug in a regular expression. (Thanks Fred Damberger, Real#0103) Bug fixed: getEmail() and getName() would raise an error if the acl_users (if applicable) folder can't be found, which might be the case when importing an issuetracker. Bug fixed: DeployStandards checked folder existance with acquisition. New feature: If AJAX works the filter options are loaded with AJAX otherwise loaded as a new page request (on the List Issues or Complete List). New feature: When you search on multiple words, an alternative search link appears that puts "or" between all words. (Real#0102) New feature: export.csv makes it possible to download all issues in one single CSV file. If you enabled 'CSV export link' in the Advanced properties you can export the current set to CSV. Bug fixed: Assignment form no longer shown on Complete List (Real#0101) Bug fixed: The first saved draft wouldn't pop up again on Add Issue. Real#0079 became reproducable and fixed. New feature: AddIssue and QuickAddIssue is autosaving drafts now with AJAX. New feature: Inbound emails with header X-Spambayes-Classification=spam is automatically deleted and not uploaded. New feature: Option to enforce a different stylesshet http://www.issuetrackerproduct.com/Documentation/How-Tos/forced-stylesheet Bug fixed: Assigned, Added, followups etc. on the User tab were sorted such old ones were shown first in the list. Reversed that. (Real#0078) New feature: If an error occurs the an error log file is only created if the error type is *not* in zope's error_log object on Zope2.7. (Real#0077) New feature: Which list you want to use is now stored in a cookie rather than a session. - 0.6.8 Bug fixed: Emails couldn't be sent out if you had converted to storing all issues in a BTreeFolder2. So if you were using that, this fix will renable email dispatch for probably several unsent emails. New feature: Mention who added the issue on always notify messages (Real#0086) Bug fixed: Password reminder for Issue User Folder (Real#0025) Bug fixed: The 'subscribers' can no longer be set when instanciating the IssueTrackerIssue class. This solves Real#0033. New feature: When showing the issue it shows the age next to the date it was submitted, like on the List Issues. - 0.6.7 Bug fixed: It was not possible to create a Issue User Folder. Thanks Jeff for Real#0073. Bug fixed: The default sort order is now 'issuedate' and not modification date which was not recommended. Bug fixed: Who you choose with Tell-a-Friend is no longer remembered in a cookie. Can't remember why it was even doing it before. New feature: It is now possible to change the assignments if you're the one it's assigned to (Real#0045) Bug fixed: You couldn't use the View permission toggle because of a typo caused by legacy code. Thank you Goeldi (Real#0068) Bug fixed: Added SendInboundEmailConfirm_script.py back until a more grand solution has been found. - 0.6.6 New feature: Added firstname filtering as mentioned in Real#0065 New feature: If subscription option on, when you look at an issue that you're already involved in, you do not get the option to subscribe since you're already implicitily a subscriber. (Real#0047) New feature: Filter settings you make are remembered persistently. They are stored with a (hopefully) descriptive english title. You can at any time select a previously used filter and apply it again. Bug fixed: Successfully telling a friend will no longer show the more options form again. New feature: Have fixed all source code to consistently use 4 spaces for tabs. Before there was mix which could cause problems for other people except the author who uses jed (which doesn't care about the difference) New feature: AddIssue now has well defined tabindexes. Bug fixed: Finally Real#0014 has been solved thanks to Petter Warnsberg of Swede Ltd. It's called the "Holly hack" and we managed to reproduce it and see how it started to not blank out the followup box. New feature: Lists on the User page are now shortened with an option to show all. New feature: If you assign an issue to someone who is also on the 'always notify' list it used to send out two emails. One about the new issue and one about the assignment. Not it only sends the assignment one. (Real#0026) New feature: Link to Add Issue on Quick Add Issue takes what you started writing with you to Add Issue. Thanks Jan (Real#0054) New feature: Printing issues (and consequently CompleteList) now looks much better with all the forms and navs hidden. Bug fixed: If you use "Tell a Friend with more options" and change the default message, the default message was still what was sent. Bug fixed: Issue threads and notification objects' ids are now incremented sanely. Before it was 1,3,5,7,... now it's 1,2,3,4,... New feature: NotYetRecent not in the URL when issue added (Real#0048) New feature: "Images in menu" and "Can add new sections" now part of the Properties Wizard. Bug fixed: If the display format is StructuredText and the text is just a number (eg. a telephone number) it *was* converted to a numbered list. UPDATE: this introduce a new bug that is now fixed. Bug fixed: If you tried to upload an image that doesn't exist it would return hard error when it tried to create a thumbnail. Bug fixed: New issues weren't saved to the BTreeFolder2 if you had set to use one. Bug fixed: Fixed the hard ParseError on stopword searches (Real#0024) New feature: If you have lots of issues you can now change the issue tracker to store all issues in a BTreeFolder2 container instead. This changes the URLs but if you accidently use a stale URL it will redirect you to the correct new issue URL. New feature: Issues are now movable in the ZMI. (Real#0030) New feature: Much improved Properties tab that uses web standards and Javascript. Degrades perfectly. See http://www.issuetrackerproduct.com/News/improved-properties-tab Bug fixed: Lots of tidying up of the XHTML to make tidy happier. Bug fixed: Fixed bug in relative_url() due to new feature below. Bug fixed: Displaying issues without the "Encode email links" option on would cause a problem with addhrefs version 0.6 (Thank you Jan Real#0034) New feature: discovery of absolute_url_path() replaces custom method. New feature: Ids are shown on the *left* of titles now (Real#0032) (if you enable showing ids with the title) Bug fixed: Email addresses found inside an issue or a followup text is also encoded if that option is on. (http://www.peterbe.com/plog/add-hrefs-III) New feature: Option for Issue Users to enable keyboard shortcuts. Bug fixed: Due to a typo in getFilterValue() filters weren't saved in the SESSION so if you go anywhere else the filter options were forgotten. Bug fixed: Filters on lists from the user tab now work. Real#0010 Bug fixed: Home page looked dodgy with no issues but Recent search history. Real#0002 Bug fixed: Use of builtin False caused bombing out NameError for people who use Python2.1. (Also fixed some SyntaxWarnings for Python2.1) This fixes Real#0028. Thanks Stephan Goeldi. - 0.6.5 Bug fixed: Pressing the Update Everything button twice caused a CatalogError because it tried to add certain indexes twice. - 0.6.4 Bug fixed: When writing a followup to an issue in the form at the bottom of the page and change your mind to use another action the text you've written is remembered. (Real#0020) New feature: New (advanced) option to show the issue Id next to the title of the issue in all displays. (asked for by Brent Skinner) New feature: If your search term is an issue id prefixed with a # it goes immediately to that issue without searching inside titles or descriptions. Bug fixed: POP3 management page could say that you need to have a folder called 'pop3' when it was not necessary (Real#0023) Bug fixed: Menu lost focus on Add Issue when you pressed the Save draft button. Bug fixed: Some hex encoded characters didn't show up correctly in the User template and shortening them went wrong in the Add Issue drafts list (if you have one). New feature: Previewing a followup or an issue now shows properly which people will be notified once it's submitted. New feature: User tab now has a list for all issues you are subscribed to. New feature: Issues and threads are now indexed in the ZCatalog with the ZCTextIndex instead to give much better search results when searching for issues. It's faster and more advanced. (Press the Update Everything button to activate it) New feature: Using a search term which is a section, status, type, urgency, fromname or email will make a special filter-link appear under the search box. New feature: _searchCatalog() is now more careful to not fetch thread objects that don't exist. (thanks Mark Thomas, Real#0018) New feature: Great speed optimization to ListIssues and CompleteList. Bug fixed: Resaving the cookie with draft issues now makes sure there's no stale drafts in the cookie. New feature: Compact and Rich lists in ListIssues. Default is Rich which is the good old list but if you click Compact you get a compacter list. See bottom of ListIssues page. New feature: People who can log in as Issue Users to an IssueTracker that is publically available can enable auto-login which means they're taken passed the login page when they come back. Bug fixed: Cookies expiration is again stored with proper RFC822 format. Bug fixed: Fixed some issues with the tell_a_friend feature for issue users where their name wasn't found. New feature: Check in SubmitIssue() prevents duplicates. Same happens for new followups. Bug fixed: Fixed some filtering links from the Statistics page. New feature: Now you can specify Issue User Folder usernames in the Always notify property too. New feature: Added a standard_error_message with user-friendly error messages and directions for bug reporting. - 0.6.3 Bug fixed: CheckoutableTemplates was made a requirement for IssueTrackerProduct to start. Caused broken objects for people without CheckoutableTemplates. Bug fixed: HTML escaped in displayBriefTitle() used in Recent History and Home page but HEX HTML entities preserved. - 0.6.2 Bug fixed: Made ValidEmailAddress() return True for foo'bar@some.com Bug fixed: Typo in IssueUserFolder made it impossible change 'must_change_password'. Bug fixed: Improvements to feature about new sections (see below) Bug fixed: Improved how a Issue User saves display_format New feature: New option for making it possible to add new sections to an issuetracker when adding a new issue. (feature disabled by default. see Properties tab) New feature: Implemented a much faster unique() function. Bug fixed: sendReturnErrorEmail() had a typo bug. (thanks Bart Cortooms) Bug fixed: _alwaysNotifyMessage() now gets the title of an issue via the getTitle() method. Bug fixed: Filtering links from the home page used the wrong approach. Instead of showing the selected status it showed everything else. Thanks Simon Lucy Bug fixed: AcceptingEmail class inherited a whole module, not a class. This caused problems with its attributes and methods. Bug fixed: If 'slimmer' is installed, the Management, Properties and POP3 tabs would turn black. This happened because TemplateAdder.py had a bug where the 'optimize' parameter was incorrectly remember. Bug fixed: generateID() could raise a ValueError in Python2.1 if and when the prefix is an empty string. New feature: Option to disable XHTML optimization (if being applied) with environment variable OPTIMIZE_ISSUETRACKERPRODUCT Bug fixed: StandardHeader template could not be checked out even though it was defined as a CheckoutableTemplate. Bug fixed: Saving listlike properties through the Properties tab would not filter duplicates. New feature: this_package_home defined in IssueTracker class to filter out templates when using showCheckoutableTemplates. - 0.6.1 New feature: Installed addhrefs.py version 0.5. New feature: Option in Properties tab to make default sort order by modification- or by creation date. Also implemented in Properties Wizard. Bug fixed: Finding one issue when searching did not take the searchterm with it so no highlighting. Bug fixed: When initiating the ZCatalog, the 'title' TextIndex was not attached to the Vocabulary which prevented wildcard searches on the title. Bug fixed: Previewing follow ups shows who will be notified but if someone in that list has left their email blank they are now ignored. Bug fixed: Statistics could raise an UnboundLocalError in certain sitautions. Bug fixed: Assigning an issue to oneself does not check the little checkbox about notifications that appears. New feature: Installed the new addhrefs module (see http://www.peterbe.com/plog/add-hrefs-II) Bug fixed: Using getRolesInContext(self) instead of getRoles() to take extra care for local roles. (Thanks Danny W. Adair, Asterisk Ltd) Bug fixed: Quick Add Issue could sometimes ask for Email when you are logged in as a Issue User. Bug fixed: If you delete an issue user the assignment as shown in ShowIssueData will no longer raise an error. Bug fixed: UnboundLocalError in dispatcher() caused notifications about followups not to be sent out. Bug fixed: If you forgot the subject line in a new issue, the REQUEST object was not passed back to AddIssueTemplate properly. Bug fixed: sendAlwaysNotify() is now wrapped with a nice try:except: catcher Bug fixed: Displayformat for Issue Users sometimes forgotten. Bug fixed: Switching on Issue Assignment was not backwardcompatible with old instances. Bug fixed: Was not compatible to Python 2.1 in HighlightQ(). - 0.6.0 Bug fixed: Assignees get notified with an email if not selected otherwise. Bug fixed: It is now possible to specify a notifyable groupname as an always notify. Will be very useful for teams that should all get notified of an added issue. New feature: You can now specify (in Properties tab) the signature to be used for the email sendouts. New feature: New option in properties makes it possible to have little icons to go with the items on the menu bar. See Properties tab. New feature: When logged in (standard ACL User or IssueTrackerUser) you now get a page with all your related issues and followups. New feature: An internal counter keeps track of what numerical increment to use when generating the next Id for an issue or a follow up. This works like sequences in PostgreSQL and prevents reappearing URLs when issues have been deleted. New feature: title tag now reflects what page you're on. Bug fixed: JPEG file attachments uploaded when using Internet Explorer used a non-standard content-type. (Thanks Melvin Jacobson) Bug fixed: File attachments that aren't file attachments can not be uploaded. Before one could manually write anything in the file attachment input box and it would be treated as a file. (Thanks Melvin Jacobson) New feature: Much improved information page about StructuredText with simple quick examples. New feature: Temporarily uploaded file attachments are now stored inside the issue tracker instance instead of the global /temp_folder which should do away with some low level persistency errors in Zope2.7.x. New feature: A button on the Management tab (ZMI) that updates everything that deploys standards, updates the ZCatalog, assures all objects have the correct attributes and clears temporary uploaded file attachments. New feature: A new and improved Properties Wizard. New feature: When showing issues, the urgency is explicitly styled if anything else than 'normal'. E.g. 'critical' is shown in bold and red. New feature: Whenever you add an issue, the selected sections you chose become more "popular" since their position in the list of options is moved up one notch. New feature: File attachments that are images are uploaded now with a little thumbnail if PIL is installed. New feature: Added a RSS 1.0 feed as the default feed. The 0.91 feed can still be reached on /rss-0.91.xml. Bug fixed: Certain strictly Management pages weren't secured for Zope managers only. Now they are. Bug fixed: When you update the catalog manually from the Management tab it logged lots of ERROR(200) messages which were not necessary and should now not appear anymore. New feature: ListIssues changed in layout and now also shows the first couple of words from the issue description. Bug fixed: Since Zope 2.5.x, when you add a ZCatalog it does not automatically create a Vocabulary. Added that to InitZCatalog(). New feature: Improved the use of Recent history so the page templates will now show them without having to create a Batch object. This will decrease rendering time. New feature: If you search an click a found issue, '?=' is not passed in the URL but still works the same. More convenient for copy-and-paste URLs without the search argument. New feature: All templates are now interfaced as CheckoutableTemplates (see http://zope.org/Members/peterbe/CheckoutableTemplates) This means that any IssueTrackerProduct instance's templates can easily be customized without having to change any sourcecode and makes it possible to install upgrades. New feature: Almost all occurances of absolute URLs in the generated HTML has been replaced by relative ones. This makes the resulting HTML slightly smaller in size. New feature: ListIssues and CompleteList is now much faster if you are not doing a search. It was before doing a search on nothing instead of using objectValues(). Great speed improvement. New feature: POP3Account now has a configurable port number. Bug fixed: POP3 was not working in Zope270 due to a bad class import. - 0.5.2 Bug fixed: Added View security on RSS feed. Bug fixed: Removed the use of the variable 'yield' since this is now a reserved keyword in Python 2.3.x New feature: If you're logged in to Zope, your fromname and email is stored so that if you loose your cookie but log in the same the fromname and email is still there. Bug fixed: preParseEmailString() in IssueTrackerUtils() could return None when expecting list, now it returns [] in this case. Affected POP3. Bug fixed: Email and followup action shown separatly now on "5 Latest Issues". Bug fixed: Sometimes the "Notify the others" on followup showed incorrect results. Bug fixed: Subscribers aren't automatically added to all next issues. New feature: List Issues now shows what last happened in every issue. New feature: "5 Latest Issues" shows who posted the last followup. New feature: If the submitter is in Always notify, he is not getting the Always notify email. - 0.5.1 Bug fixed: AddIssue and QuickAddIssue wasn't properly implemented with the ClassSecurityInfo() which would cause a "unauthorized" error without login prompt if a parent object has the View permission switched off. Bug fixed: The accesskeys previously implemented didn't work properly in Internet Explorer so I took it off until I know more about adding accesskeys. New feature: Accesskeys to the main tabs. Try for example: ALT + h New feature: There is now a 'Show Always notify status' option if you want to display which Always notify that have been notified. Very useful to the "issue adder" if uncertain who gets to know about the added issue. Default is off. See Advanced properties. New feature: If you call rss.xml?show=all then it also shows threads/follow ups are items with "Followup to:" added to the title of the issue. Bug fixed: rss.xml did not sort on 'issuedate' but on default sortorder which could become confusing. Bug fixed: When using your own statuses and the first one is not 'open', you would get an error when trying to add an issue when not running as manager. Bug fixed: When creating an issuetracker instance and using the Properties Wizard some default boolean values were incorrectly saved. Bug fixed: Creation of emails was buggy because of spelling misstake. It read '\r\b' when it should have been '\r\n'. Bug fixed: Dodgy emails wheren't ignored properly. Now they are. Bug fixed: "(mail@peterbe.com) bla bla" was converted to: "<a href="mailto:(mail@peterbe.com)">(mail@peterbe.com)</a> bla bl" but now that is fixed. - 0.5.0b New feature: When viewing an issue, the Add followup etc. buttons is only shown at the top. At the bottom is a minial version of the Add Followup form. New feature: Now statuses are no longer hardcoded. See the Properties tab where each line is a state,action combo of statuses. Looking at the existing list it should be obvious how it works. New feature: Download button and the Confidential & Hide me options are now by default hidden. New feature: JavaScript encoded email hyperlinks. Idea taken from http://www.zope.org/Members/jmeile/email%20encoder A new property has been introduced for this. See bottom of Properties tab. This is **recommended** for public instances. New feature: A simple Statistics page. It's linked from the Home page if you're logged in as Manager or if you have switched of the Private Statistics property. New feature: Inbound emails now get a confirmation message sent back. This is optional and for it to work you need to redeploy standards to get the SendInboundEmailConfirm_script Bug fixed: Accepting email objects can not be created if the email address is already used as an always-notify to prevent notifications becoming inbound email. Bug fixed: Dodgy email attachments are now ignored thanks to an added try & except statement. New feature: When you upgrade the product some new properties might have been introduced but not instanciated for. Visit the Management tab and see the All properties header. New feature: Inbound email via POP3. You can now set up one or more POP3 accounts each with one or more email addresses that are dedicated to sniff for emails sent to it that is converted to issues. It depends on the email package from http://mimelib.sourceforge.net/ and if you want to strip HTML formatted emails you are advised to install Strip-o-Gram from http://www.zope.org/Members/chrisw/StripOGram For this to work you are expected to set up some sort of cron job to periodically check for new email. Bug fixed: Lines that start with a URL or an email address turned to e.g. c<a href="mailto:opyright@dot.com">opyright@dot.com</a>. Now that has been fixed. Bug fixed: Removing several groups in the Notifyable groups now works. Thank you Wolfgang Reinelt. Bug fixed: Adding and removing other properties now work. Bug fixed: Open option available when issue already Open. (was because it tested if 'Open'=='open') New feature: Added createIssueObject() method so that you'll be able to create issue objects other than via the web. New feature: Added Subscription to issues. See Properties tab to enable this. Bug fixed: Searching for name or email was before assumed AND, but now it assumes OR. New feature: File attachements to followups. See Properties tab. Bug fixed: Improved getRoot() even more since last bug fix was not sufficient. Bug fixed: When pressing the Delete button you no longer get a Javascript error. Bug fixed: Improved getRoot() when using Virtual Hosts Bug fixed: When pressing 'Save Changes' in the Properties tab the page was returned completely blank. Now it "redirects" back to the Properties tab as expected. Bug fixed: ShowIssueData and the 'change these attributes' action link was misspelled. Now fixed. Gave up on the ReportLab module. CompleteList will have to suffice for now. Bug fixed: UpdateCatalog() now clears the catalog before it reindexes all objects. New feature: added showURL2Issue() which concatenates too long URLs in ShowIssueData. Bug fixed: Search highlight does now not mess with tags. Try searching for 'strong' or 'p'. Before it looked odd. Bug fixed: Latest issues on homepage is now latest issues. Before it used cookie sortorders. - 0.4.9b Improved "5 latest issues" to indicate followups. Improved wording in batchlinks. Improved search results so that it applies sorting based on the actual search. New feature: Comments searchable. Together with this, see the Management tab for the new Update Catalog button which you'll need to press. - 0.4.9a Bug fixed: Searching the catalog with Zope2.6.1b1 New feature: Added Properties Wizard for when you create an instance. Can be reached from the Properties tab too. New feature: Added favicon.ico object. Improved look of the StandardHeader and standards.css This requires redeployment. Bug fixed: Always notify crashed if no always_notify. New feature: Complete batch alternative "Show them all". Bug fixed: filteroptions now stored in session. New feature: Hidable filter options. New feature: Sortorder settings stored in cookie now. New feature: CompleteList page, shows all of issues and their comments. Improved addhrefs() to ignore ending brackets, dots, commas and semicolon. Improved manage_editIssueTrackerProperties() to use __dict__ instead of exec New feature: cache prevent based on a property called 'Stop cache' Requires that you redeploy StandardLook. Bug fixed: ListIssues now has security settings like it's supposed to have. New feature: If you set 'Allow issue attribute change' in Properties managers can change the properties of an issue once it's been submitted. Thanks Robert Allyn for suggesting this. Bug fixed: 'Notify others' option was ignored whilst you're previewing a followup. Bug fixed: how always_notifys are sent out. Thanks Robert Allyn for noticing this. New feature: to Management called ReplaceEmail. - 0.4.8a Added HREFs to URLs in text. Fixed so that anonymous users could preview followups. Bug fixes when UpdateCatalog() has been invoked. Made getRecentHistory() careful with deleted issues. Fixed some bug where instance variable name_cookiekey was used. - 0.4.7b Removed gif [action]buttons and used CSS instead. (faster loading) Added always_notify property so that you can have people that get emails silently about all new issues added. This works together with Notifyables. Made it possible to search by Issue id. Type in an issue id and it takes you there. Made emailstring cookies be per instance but fromname and email is across all instances. Added "Your Recent History" feature. It's "cacheconfusion" is debatable still. Changed the searching so that when searching the title attributes is wrapped with two wildcards on both sides. Will now find "This is the title..." when searching for "itl". Added preview when adding a followup. Fixed bug in "Complete redeployment" Now you can Send2Friends from any issue and not only newly added ones. (requires you to redeploy SendIssue2Friends_script) Added link to document about Structured Text. Fixed little bug in SendIssue2Friends_script so it saves email address even if emails fail to send. Removed lots of redundant code. Especially from IssueTrackerUtils. Created the Issue Tracker Notifyable Container folderish object so that many instances can share the same notifyables. Made notifyable groups a class with 'Issue Tracker Notifyable Group' objects instead of just a list property. Separated out the notifyables stuff to IssueTrackerNotifyables.py. Created IssueTrackerConstants.py module for better software design. Usability improvments to Add Issue, Quick Add Issue and Management. - 0.4.6a File attachments feature (see properties to turn it on). Nice and cachable URLs now used for sortorder, reverse and batchsizing. ListIssues/sortorder-sections/reverse-1/start-12 ...instead of... ListIssues?sortorder=sections&reverse=1&start=12 Improved AddIssue, QuickAddIssue and AddFollowup so that it sets focus in the first field of the input first automatically. (requires StandardLook object refreshed) Modified generateID() to generate more sensible ids independent of their location in space. Minor improvements to the dispatcher.html object in standards. Fixed the tabs for IssueNotifications. Fixed the tabs for IssueThreads. - 0.4.5b Fixed some major bugs for sending out emails. You must now redeploy the dispatcher.html object. Added search feature. Queried word is highlighted case insensitivly. - 0.4.4b What makes an IssueTracker manager is now stored in 'manager_roles' property. Emailstrings on new issues for multipmail is now saved in a cookie. You need to Redeploy the SendIssue2Friends_script object (i.e. delete it and visit the Management tab and press "Deploy and preserve existing") Fixed a typo that broke the batching of lots of issues. Twice. - 0.4.4a Added sendEmail() method and changed SendIssue2Friends_script.py correspondingly. Added the filter options at the bottom of ListIssues (for evaluation) - 0.4.3 Started making the ZClass product a python product ���������������������������������������������������������������������������IssueTrackerProduct/dtml/���������������������������������������������������������������������������0000755�0001750�0001750�00000000000�11012074374�015620� 5����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/ManagementUsers.dtml�������������������������������������������������������0000644�0001750�0001750�00000011360�11012074373�021600� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Management')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <dtml-var "ManagementTabs('Users')"> <style> div.area { background-color:#efefef; padding:1px 7px; margin:5px;} </style> <p class="form-title">User Management</p> <div class="area"> <p><strong>Anonymous access</strong></p> <dtml-if "isViewPermissionOn()"> <p>This IssueTracker instance is currently <b>open</b> to the public (or equivalently what public is to your network setup). This means that anonymous access is granted. Anonymous users can add issues and see the non-confidential ones listed.</p> <form action="manage_ViewPermissionToggle"> <input type="hidden" name="DestinationURL" value="<dtml-var URL>"> <p>This means that the <code>View</code> permission is granted for the <code>Anonymous</code> Zope user. If you want to switch off this and only allow users who can log in access, press the button below:<br> <div align="center"> <input type="submit" value="Disallow anonymous access"> </div> </p> </form> <dtml-else> <form action="manage_ViewPermissionToggle"> <input type="hidden" name="DestinationURL" value="<dtml-var URL>"> <p>This IssueTracker instance is currently <b>closed</b> to the public (or equivalently what public is to your network setup). This means that anonymous access is <b>not</b> granted. Every visiting user must have a role other than Anonymous that gives them access. You can grant anonymous access by pressing the button below:<br> <div align="center"> <input type="submit" value="Allow anonymous access"> </div> </p> </form> </dtml-if> </div> <p> </p> <div class="area"> <p><strong>Issue Assignment</strong></p> <dtml-let userfolders="getAllIssueUserFolders()" all_users="getAllIssueUsers(userfolders, filter=0)"> <dtml-if "UseIssueAssignment()"> <dtml-if all_users> <dtml-let blacklist="getIssueAssignmentBlacklist(check_each=1)"> <form action="<dtml-var URL1>"> <input type="hidden" name="DestinationURL" value="<dtml-var URL>"> <p>You can blacklist (make unselectable when adding an issue) some users here that it will not be possible to assign issues to. By default, an issue can be assigned to anybody from the selection list below. <table> <tr> <th>Available</th> <th> </th> <th>Blacklisted</th> </tr> <tr> <td><select name="add_identifiers:list" multiple="multiple" size="<dtml-var "_.min(10, _.len(all_users)-_.len(blacklist))">"> <dtml-in all_users mapping> <dtml-if "identifier not in blacklist"> <option value="<dtml-var identifier>"><dtml-var "user.getFullname()"></option> </dtml-if> </dtml-in> </select> </td> <td>   </td> <td><dtml-if blacklist> <select name="remove_identifiers:list" multiple="multiple" size="<dtml-var "_.min(10, _.len(blacklist))">"> <dtml-in all_users mapping> <dtml-if "identifier in blacklist" > <option value="<dtml-var identifier>"><dtml-var "user.getFullname()"></option> </dtml-if> </dtml-in> </select> <dtml-else> <p><em>None blacklisted</em> </dtml-if> </tr> <tr> <td align="center"><input type="submit" name="manage_AddToBlacklist:method" value="Blacklist >>>"></td> <td> </td> <td align="center"><dtml-if blacklist><input type="submit" name="manage_RemoveFromBlacklist:method" value="<<< Enable"> </dtml-if></td> </tr> </table> </form> </dtml-let> <dtml-else> <p>You have Issue Assignment <b>on</b> but no users defined.<br> <dtml-if userfolders> <a href="<dtml-var "userfolders[0].absolute_url()">/manage_main">Click here to define some users</a>. <dtml-else> First you have to create a Issue Tracker User Folder object. Follow <a href="manage_addProduct/IssueTrackerProduct/addIssueUserFolder">this link</a> and return here after. </dtml-if> </p> </dtml-if> <br> <form action="manage_UseIssueAssignmentToggle"> <input type="hidden" name="DestinationURL" value="<dtml-var URL>"> <p>You can disable Issue Assignment without loosing your settings for blacklisting.<br> <input type="submit" value="Disable Issue Assignment"> </form> <dtml-else> <form action="manage_UseIssueAssignmentToggle"> <input type="hidden" name="DestinationURL" value="<dtml-var URL>"> <p>You currently have Issue Assignment <b>disabled</b> meaning that IssueTracker users can not be assigned to issues even though IssueTracker users are defined. To switch it on, follow the link below:<br> <div align="center"> <input type="submit" value="Use Issue Assignment"> </div> </p> </form> </dtml-if> </dtml-let> </div> <br>  <dtml-var manage_page_footer> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/tiny_mce_itp.js.dtml�������������������������������������������������������0000644�0001750�0001750�00000001071�11012074373�021576� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=1)"><dtml-call "RESPONSE.setHeader('Content-type','application/x-javascript')"> tinyMCE.init({ mode : "textareas", theme : "advanced", theme_advanced_buttons1 : "bold,italic,underline,separator,strikethrough,justifyleft,justifycenter,justifyright, justifyfull,bullist,numlist,undo,redo,link,unlink,separator,help", theme_advanced_buttons2 : "", theme_advanced_buttons3 : "", theme_advanced_toolbar_location : "bottom", theme_advanced_toolbar_align : "left", onblur_callback : "stopautosave()", onfocus_callback : "startautosave()" });�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/ManagementNotifyables.dtml�������������������������������������������������0000644�0001750�0001750�00000001452�11012074373�022757� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Management')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <dtml-var "ManagementTabs('Notifyables')"> <p class="form-title">Notifyables Mangement</p> <dtml-if hasGlobalContainer> <p><b> <img src="/misc_/IssueTrackerProduct/issuetracker_notifyablecontainer.gif" alt="Issue Tracker Notifyable Container" title="Issue Tracker Notifyable Container" border="0" align="left" /> Bare in mind</b> that you have a global container for notifyables.<br> The ones you create here only work within this instance.</p> </dtml-if> <dtml-var NotifyableManagementPartForm> <br>  <dtml-var manage_page_footer> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/ManagementUpgrade.dtml�����������������������������������������������������0000644�0001750�0001750�00000003536�11012074373�022074� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Management')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <dtml-var "ManagementTabs('Upgrade')"> <style type="text/css"> div.good { border:1px solid green;padding:5px } div.bad { border:1px solid red;padding:5px } </style> <dtml-if manage_canUpgrade> <div class="good"> <p style="font-weight:bold;color:green">Yes! You can upgrade your IssueTrackerProduct</p> <dtml-let info="manage_getUpgradeInfo()"> <p>The latest available version of the IssueTrackerProduct is <b><dtml-var "info['version']"></b> from <a href="<dtml-var "info['url']">"><dtml-var "info['url']"></a>. </p> </dtml-let> <form action="manage_doUpgrade"> <input type="submit" value="Perform the upgrade" onclick="this.value='Performing the upgrade...';document.getElementById('pleasewait').innerHTML='Please wait...'" /> <div id="pleasewait" style="font-style:italic;font-family:sans-serif;"></div> </form> </div> <dtml-elif "manage_isUsingCVS()"> <div class="bad"> <p>You <b>can't upgrade because you're using a CVS version</b> which can't be upgraded via this management system. </div> <dtml-else> <dtml-let info="manage_getUpgradeInfo()"> <dtml-if "info['version']==getIssueTrackerVersion()"> <div class="good"> <p>You're currently using the latest version.</p> </div> <dtml-else> <div class="bad"> <p style="color:red;font-weight:bold">You can't upgrade for unknown reason :(</p> </div> </dtml-if> </dtml-let> </div> </dtml-if> <p><b>Your installed version: <em><dtml-var getIssueTrackerVersion></em></b></p> <br>  <dtml-var manage_page_footer> ������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/editNotifyableForm.dtml����������������������������������������������������0000644�0001750�0001750�00000003637�11012074373�022300� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">IssueTracker Notifyable</p> <dtml-if isGlobalHere> <form action="manage_GlobalManagementForm" method="get"> <input type="submit" value="Return to Notifyables management" /> </form> <dtml-else> <form action="manage_ManagementNotifyables" method="get"> <input type="submit" value="Return to Notifyables management" /> </form> </dtml-if> <dtml-let notify_groups="getNotifyableGroups()"> <form action="<dtml-var URL1>" method="post"> <input type="hidden" name="id" value="<dtml-var id>"> <dtml-if "REQUEST.has_key('back_url')"> <input type="hidden" name="back_url" value="<dtml-var "REQUEST['back_url']">"> </dtml-if> <table> <tr> <td><div class="form-label">Alias</div></td> <td><input name="alias" value="<dtml-var alias html_quote>" maxlength="50" size="40"></td> </tr> <tr> <td><div class="form-label">Email</div></td> <td><input name="email" value="<dtml-var email html_quote>" maxlength="50" size="40"></td> </tr> <dtml-if "_.len(notify_groups)"> <tr> <td><div class="form-label">Groups</div></td> <td> <select name="groups:list" multiple="multiple" size="<dtml-var "_.len(notify_groups)">"> <dtml-let this_groups="getGroups()"> <dtml-in notify_groups> <option value="<dtml-var getId>" <dtml-if "getId() in this_groups">selected</dtml-if> ><dtml-var title></option> </dtml-in> </dtml-let> </select> </td> </tr> </dtml-if> <tr> <td colspan="2" align="center"> <input type="submit" value="Save Changes" name="manage_editNotifyable:method"> </td> </tr> </table> </form> </dtml-let> <dtml-var manage_page_footer> �������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/configureMenuForm.dtml�����������������������������������������������������0000644�0001750�0001750�00000005222�11012074373�022134� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <script type="text/javascript"> function clearInputs(number) { document.getElementById("href"+number).value=""; document.getElementById("label"+number).value=""; document.getElementById("inurl"+number).value=""; } </script> <div style="text-align:right;font-size:10px; padding:6px"> <a href="manage_editIssueTrackerPropertiesForm" style="text-decoration:underline"><<< Return to all properties</a> </div> <form action="manage_editMenuItems" method="post"> <table> <tr> <th> </th> <th>href</th> <th>label</th> <th>inurl</th> </tr> <dtml-in "getMenuItemsList()" mapping> <tr> <td><dtml-var sequence-number>.</td> <td><input name="hrefs:list" id="href<dtml-var sequence-number>" size="30" value="<dtml-var href>" /></td> <td><input name="labels:list" id="label<dtml-var sequence-number>" size="30" value="<dtml-var label>" /></td> <td><input name="inurls:list" id="inurl<dtml-var sequence-number>" size="30" <dtml-if "hasattr(inurl, 'upper')"> value="<dtml-var inurl>" /> <dtml-else> value="<dtml-var "' '.join(inurl)"> "/> </dtml-if> </td> <td><input type="button" value="clear" onclick="clearInputs(<dtml-var sequence-number>)" /></td> </tr> </dtml-in> <tr> <td><em>n</em>.</td> <td><input name="hrefs:list" size="30" value="" /></td> <td><input name="labels:list" size="30" value="" /></td> <td><input name="inurls:list" size="30" value="" /></td> </tr> </table> <br /> <input type="submit" value="Save changes" /> <br /> <input type="submit" name="reset_to_default" value="Reset to default values" /><br /> <h4>Help</h4> <p>This page allows you to change what appears in the menu. The menu is shown in two places, the top navigation bar and centred at the bottom of each screen. One item is added dynamically and that's the Login tab which changes to the fullname if you're logged in. That tab can't be configured here.</p> <p>The only non-obvious field is the <b>inurl</b> one. This is a word (or several if split by space) that tells the dynamic script if you're on that page or not and shows this in the menu. If you're uncertain, just make it the same as the <b>href</b> but without any / forward slashes. If you enter several words for the <b>inurl</b> and separate them by space, then it will do one check for each.</p> <p>If you want to <b>remove</b> one item from the menu, just make it blank on href, label and inurl.<br /> If you want to <b>add</b> a new menu item, use the <em>n<sup>th</sup></em> line. </p> </form> <dtml-var manage_page_footer> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/addIssueUserFolder.dtml����������������������������������������������������0000644�0001750�0001750�00000011223�11012074373�022234� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-var "manage_form_title(this(), _, form_title='Add IssueUserFolder', )"> <dtml-call "REQUEST.set('in_issue_tracker', 0)"> <dtml-try> <dtml-let issuetrackerroot="getRoot()"> <dtml-if "issuetrackerroot.meta_type=='Issue Tracker'"> <dtml-call "REQUEST.set('in_issue_tracker', 1)"> </dtml-if> </dtml-let> <dtml-except> </dtml-try> <p> <dtml-if "REQUEST['in_issue_tracker']"> Since you are creating this User Folder <em>inside an Issue Tracker object</em>, the webmaster email (<em><dtml-var getSitemasterEmail></em>) will be used for <b>possible password email reminders</b>. <dtml-call "REQUEST.set('SitemasterEmail', getSitemasterEmail())"> <dtml-else> Since you are <em>not</em> creating this User Folder inside a Issue Tracker object you have to enter a webmaster email if you want to use the password email reminders feature for users within. </dtml-if> </p> <form action="manage_addIssueUserFolder" method="post"> <table cellspacing="0" cellpadding="2" border="0"> <tr> <td align="left" valign="top"> <div class="form-optional"> Webmaster email </div> </td> <td align="left" valign="top"><p> <input type="text" name="webmaster_email" size="40" value="" /> <dtml-if "REQUEST.get('SitemasterEmail')"> (you can leave this empty) </dtml-if> </td> </tr> <tr> <td align="left" valign="top"> </td> <td align="left" valign="top"> </td> </tr> </table> <dtml-if "'acl_users' in objectIds('User Folder')"> <p>Please note that since there already is User Folder here, you can to convert all the users to be Issue Users.<br> If we can't find a valid email address for a user, <em>that user will NOT be converted and you have to recreate it.</em></p> <dtml-in "manage_getUsersToConvert()" mapping> <dtml-let key="_.string.replace(username,' ','')"> <input type="hidden" name="keys:list" value="<dtml-var key>"> <dtml-if sequence-start> <table border=0> <tr> <th>Keep</th> <th>Username</th> <th>Fullname</th> <th>Email</th> <th>Roles</th> <th><em>Domains</em></th> </tr> </dtml-if> <tr> <td valign="top"><input type="checkbox" name="keep_usernames:list" value="<dtml-var key>" <dtml-unless invalid_email>checked="checked"</dtml-unless>></td> <td valign="top"><input name="username_<dtml-var key>" value="<dtml-var username>"> </td> <td valign="top"> <dtml-if "_.len(fullname)>1"> <select name="fullname_<dtml-var key>" size="<dtml-var "_.len(fullname)">"> <dtml-in fullname> <option value="<dtml-var sequence-item>" <dtml-if sequence-start>selected="selected"</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select> <dtml-else> <input name="fullname_<dtml-var key>" value="<dtml-if fullname><dtml-var "fullname[0]"><dtml-else><dtml-var "username.capitalize()"></dtml-if>"> </dtml-if> </td> <td valign="top"> <dtml-if "_.len(email)>1"> <select name="email_<dtml-var key>" size="<dtml-var "_.len(email)">"> <dtml-in email> <option value="<dtml-var sequence-item>" <dtml-if sequence-start>selected="selected"</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select> <dtml-else> <input name="email_<dtml-var key>" value="<dtml-if email><dtml-var "email[0]"><dtml-else>required</dtml-if>" <dtml-if invalid_email>style="color:red;"</dtml-if>> </dtml-if> </td> <td valign="top"><dtml-let vroles=valid_roles><select name="roles_<dtml-var key>:list" size="<dtml-var "_.len(vroles)-2">"> <dtml-in vroles> <dtml-if expr="_vars['sequence-item'] != 'Authenticated'"> <dtml-if expr="_vars['sequence-item'] != 'Anonymous'"> <dtml-if expr="_vars['sequence-item'] != 'Shared'"> <option value="<dtml-var sequence-item html_quote>" <dtml-if "_['sequence-item'] in roles">selected="selected"</dtml-if> ><dtml-var sequence-item></option> </dtml-if> </dtml-if> </dtml-if> </dtml-in> </dtml-let> </select> </td> <td valign="top"><input name="domains_<dtml-var key>:tokens" value="<dtml-var "_.string.join(domains, ' ')">"> </td> </tr> <dtml-if sequence-end> </table><br> </dtml-if> </dtml-let> </dtml-in> </dtml-if> <div class="form-element"> <input class="form-element" type="submit" name="submit" value="Add IssueTracker User Folder" /> </div> </form> <dtml-var manage_page_footer> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/ReportScriptEdit.dtml������������������������������������������������������0000644�0001750�0001750�00000007460�11012074373�021756� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-var manage_tabs> <form action="&dtml-URL1;" method="post"> <input type="hidden" name=":default_method" value="ZPythonScriptHTML_changePrefs"> <table width="100%" cellspacing="0" cellpadding="2" border="0"> <dtml-with keyword_args mapping> <tr> <td align="left" valign="top"> <div class="form-optional"> Title </div> </td> <td align="left" valign="top" width="99%"> <input type="text" name="title" size="40" value="&dtml-title;" /> </td> </tr> <tr> <td align="left" valign="top" nowrap> <div class="form-optional"> Parameter List </div> </td> <td align="left" valign="top"> <input type="text" name="params" size="40" value="&dtml-params;" /> </td> </tr> </dtml-with> <dtml-with getBindingAssignments> <dtml-if getAssignedNamesInOrder> <tr> <td align="left" valign="top"> <div class="form-label"> Bound Names </div> </td> <td align="left" valign="top"> <div class="form-text"> <dtml-in getAssignedNamesInOrder> &dtml-sequence-item;<dtml-unless sequence-end>, </dtml-unless> </dtml-in> </div> </td> </tr> </dtml-if> </dtml-with> <tr> <td align="left" valign="top"> <div class="form-label"> Last Modified </div> </td> <td align="left" valign="top"> <div class="form-text"> <dtml-var bobobase_modification_time fmt="%Y-%m-%d %H:%M"> </div> </td> </tr> <dtml-if errors> <tr> <td align="left" valign="middle" class="form-label">Errors</td> <td align="left" valign="middle" style="background-color: #FFDDDD"> <pre><dtml-var expr="'\n'.join(errors)" html_quote></pre> </td> </tr> </dtml-if> <dtml-if warnings> <tr> <td align="left" valign="middle" class="form-label">Warnings</td> <td align="left" valign="middle" style="background-color: #FFEEDD"> <pre><dtml-var expr="'\n'.join(warnings)" html_quote></pre> </td> </tr> </dtml-if> <dtml-with keyword_args mapping> <tr> <td align="left" valign="top" colspan="2"> <div style="width: 100%;"> <dtml-let cols="REQUEST.get('dtpref_cols', '100%')" rows="REQUEST.get('dtpref_rows', '20')"> <dtml-if "cols[-1]=='%'"> <textarea name="body:text" wrap="off" style="width: &dtml-cols;;" <dtml-else> <textarea name="body:text" wrap="off" cols="&dtml-cols;" </dtml-if> rows="&dtml-rows;">&dtml-body;</textarea> </dtml-let> </div> </td> </tr> </dtml-with> <tr> <td align="left" valign="top" colspan="2"> <div class="form-element"> <dtml-if wl_isLocked> <em>Locked by WebDAV</em> <dtml-else> <input class="form-element" type="submit" name="ZPythonScriptHTML_editAction:method" value="Save Changes"> </dtml-if>    <input class="form-element" type="submit" name="height" value="Taller"> <input class="form-element" type="submit" name="height" value="Shorter"> <input class="form-element" type="submit" name="width" value="Wider"> <input class="form-element" type="submit" name="width" value="Narrower"> </div> </td> </tr> </table> </form> <p class="form-help"> Download as <a href="<dtml-var absolute_url>.py"><dtml-var getId>.py</a><br /> or <a href="document_src">view the source</a> </p> <form action="ZPythonScriptHTML_upload" method="post" enctype="multipart/form-data"> <table cellpadding="2" cellspacing="0" border="0"> <tr> <td align="left" valign="top"> <div class="form-label"> File   </div> </td> <td align="left" valign="top"> <input type="file" name="file" size="25" value=""> </td> </tr> <tr> <td></td> <td align="left" valign="top"> <div class="form-element"> <dtml-if wl_isLocked> <em>Locked by WebDAV</em> <dtml-else> <input class="form-element" type="submit" value="Upload File"> </dtml-if> </div> </td> </tr> </table> </form> <dtml-var manage_page_footer> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/POP3ManagementForm.dtml����������������������������������������������������0000644�0001750�0001750�00000051023�11012074373�022044� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='POP3')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <style type="text/css"> .accountsection { } .accountsection h4 { background-color:#ccc; padding:5px; font-size:80%; font-family:arial, sans-serif; } #testaccountform { margin:0 10px; } #editaccount { float:left; margin-right:15px; } #headsup { float:right; width:45%; margin:7px; padding-left:10px; padding-bottom:5px; border:1px solid #666; border-bottom:1px solid #666; } </style> <dtml-comment> Hack: If there is only one POP3 Account we might as well show all about it all the time. </dtml-comment> <dtml-if "not REQUEST.has_key('pop3accountid') and _.len(getPOP3Accounts())==1"> <dtml-call "RESPONSE.redirect(getRootURL()+'/manage_POP3ManagementForm?pop3accountid=%s'%getPOP3Accounts()[0].getId())"> </dtml-if> <dtml-unless "manage_hasEmailRepliesPossible()"> <div id="headsup"> <p><strong>Do you want to be able to reply to email notifications?</strong><br /> For that to work you need to make the "Sitemaster email" property be the same email address as one accepting email defined below. It is also highly recommended that your new-issue-notifications contain the full description of the issue and not just the title.</p> <dtml-if "getPOP3Accounts()"> <form action="manage_enableEmailRepliesSetting"> <input type="hidden" name="pop3accountid" value="<dtml-var "REQUEST.get('pop3accountid')">" /> <dtml-if "REQUEST.has_key('pop3accountid') and hasAcceptingEmails(REQUEST['pop3accountid'])"> <p>Change Sitemaster email to: <select name="email"> <dtml-in "getAcceptingEmails(REQUEST['pop3accountid'])"> <option value="<dtml-var email_address>"><dtml-var email_address></option> </dtml-in> </select> <dtml-else> <dtml-if "ValidEmailAddress(getSitemasterEmail())"> <p>Create an accepting email for <code><dtml-var getSitemasterEmail></code> <input type="hidden" name="email" value="<dtml-var getSitemasterEmail>" /> <dtml-else> <p><strong style="color:red"><dtml-var getSitemasterEmail> is <em>not</em> a valid email address.</strong> <a href="manage_editIssueTrackerPropertiesForm">Change it first</a>. </dtml-if> </dtml-if> <br /> <input type="checkbox" name="include_description_in_notifications" value="1" checked="checked" /> and include full description in email notifications about new issues. <br /> <input type="submit" value="Make it possible to reply to notifications" /> <dtml-unless "manage_hasFormatFlowedInstalled()"> <p>It is highly recommended that you first install <a href="http://www.zopatista.com/projects/formatflowed">formatflowed.py from Zopatista.com</a> on your system to better parse the email replies. </p> </dtml-unless> </form> <dtml-else> <p>For this to work you first have to create at least one POP3 account.</p> </dtml-if> </div> </dtml-unless> <p><strong>This is how it works:</strong><br> You create one or more POP3 accounts.<br> For each account you define one or more email address which is dedicated to accepting emails into the IssueTracker.<br> For each email address you define what Sections, Type and Urgency the issue should take if it's not obvious from the subject line of the email.<br> The email sender can override these by mentioning certain words in the subject line in a special format.<br> It is your responsibility to periodically trigger the search for new emails.</p> <p>Read more about <a href="#cronjob">periodically checking for email</a> and for a list of examples of how the email sender can compose the subject of the email see the <a href="#examplelist">bottom of this page</a>. <dtml-if "REQUEST.has_key('pop3accountid')"> <dtml-with "getPOP3Account(REQUEST['pop3accountid'])"> <p class="form-title">POP3 Account <em><dtml-var getHostname></em></p> <div id="accountdetails" class="accountsection"> <h4>POP3 Account details</h4> <div id="editaccount"> <form action="manage_editPOP3Account" autocomplete="off"> <input type="hidden" name="id" value="<dtml-var "REQUEST['pop3accountid']">" /> <table> <tr> <td><p><strong>Hostname:Port</strong></p></td> <td><input name="hostname" value="<dtml-var getHostname>" /> <b>:</b> <input name="portnr:int" value="<dtml-var getPort>" size="3"></td> </tr> <dtml-if "SupportPOP3SSL()"> <tr> <td><p><strong>SSL</strong></p></td> <td><input name="ssl" type="checkbox" value="1" <dtml-if "doSSL()">checked="checked"</dtml-if> /> </td> </tr> </dtml-if> <tr> <td><p><strong>Username</strong></p></td> <td><input name="username" value="<dtml-var getUsername>" /></td> </tr> <tr> <td><p><strong>Password</strong></p></td> <td><input name="password" type="password" value="password" /></td> </tr> <tr> <td><p><strong>Delete emails after</strong></p></td> <td><input name="delete_after" type="checkbox" value="1" <dtml-if "doDeleteAfter()">checked="checked"</dtml-if> /></td> </tr> <tr> <td>  <input type="hidden" name="password_dummy" value="password" /> </td> <td><input type="submit" value="Save Changes" /></td> </tr> </table> </form> </div> <div id="testaccountform"> <dtml-if "REQUEST.get('connectiontest_result')"> <dtml-let result="REQUEST.get('connectiontest_result')"> <dtml-if "result.find('+OK') > -1"> <p style="font-weight:bold;color:green"><code><dtml-var result html_quote></code></p> <dtml-else> <p style="font-weight:bold;color:red"><code><dtml-var result html_quote></code></p> </dtml-if> </dtml-let> <dtml-else> <p>If you want to you can test the account. To see if we can connect and get the POP3 servers welcome message.</p> </dtml-if> <form action="manage_testPOP3Account" method="get"> <input type="hidden" name="accountid" value="<dtml-var "REQUEST['pop3accountid']">" /> <input type="submit" value="Test connection" /> </form> </div> <br clear="left" />  </div> <div id="newacceptingemail" class="accountsection"> <h4>Create new accepting email on this account</h4> <form action="createAcceptingEmail"> <input type="hidden" name="id" value="<dtml-var "REQUEST['pop3accountid']">"> <table border="0"> <tr> <td valign="top"><p>Email address</p> <input name="email_address" size="35" /> <p><input type="checkbox" name="send_confirm:boolean" value="1" <dtml-if "doSendConfirmSuggestion()">checked="checked"</dtml-if> /> Send confirmation message<br/> <input type="checkbox" name="reveal_issue_url:int" value="1" checked="checked" /> <acronym title="If checked, confirmation messages will contain the full URL to the newly created issued." >Reveal issue URL</acronym><br/> </td> <td valign="top" rowspan="2"><p>Default sections</p> <select name="defaultsections:list" size="<dtml-var "_.min(5, _.len(sections_options))">" multiple="multiple"> <dtml-in sections_options> <option value="<dtml-var sequence-item>" <dtml-if "_['sequence-item'] in defaultsections">SELECTED</dtml-if>><dtml-var sequence-item></option> </dtml-in> </select> </td> <td valign="top"><p>Default type</p> <select name="default_type"> <dtml-in types> <option <dtml-if "_['sequence-item']==default_type">SELECTED</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select> </td> <td valign="top"><p>Default urgency</p> <select name="default_urgency"> <dtml-in urgencies> <option <dtml-if "_['sequence-item']==default_urgency">SELECTED</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select> </td> </tr> <tr> <td> </td> <td colspan="2" align="center"><input type="submit" value="Save accepting email" /></td> </tr> </table> </form> <br>  </div> <dtml-if "hasAcceptingEmails(getId())"> <div id="existingacceptingemails" class="accountsection"> <h4>Existing accepting emails</h4> <form action="<dtml-var getRootURL>" method="post"> <input type="hidden" name="id" value="<dtml-var "REQUEST['pop3accountid']">" /> <table border="0" cellspacing="0" cellpadding="4"> <thead> <tr> <td> </td> <td><p><strong>Email address</strong></p></td> <td><p><strong>Default sections</strong></p></td> <td><p><strong>Default type</strong></p></td> <td><p><strong>Default urgency</strong></p></td> </tr> </thead> <dtml-in getAcceptingEmails sort=bobobase_modification_time reverse> <tbody <dtml-if sequence-even>style="background-color:#E5E5E5;"</dtml-if>> <tr id="ae<dtml-var getId>"> <td valign="top"><input type="checkbox" name="ids:list" value="<dtml-var getId>"></td> <td valign="top"><input name="email_address-<dtml-var getId>" size="35" value="<dtml-var getEmailAddress>"> <input type="hidden" name="allids:list" value="<dtml-var getId>"> <p><input type="checkbox" name="send_confirm-<dtml-var getId>:boolean" value="1" <dtml-if doSendConfirm>checked="checked"</dtml-if> /> Send confirmation message<br/> <input type="checkbox" name="reveal_issue_url-<dtml-var getId>:boolean" value="1" <dtml-if revealIssueURL>checked="checked"</dtml-if> /> Reveal issue URL<br/> <a href="manage_POP3ManagementForm?pop3accountid=<dtml-var "REQUEST['pop3accountid']" url_quote>&blackwhitelist=<dtml-var getId url_quote>#ae<dtml-var getId>" >Manage white- and blacklists</a> </td> <td><select name="defaultsections-<dtml-var getId>:list" size="<dtml-var "_.min(5, _.len(sections_options))">" multiple> <dtml-in sections_options> <option value="<dtml-var sequence-item>" <dtml-if "_['sequence-item'] in defaultsections">SELECTED</dtml-if>><dtml-var sequence-item></option> </dtml-in> </select></td> <td valign="top"><select name="default_type-<dtml-var getId>"> <dtml-in types> <option <dtml-if "_['sequence-item']==default_type">SELECTED</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select></td> <td valign="top"><select name="default_urgency-<dtml-var getId>"> <dtml-in urgencies> <option <dtml-if "_['sequence-item']==default_urgency">SELECTED</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select></td> </tr> <dtml-if "REQUEST.get('blackwhitelist')==getId() or getWhitelistEmails() or getBlacklistEmails()"> <tr> <td colspan="5"> <dtml-if "REQUEST.get('blackwhitelist')==getId()"> <input type="hidden" name="acceptingemail_id" value="<dtml-var "REQUEST.get('blackwhitelist')">" /> <table> <tr> <th>Blacklist</th> <th>Whitelist</th> </tr> <tr> <td> <textarea name="blacklist_emails:lines" rows="4" cols="40" ><dtml-var "'\n'.join(getBlacklistEmails())"></textarea> </td> <td> <textarea name="whitelist_emails:lines" rows="4" cols="40" ><dtml-var "'\n'.join(getWhitelistEmails())"></textarea> </td> </tr> </table> <input type="submit" name="manage_saveBlackWhitelist:method" value="Save changes" style="font-weight:bold" /> <input type="button" value="Cancel" onclick="location.href='<dtml-var URL1>/manage_POP3ManagementForm?pop3accountid=<dtml-var "REQUEST['pop3accountid']">'"/> <p class="small">Enter one email address per line. You can use * as a wildcard (<em>eg. *@issuetrackerproduct.com</em>). Default is to accept all emails.</p> <dtml-else> <dtml-if "getWhitelistEmails() or getBlacklistEmails()"> <p> <dtml-if "getWhitelistEmails()"> <b>Whitelist:</b> <dtml-var "', '.join([x for x in getWhitelistEmails()])"><br /> </dtml-if> <dtml-if "getBlacklistEmails()"> <b>Blacklist:</b> <dtml-var "', '.join([x for x in getBlacklistEmails()])"> </dtml-if> </p> </dtml-if> </dtml-if> </td> </tr> </dtml-if> </tbody> <dtml-if sequence-end> <dtml-unless "REQUEST.get('blackwhitelist')"> <tbody> <tr> <td colspan=2><input type="submit" name="manage_delAcceptingEmails:method" value="Delete selected"></td> <td colspan=3 align="right"> <input type="submit" value="Save changes" name="saveAcceptingEmails:method"></td> </tr> </tbody> </dtml-unless> </dtml-if> </dtml-in> </table> </form> </div> </dtml-if> <br />  </dtml-with> </dtml-if> <p class="form-title">POP3 Mangement</p> <table width="100%" cellpadding="4" border=0> <tr class="list-header"> <th> <img src="/misc_/IssueTrackerProduct/issuetracker_pop3account.gif" alt="Issue Tracker POP3 Account" title="Issue Tracker POP3 Account" border="0" align="left" />Existing POP3 Accounts</th> <th>Create New POP3 Account</th> </tr> <tr> <td valign="top"> <dtml-in getPOP3Accounts> <dtml-if sequence-start> <form action="<dtml-var URL1>" method="post"> <table> </dtml-if> <tr> <td><input type="checkbox" name="ids:list" value="<dtml-var getId>"></td> <td><a href="<dtml-var ActionURL>?pop3accountid=<dtml-var getId>" style="text-decoration:underline" title="Click to edit this POP3 Account"><dtml-var getTitle> (<dtml-var getUsername>)</a></td> <td><p><em style="font-size:80%"> <dtml-in getAcceptingEmails><dtml-var getEmailAddress><dtml-unless sequence-end>, </dtml-unless></dtml-in></em></p></td> </tr> <dtml-if sequence-end> <tr> <td colspan=3><input type="submit" name="manage_delPOP3Accounts:method" value="Delete selected"></td> </tr> </table> </form> </dtml-if> <dtml-else> <p><em>None</em></p> </dtml-in> </td> <td valign="top"> <form action="createPOP3Account"> <table> <tr> <td><p><strong>Hostname</strong></p></td> <td><input name="hostname"></td> </tr> <tr> <td><p><strong>Port</strong></p></td> <td><input name="portnr:int" size="3" value="110" /></td> </tr> <dtml-if "SupportPOP3SSL()"> <tr> <td><p><strong>SSL</strong></p></td> <td><input name="ssl" type="checkbox" value="1" /> </td> </tr> </dtml-if> <tr> <td><p><strong>Username</strong></p></td> <td><input name="username" /></td> </tr> <tr> <td><p><strong>Password</strong></p></td> <td><input name="password" type="password" /></td> </tr> <tr> <td><p><strong>Delete emails after</strong></p></td> <td><input name="delete_after" type="checkbox" value="1" checked="checked" /> </td> </tr> <tr> <td> </td> <td><input type="submit" value="Create POP3 Account"></td> </tr> </table> </form> </td> </tr> </table> <br>  <style> td.code { font-family: 'Courier New', Courier; font-size:80%; border-bottom:1px solid #CCCCCC; } span.code { font-family: 'Courier New', Courier; font-size:80%; } .expl { font-family: Verdana, Helvetica, sans-serif; font-size:70%; color:#333333; border-bottom:1px solid #CCCCCC; } </style> <a name="examplelist"></a> <p class="form-title">Examples of subject lines</p> <table cellpadding=3> <tr class="list-header"> <td><p><strong> Example</strong></p></td> <td><p><strong> Yield</strong></p></td> <td><p><strong> Condition</strong></p></td> </tr> <tr> <td valign="top" class="code">Office: There is a bug...</td> <td valign="top" class="expl">Issue title: There is a bug...<br> Sections: "Office"<br> Type: <whatever is default><br> Urgency: <whatever is default></td> <td valign="top" class="expl">Section called "Office" defined</td> </tr> <tr> <td valign="top" class="code">Office: There is a bug...</td> <td valign="top" class="expl">Issue title: Office: There is a bug...<br> Sections: <whatever is default><br> Type: <whatever is default><br> Urgency: <whatever is default></td> <td valign="top" class="expl">No section called "Office"</td> </tr> <tr> <td valign="top" class="code">Office, stationary: There is a bug...</td> <td valign="top" class="expl">Issue title: There is a bug...<br> Sections: [Office, Stationary]<br> Type: <whatever is default><br> Urgency: <whatever is default></td> <td valign="top" class="expl">Sections called "Office" and "Stationary" defined</td> </tr> <tr> <td valign="top" class="code">Office, stationary: There is a bug...</td> <td valign="top" class="expl">Issue title: stationary: There is a bug...<br> Sections: "Office"<br> Type: <whatever is default><br> Urgency: <whatever is default></td> <td valign="top" class="expl">Section called "Office" defined but not "Stationary"</td> </tr> <tr> <td valign="top" class="code">General: There is a bug...</td> <td valign="top" class="expl">Issue title: There is a bug...<br> Sections: "General"<br> Type: <whatever is default><br> Urgency: <whatever is default></td> <td valign="top" class="expl">Section called "General" and type called "general"</td> </tr> <tr> <td valign="top" class="code">Bug report, Office: There is a bug...</td> <td valign="top" class="expl">Issue title: There is a bug...<br> Sections: "Office"<br> Type: "bug report"<br> Urgency: <whatever is default></td> <td valign="top" class="expl">Section called "Office" and a type called "bug report"</td> </tr> <tr> <td valign="top" class="code">Bug Report, Office: There is a bug...</td> <td valign="top" class="expl">Issue title: There is a bug...<br> Sections: <whatever is default><br> Type: "bug report"<br> Urgency: <whatever is default></td> <td valign="top" class="expl">A type called "bug report" but no section called "Office"</td> </tr> <tr> <td valign="top" class="code">Urgent: There is a bug...</td> <td valign="top" class="expl">Issue title: There is a bug...<br> Sections: <whatever is default><br> Type: <whatever is default><br> Urgency: "urgent"</td> <td valign="top" class="expl">An urgency called "urgent"</td> </tr> <tr> <td valign="top" class="code">Bug report, Feature request: There is a bug...</td> <td valign="top" class="expl">Issue title: Feature request: There is a bug...<br> Sections: <whatever is default><br> Type: "bug report"<br> Urgency: <whatever is default></td> <td valign="top" class="expl">At least a type called "bug report" nomatter if a you have<br> type called "feature request"</td> </tr> <tr> <td valign="top" class="code">Feature request, Bug report: There is a bug...</td> <td valign="top" class="expl">Issue title: Bug report: There is a bug...<br> Sections: <whatever is default><br> Type: "feature request"<br> Urgency: <whatever is default></td> <td valign="top" class="expl">At least a type called "feature request" nomatter if a you have<br> type called "bug report"</td> </tr> </table> <p><strong>Note:</strong> <ul> <li><p>Case does <strong>not</strong> matter.</p></li> <li><p>A <code>:</code> sign (or <code>[square brackets]</code>) must be present for any interesting extraction</p></li> <li><p><span class="code">Office, Urgent: There is a bug...</span> and <span class="code">[Office, Urgent] There is a bug...</span> yields exactly the same result</p></li> <li><p>Data is extracted in the order: sections -> type -> urgency. So if there are words that belong to more than one, this order is respected.</p></li> </ul> <br>  <p class="form-title">Periodically checking for email</p> <a name="cronjob"></a> <p>The URL you must periodically invoke to check for new emails is:<br> <code><a href="<dtml-var getRootURL>/check4MailIssues" ><dtml-var getRootURL>/check4MailIssues</a></code> (or <a href="<dtml-var getRootURL>/check4MailIssues?verbose=1" >same but more verbose</a>) </p> <p>If you're uncertain about how to set up a cron job, then try <a href="http://www.issuetrackerproduct.com/Documentation/How-Tos/cronjob-email-check">this mini-howto</a> for how to setup crontab on Linux.<br /> Alternatively you can <a href="http://dev.legco.biz/products/ZopeScheduler">download and install ZopeScheduler</a> but for that to work you need to have a persistent ZODB object that the ZopeScheduler can trigger. To do that, simply <a href="manage_addProduct/PythonScripts/manage_addPythonScript?id=TriggerInbounceEmailCheck&submit=+Add+and+Edit+">create a Python Script</a> that can contain this code: <pre> context.check4MailIssues(verbose=True)</pre> </p> <br>  <dtml-var manage_page_footer> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/GlobalManagementForm.dtml��������������������������������������������������0000644�0001750�0001750�00000001430�11012074373�022520� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Management')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">IssueTracker Notifyable Container</p> <p>The notifyables you define here can be reached by all Issue Tracker instance in its acquisition path.</p> <p>Click <a href="<dtml-var URL2>/manage_findResult?searchtype=simple&obj_metatypes%3Alist=Issue+Tracker&obj_ids%3Atokens=&obj_searchterm=&obj_mspec=%3C&obj_mtime=&search_sub%3Aint=1&btn_submit=Find">here</a> to find which instances can reach this container.</p> <dtml-var NotifyableManagementPartForm> <dtml-var manage_page_footer> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/keyboardshortcuts.js.dtml��������������������������������������������������0000644�0001750�0001750�00000010501�11012074373�022670� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=24)"><dtml-call "RESPONSE.setHeader('Content-Type','application/x-javascript')"> var keyboard_shortcuts_enabled = true; function disableKS(e) { keyboard_shortcuts_enabled = false; } function enableKS(e) { keyboard_shortcuts_enabled = true; } var curr_ilink_idx=-1; var all_ilinks = new Array(); var prev_keycode; var num_keycode=""; //numeric keycode function body_onkeypress(evt){ if (!keyboard_shortcuts_enabled) return; function S(k) { return String.fromCharCode(k); } if (window.event) key=window.event.keyCode; else key=evt.which; var s = S(key).toLowerCase(); if (prev_keycode) prev_s = S(prev_keycode).toLowerCase(); else prev_s = ""; var burl='<dtml-var getRootRelativeURL>'; if (s != 'g' && prev_s=='g'){ if (s=='h') { location.href=burl+'/'; return false; } else if (s=='a') { //65 = a location.href=burl+'/AddIssue'; return false; } else if (s=='q') { // 81 = q location.href=burl+'/QuickAddIssue'; return false; } else if (s=='l') { // 76 = l location.href=burl+'/ListIssues'; return false; } else if (s == 'c') { // 67 = c location.href=burl+'/CompleteList'; return false; } else if (s == 'u') { // 85 = u location.href=burl+'/User'; return false; } else if (s == 'r') { location.href=burl+'/Reports'; return false; <dtml-if "hasManagerRole() or not PrivateStatistics()"> } else if (s == 's') { location.href=burl+'/Statistics'; return false; </dtml-if> } else if (s == 'd') { // Compare! if (!$('#issuedata').size()) { alert("Go to the issue first to compare"); return false; } var issue = prompt("Compare issues - Enter issue number:",""); if (issue !== null) { var issueid = __getIssueID(issue); if (issueid) location.href=location.href.split('?')[0]+'?compareTo='+issueid; else alert("Invalid Issue ID"); } return false; } } else if (s == '#') { // 51 = # var issue = prompt("Enter issue number or title:",""); if (issue !== null) { var issueid = __getIssueID(issue); if (issueid) { location.href=burl+'/'+issueid; return false; } else { location.href='<dtml-var "getRootURL()">/<dtml-var "whichList()">/?search_only_on=title&q='+escape(issue); } } } else if (s=='n') { if (all_ilinks.length>1) { if (curr_ilink_idx==-1) curr_ilink_idx=0; else { unhighlightILink(all_ilinks[curr_ilink_idx]); curr_ilink_idx = (curr_ilink_idx+1) % all_ilinks.length; } highlightILink(all_ilinks[curr_ilink_idx]); } } else if (s=='p') { if (all_ilinks.length>1) { if (curr_ilink_idx==-1) curr_ilink_idx=all_ilinks.length-1; else { unhighlightILink(all_ilinks[curr_ilink_idx]); curr_ilink_idx = (curr_ilink_idx-1) % all_ilinks.length; } highlightILink(all_ilinks[curr_ilink_idx]); } } else if (s=='s' || s=='/') { window.scrollTo(0,0); disableKS(); $('#q').focus(); $('#q').select(); } prev_keycode=key; return true; } function __getIssueID(issue) { var issueid = issue.replace(/^\s+|\s+$/g,''); <dtml-if "issueprefix"> var ok_id = new RegExp(/^\d+$|/^<dtml-var issueprefix>\d+$/); <dtml-else> var ok_id = new RegExp(/^\d+$|^#\d+$/); </dtml-if> if (issueid) { if (ok_id.exec(issueid)) { if (issueid.length!=<dtml-var "randomid_length">) while(issueid.length < <dtml-var "randomid_length">) issueid = "0"+issueid; if (issueid[0]=='#') issueid = issueid.substring(1, issueid.length); } return issueid; } return null; } $(document).bind("keypress", body_onkeypress); function toggleInputsKS() { $("input,textarea,select").bind("blur", enableKS).bind("focus", disableKS); } function findILinks() { var c=0; $('a.ilink').each(function() { all_ilinks.push(this); }); } function highlightILink(el){ el.innerHTML = '>' + el.innerHTML; el.focus(); } function unhighlightILink(el){ var s= el.innerHTML; el.innerHTML = el.innerHTML.substring(4); } $(function() { toggleInputsKS(); findILinks(); }); �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/passwordReminder.dtml������������������������������������������������������0000644�0001750�0001750�00000002415�11012074373�022033� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-var "manage_form_title(this(), _, form_title='Send password reminder', )"> <form action="./" method="post"> <input type="hidden" name="name" value="<dtml-var expr="user.name" html_quote>" /> <table> <tr> <td> <div class="form-label"> To </div> </td> <td> <code><dtml-if "user.fullname"><dtml-var "user.fullname"> <<dtml-var "user.email">><dtml-else><dtml-var "user.email"></dtml-if></code> </td> </tr> <tr> <td> <div class="form-label"> From </div> </td> <td> <input name="email_from" size="30" value="<dtml-var from_field>" /> </td> </tr> <tr> <td> <div class="form-label"> Subject </div> </td> <td> <input name="email_subject" size="30" value="<dtml-var subject html_quote>" /> </td> </tr> <tr> <td colspan="2"> <textarea name="remindertext" cols="80" rows="12"><dtml-var message html_quote></textarea> </td> </tr> <tr> </tr> </table> <input type="submit" name="manage_sendReminder:method" value="Send reminder" onclick="this.value='Please wait...'" /> <input type="submit" name="manage_main:method" value="Cancel" /> </form> <dtml-var manage_page_footer> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/PropertiesStatusScores.dtml������������������������������������������������0000644�0001750�0001750�00000006032�11012074373�023221� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <div style="float:right;font-size:10px; padding:6px"> <a href="manage_editIssueTrackerPropertiesForm" style="text-decoration:underline"><<< Return to all properties</a> </div> <h2>Status scores</h2> <p>The status scores is a way to put a value on each status. This makes it possible to tell how valuable it is to have an issue in a particular status, where the lowest value is given to a status where the issue is not near completion an the higest value when the issue is in its ideal state.</p> <form action="manage_saveStatusScores" method="post"> <dtml-let values="getStatusScoreValues(return_incomplete=1)"> <dtml-comment> values here is a dict where for each status it gives a percentage (int [0-100]) or None </dtml-comment> <table border="0"> <dtml-in "getStatuses()"> <dtml-let preval="values.get(_['sequence-item'])"> <tr> <td> <p><b><dtml-var sequence-item></b></p> <input type="hidden" name="used_statuses:list" value="<dtml-var sequence-item>" /> </td> <td><select name="values:list"> <option value="" <dtml-if "preval==_.None">selected="selected"</dtml-if> >n/a</option> <dtml-in "_.range(0, 101)"> <option value="<dtml-var sequence-item>" <dtml-if "preval==_['sequence-item']">selected="selected"</dtml-if> ><dtml-var sequence-item></option> </dtml-in> </select>% </td> <td><p> <dtml-if "preval==_.None"> <!-- some default values --> <dtml-if "_['sequence-item']=='open'"> suggestion: 0% <dtml-elif "_['sequence-item']=='taken'"> suggestion: 25% <dtml-elif "_['sequence-item']=='on hold'"> suggestion: 50% <dtml-elif "_['sequence-item']=='rejected'"> suggestion: 95% <dtml-elif "_['sequence-item']=='completed'"> suggestion: 100% </dtml-if> <dtml-else>  </dtml-if> </p> </td> </tr> </dtml-let> </dtml-in> </table> <input type="submit" value="Save" /> </dtml-let> </form> <p>The status scores are used when calculating the progress of a whole issuetracker. If most issues are in a status with a high status score, it means that the whole issuetracker is making good progress. This calculation is not very scientific because the dependencies on people or software might be a cause of a low total score which might give a false image of lack of progress. <br /> This calculation will be shown on the <a href="Statistics">More statistics</a> page.</p> <dtml-let status_values="getStatusScoreValues()"> <dtml-if "hasStatusValues(status_values)"> <p> <dtml-let calculated_average="calculateStatusScoreProgress(status_values)"> Current overall status progress: <b><dtml-var "'%.1f'%calculated_average">%</b> </dtml-let> </p> </dtml-if> </dtml-let> <dtml-var manage_page_footer> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/QuickAddIssueJavascript.dtml�����������������������������������������������0000644�0001750�0001750�00000001431�11012074373�023225� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=48)"><dtml-call "RESPONSE.setHeader('Content-type','application/x-javascript')"> // Useful functions for Quick Add Issue <dtml-if "SaveDrafts() and UseAutoSave()"> function autosave() { $.post('AutoSaveDraftIssue', $(document.ai).fastSerialize(), function(resp) { if (resp) { $('input[name="draft_issue_id"]').val(resp); } }); } var as_timer; stopautosave=function() { if (as_timer) clearTimeout(as_timer); }; startautosave = function() { autosave(); as_timer=window.setTimeout("startautosave()", <dtml-var getAutosaveInterval>*1000); }; </dtml-if> function softsubmit() { if (document.ai) { document.ai.action='<dtml-var getRootURL>/AddIssue'; document.ai.submit(); } }���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/print.css.dtml�������������������������������������������������������������0000644�0001750�0001750�00000011452�11012074373�020427� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=30)"><dtml-call "RESPONSE.setHeader('Content-type','text/css')"> html, body { margin:0; padding:0; background: white; color: black; font-family:Arial, sans-serif, verdana; } p,td,th,div,span { font-size: x-small; voice-family: "\"}\""; voice-family: inherit; font-size: small } .status { color:red; } span.id { color:#333; } h3 span.id { color:#333; font-size:11pt; } h1.head { font:bold 18pt Verdana; } h3.preview { font-style:italic; } .bigger { font-size:120%; } .hidden { display:none; } .smaller, .s { font-size:85%; } .floatL { float:left; } .floatR { float:right; } div.errormessage { border:1px solid #F99; background-color:#FCC; padding-left:10px; padding-right:10px; } .submiterror { color:red; font-weight:bold; } .aeh a { text-decoration:none; } .bumf { width:500px; padding-left:20px; } /* Header --------------------------- */ #head { background-color:#CC9; margin-bottom:10px; } div#logo { padding-right:10px; float:left; } h1.head { margin-left:10px; float:left; } h2.head { float:left; margin-left:30px; } .clearer { clear: left; line-height: 0; height: 0; } .clearerR { clear: right; line-height: 0; height: 0; } #main { padding:7px; padding-bottom:2px; } /* List Issues --------------------------- */ .ftitle { text-align:center; font-weight:bold; padding-bottom:3px; } .bd { /* brief description */ color:#828282; } .ilink { font-weight:bold; font-style:italic; } tr.lhead { background-color:#C3C3C3; font-weight:bold; } tr.l_commenthead { border-top:1px solid #666; background-color:#C3C3C3; font-weight:bold; } .leven { background-color:#DFDFDF; } .lodd { background-color:#F1F1F1; } /* Search result ----------------------------------------- */ .q_highlight { font-weight: bold; background-color: #FFFF66; font-style: italic; } /* Forms --------------------------------- */ .submitbutton { font-weight:bold; } .smallbutton { display:none; } /* Show Issue ----------------------- */ #issuedrafts { float:right; width:340px; border:1px solid #666; background-color:#F6F6F6; padding:2px 9px; } #issuedrafts p { margin-top:4px;margin-bottom:4px; } .assignment form { display:none; } /* Misc. -------------------------- */ /* Urgencies */ .ur-0 {color:#666; } .ur-1 { } /* default 'normal' */ .ur-2 {font-weight:bold; } .ur-3 {font-weight:bold; color:red; } .ur-4 {font-weight:bolder; color:red; } div.note { font-size:80%; color:#333; } .extendedoptions { padding:0 4px 4px 4px; float:left; } .specialoptions { display:none; } .formfollowup { display:none; } #recent { display:none; } #topright { display:none; } #foot { clear: both; display:none; } /* For the tabs ------------------------ */ div#tabs { display:none; } /* Option buttons ---------------------------- */ ul#navlist { display:none; } /* Show Issue ----------------------- */ .issue { /* border:1px solid #999; */ /* background-color:#eee; */ padding:0; /* width:690px; */ margin-bottom:1.9em; } .ihead { margin:0; } .ihead { /* background:#ccc; */ /* height:30px; */ } .ihead h3 { font-family:arial,sans-serif; font-size:14px; padding:6px 10px 0 15px; margin:0; } .istatus { padding:8px 12px; float: right; font-family:verdana; font-weight:bold; color:red; /* background-color: #eee; */ } .ibody { font-size: 10pt; font-family:verdana; padding:6px 24px; } .sig, .sig a { /* signature in text */ color:#666; } .ilabel { font-weight:bold; padding-right:8px; vertical-align:top; } .shade { color:#666; } .announcement { border:1px solid black; background-color:#efefef; margin-bottom:15px; } img.fileatt { vertical-align:middle; padding:3px; } a.assignment { font-size:0.8em; } .topbar { display:none; } table.lhead { display:none; } #filteroptions { display:none; } .idetails-table, .idetails2 { margin-left:15px; margin-bottom:10px; } div.assignment { margin-left:15px; } .threadbox { margin-left:20px; margin-bottom:13px; } .thead { padding:5px 10px; } .tbody { font-size: 10pt; font-family:verdana,helvetica, arial, sans-serif; padding:2px 20px; } .threaddate { margin-left:20px; } .threadfiles { padding:0 4px; } .cth { /* ColorThreadChange */ color:#c00; } .idetails2 { float:left; } .plink, a.highlight-toggle, a.backlink { display:none; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/PropertiesWizard.dtml������������������������������������������������������0000644�0001750�0001750�00000076343�11012074373�022033� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">Properties Wizard</p> <style type="text/css"> div.errormessage { border:1px solid #FF9999; background-color:#FFCCCC; padding-left:10px; padding-right:10px; } .submiterror { font-family:Arial, Verdana, sans-serif; color:red; font-weight:bold; } #backlinks { padding-left:13px; float:right; width:40%; border:1px solid #666; margin:15px; } #backlinks a { text-decoration:underline; font-weight:bold; } li { font-family: Verdana, Helvetica, sans-serif; font-size: 10pt; color: #333333; } p.smaller { font-size:0.7em; } /* Urgencies */ .ur-0 {color:#666; } .ur-1 { } /* default 'normal' */ .ur-2 {font-weight:bold; } .ur-3 {font-weight:bold; color:red; } .ur-4 {font-weight:bolder; color:red; } label.fmt { font-size:0.8em; font-family: Verdana, Helvetica, sans-serif; } </style> <dtml-if error> <dtml-var show_submissionerror_message> <dtml-else> <dtml-call "REQUEST.set('error',{})"> </dtml-if> <form action="manage_PropertiesWizard" method="post" name="form1"> <input type="hidden" name="stage:int" value="<dtml-var stage>" / <dtml-if firsttime> <input type="hidden" name="firsttime:int" value="<dtml-var firsttime>" /> </dtml-if> <p>Stage: <dtml-var stage> of 12</p> <dtml-if "stage==3"> <div id="backlinks"> <p>You can...<br /> <a href="manage_PropertiesWizard?submit:int=0&stage:int=1&firsttime:int=<dtml-if firsttime><dtml-var firsttime><dtml-else>0</dtml-if>">go back and add more sections?</a> </div> <dtml-elif "stage==5"> <div id="backlinks"> <p>You can...<br /> <a href="manage_PropertiesWizard?submit:int=0&stage:int=4&firsttime:int=<dtml-if firsttime><dtml-var firsttime><dtml-else>0</dtml-if>" >go back and change the types and urgencies?</a> </div> <dtml-elif "stage==9"> <div id="backlinks"> <p>You can...<br /> <a href="manage_PropertiesWizard?submit:int=0&stage:int=8&firsttime:int=<dtml-if firsttime><dtml-var firsttime><dtml-else>0</dtml-if>" >go back and change who's running this issue tracker?</a> </div> </dtml-if> <!-- ----------------------------------------- --> <dtml-if "stage==1"> <p>How do you intend to use this IssueTracker? Choose which one you think is most appropriate. <br /><br /> <input type="radio" name="whatuse" value="softwaredevelopment" checked="checked" id="wu1"/> <label for="wu1">Software/Application development (default)</label <br /> <input type="radio" name="whatuse" value="helpdesk_external" id="wu2" /> <label for="wu2">Helpdesk (open/external)</label> <br /> <input type="radio" name="whatuse" value="helpdesk_internal" id="wu3" /> <label for="wu3">Helpdesk (closed/internal)</label> <br /> <dtml-elif "stage==2"> <p>Now enter the various sections that issues should be able to belong to. <br /> It is recommended that you enter a few more than the default suggestions but these can be changed later. <br /><br /> <dtml-var "ShowError(error, 'sections_options')"> <textarea name="sections_options:lines" rows="9" cols="35"><dtml-var "_.string.join(sections_options,'\n')" html_quote></textarea> <br /> <dtml-elif "stage==3"> <p>Select the default section issues should belong to if someone who adds an issue do <em>not</em> specify which section it belongs to:<br /> <br /> <dtml-let this_sections_options="REQUEST.get('sections_options',sections_options)"> <select name="defaultsections:list" multiple size="<dtml-var "_.min(7, _.len(this_sections_options))">"> <dtml-in this_sections_options> <option <dtml-if "_['sequence-item'] in defaultsections">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </dtml-let> <dtml-var "ShowError(error, 'defaultsections')"> <p class="smaller">Ideally you select one but you can select multiple default sections by holding down the <code>Ctrl</code> key as you select sections. <dtml-elif "stage==4"> <p>Enter the kind of <b>types</b> and <b>urgencies</b> that issues should have.</p> <table cellpadding=5> <tr> <th>Types</th> <th>Urgencies</th> </tr> <tr> <td> <textarea name="types:lines" rows="6" cols="35" ><dtml-var "_.string.join(types, '\n')" html_quote></textarea> <dtml-var "ShowError(error, 'types')"> </td> <td> <textarea name="urgencies:lines" rows="6" cols="35" ><dtml-var "_.string.join(urgencies, '\n')" html_quote></textarea> <dtml-var "ShowError(error, 'urgencies')"> </td> </tr> </table> <p class="smaller">Enter one item per line. All excessive whitespace will be removed.</p> <dtml-elif "stage==5"> <p>If someone does not enter a <b>type</b> or an <b>urgency</b> what should be set by default.</p> <table cellpadding=5 border=0> <tr> <th align="left">Default type</th> <th><dtml-var "' '*12"></th> <th align="left">Default urgency</th> </tr> <tr> <td valign="top"><p> <dtml-let this_types=types> <dtml-in this_types> <input type="radio" id="type<dtml-var sequence-index>" value="<dtml-var sequence-item>" name="default_type" <dtml-if "_['sequence-item']==default_type">checked="checked"</dtml-if> <label for="type<dtml-var sequence-index>" class="<dtml-var "getUrgencyCSSSelector(_['sequence-item'])">"><dtml-var sequence-item html_quote></label> <br /> </dtml-in> </dtml-let> </td> <td> </td> <td valign="top"><p> <dtml-let this_urgencies=urgencies> <dtml-in this_urgencies> <input type="radio" id="urgency<dtml-var sequence-index>" value="<dtml-var sequence-item>" name="default_urgency" <dtml-if "_['sequence-item']==default_urgency">checked="checked"</dtml-if>> <label for="urgency<dtml-var sequence-index>" class="<dtml-var "getUrgencyCSSSelector(_['sequence-item'])">"><dtml-var sequence-item html_quote></label> <br /> </dtml-in> </dtml-let> </td> </tr> </table> <dtml-elif "stage==6"> <p>How should issues be sorted if not sorted by anything explicitly? By <b>creation date</b> or by <b>modification date</b>?</p> <p><input type="radio" name="default_sortorder" value="issuedate" <dtml-if "getDefaultSortorder()=='issuedate'">checked="checked"</dtml-if> /> (Recommended) When sorted by <b>creation date</b> then posting of followups does not affect the default sort order in <a href="ListIssues">List Issues</a> (or <a href="CompleteList">Complete List</a>). This is similar to how <a href="http://gmail.google.com">Gmail</a> sorts the inbox.</p> <p><input type="radio" name="default_sortorder" value="modifydate" <dtml-if "getDefaultSortorder()=='modifydate'">checked="checked"</dtml-if> /> (Original) When sorted by <b>modification date</b> then old issues can appear at the top of the <a href="ListIssues">List Issues</a> (or <a href="CompleteList">Complete list</a>) even though added a long time ago. The modification date of the issue is the same as the creation date of the latest followup.</p> <dtml-elif "stage==7"> <p>Are there people in your organization who might use these issue trackers a lot?<br /> You can set up so called <b>notifyables</b> which simply maps a name to an email address and a group. Once these are set up you will be able to enter these people's names instead of having to remember their email address for settings and for the Tell-a-friend feature.</p> <p>You can either define notifyables globally or locally. When defined globally (recommended) it will work for all inherent issue tracker instances. You can also define them locally which is useful if you want to contain everything in one single place.</p> <p>(<b>Tip!</b> open these links in a new window and return here after)<br /><br /> <dtml-if hasGlobalContainer> You already have a global container for notifyables. <a href="<dtml-var "getGlobalContainer().absolute_url()">/manage_GlobalManagementForm">Manage it here</a>. <dtml-else> Set up a <em>global</em> notifyables container in...<br /> <dtml-in "_.string.split(_.string.replace(REQUEST.URL1, REQUEST.BASE0, ''), '/')"> <dtml-if sequence-start> <ul> <li><a href="<dtml-var "REQUEST.BASE0">/manage_addProduct/IssueTrackerProduct/addNotifyableContainerForm?goto_after=1"><em>Root</em></a></li> <dtml-call "REQUEST.set('bits',REQUEST.BASE0)"> <dtml-elif sequence-end> </ul> <dtml-else> <dtml-call "REQUEST.set('bits', REQUEST.get('bits')+'/'+_['sequence-item'])"> <li><a href="<dtml-var "REQUEST.get('bits')">/manage_addProduct/IssueTrackerProduct/addNotifyableContainerForm?goto_after=1" ><dtml-var "_.string.replace(REQUEST.get('bits'),REQUEST.BASE0, '')"></a></li> </dtml-if> </dtml-in> </dtml-if> <p> <dtml-if "getNotifyables(only='local')"> You already have a few <em>local</em> notifyables set up here in this issue tracker.<br /> <a href="manage_ManagementNotifyables">Manage them here.</a> <dtml-else> To set up <em>local</em> notifyables, <a href="manage_ManagementNotifyables">follow this link</a>. </dtml-if> <dtml-elif "stage==8"> <script> function appendWord(word) { var t = document.form1['always_notify:lines'].value; if (t!="") { t += "\n"; } var nt = t+word; document.form1['always_notify:lines'].value = nt; } </script> <p>Are there people who should get an <b>email notification</b> on all added issues?<br /> Write the email addresses line by line or if you have defined <b>notifyables</b> you can just enter their name. </p> <table> <tr> <td valign="top"> <textarea name="always_notify:lines" rows="6" cols="35"><dtml-var "_.string.join(getAlwaysNotify(), '\n')" html_quote></textarea><br /> <dtml-var "ShowError(error, 'always_notify')"> </td> <td valign="top"> <dtml-in getNotifyables> <dtml-if sequence-start> <p class="smaller">Remember that you already have the following notifyables set up:</p> <p> </dtml-if> <a href="#" title="Click to add to the list on the left" onclick="javascript:this.style['display']='none';appendWord('<dtml-var "_.string.replace(getTitle(),'\'','\\\'')">');"><dtml-var getTitle> <dtml-in "getGroupsByIds(groups)"> <dtml-if sequence-start> (</dtml-if><dtml-var title html_quote><dtml-unless sequence-end>, </dtml-unless><dtml-if sequence-end>)</dtml-if> </dtml-in></a><br> <dtml-else> <p class="smaller">It seems that you have not set up any <b>notifyables</b> here. It is generally a useful thing to have because you can partly remember only the name and also if a notifyable person changes email address the name will still work as before.<br /> Do this, <a href="">define local notifyables</a> (names will only be defined then for this issuetracker) or <a href="">create global notifyables container</a> in which you can </dtml-in> </td> </tr> </table> <dtml-elif "stage==9"> <p>Who's running this issue tracker?</p> <p class="smaller">The <b>name</b> is shown in email send-outs in the signature and the <b>email</b> is what the email comes from.</p> <table> <tr> <td><p><b>Name:</b></td> <td><input type="text" name="sitemaster_name:string" size="35" value="<dtml-var "REQUEST.get('sitemaster_name',sitemaster_name)" html_quote>"> <dtml-var "ShowError(error, 'sitemaster_name')"> </td> </tr> <tr> <td valign="top"><p><b>Email:</b></td> <td><input type="text" name="sitemaster_email:string" size="35" value="<dtml-var "REQUEST.get('sitemaster_email',sitemaster_email)" html_quote>"> <dtml-var "ShowError(error, 'sitemaster_email')"> <dtml-if "not ValidEmailAddress(REQUEST.get('sitemaster_email',sitemaster_email))"> <span style="color:red;font-family:Arial,Verdana;font-size:0.7em">The one you have now is invalid and this can cause problems with send-outs which are not followups.</span> </dtml-if> </td> </tr> </table> <dtml-elif "stage==10"> <p>Some minor details about the <b>display</b> and <b>submission</b> of issues.</p> <table> <tr> <td colspan="2"><p><b>File attachments</b></td> </tr> <tr> <td><p>Adding issues:</td> <td><input name="no_fileattachments" maxlength="2" size="2" value="<dtml-var "REQUEST.get('no_fileattachments', getNoFileattachments())">" /> <dtml-var "ShowError(error, 'no_fileattachments')"> </td> </tr> <tr> <td><p>Posting followups:</td> <td><input name="no_followup_fileattachments" maxlength="2" size="2" value="<dtml-var "REQUEST.get('no_followup_fileattachments', getNoFollowupFileattachments())">" /> <dtml-var "ShowError(error, 'no_followup_fileattachments')"> </td> </tr> <tr> <td colspan="2"> <p>Setting these to 0 switches the feature off. </td> </tr> </table> <br /> <p><b>Date format:</b></p> <select name="display_date"> <dtml-let dd="REQUEST.get('display_date',display_date)"> <dtml-in getDisplayDateFormatOptions> <option value="<dtml-var sequence-item>" <dtml-if "dd==_['sequence-item']">selected="selected"</dtml-if> ><dtml-var "ZopeTime().strftime(_['sequence-item'])"></option> </dtml-in> </dtml-let> </select> <p><b>Use "clever" date display:</b></p> <p> <dtml-let show="ShowDatesCleverly()"> <input type="radio" name="show_dates_cleverly:int" value="1" <dtml-if show>checked="checked"</dtml-if> id="sdc1" /> <label for="sdc1">Yes</label>   <input type="radio" name="show_dates_cleverly:int" value="0" <dtml-unless show>checked="checked"</dtml-unless> id="sdc0" /> <label for="sdc0">No</label> </dtml-let> <dtml-elif "stage==11"> <p>And now for some simple Yes/No questions.</p> <table cellpadding="5"> <tr> <td><p>Should Managers be able to change attributes of an issue once submitted?</td> <td><p> <dtml-let allow="REQUEST.get('allow_issueattrchange', allow_issueattrchange)"> <input type="radio" name="allow_issueattrchange:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allow1" /> <label for="allow1">Yes</label>   <input type="radio" name="allow_issueattrchange:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allow0" /> <label for="allow0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Should people be able use the Tell a friend widget?</td> <td><p> <dtml-let use="REQUEST.get('use_tellafriend', UseTellAFriend())"> <input type="radio" name="use_tellafriend:int" value="1" <dtml-if use>checked="checked"</dtml-if> id="uses1" /> <label for="uses1">Yes</label>   <input type="radio" name="use_tellafriend:int" value="0" <dtml-unless use>checked="checked"</dtml-unless> id="uses0" /> <label for="uses0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Should people be able to subscribe to issues?</td> <td><p> <dtml-let allow="REQUEST.get('allow_subscription', allow_subscription)"> <input type="radio" name="allow_subscription:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allows1" /> <label for="allows1">Yes</label>   <input type="radio" name="allow_subscription:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allows0" /> <label for="allows0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Should only the Manager be able to see the Statistics page?</td> <td><p> <dtml-let allow="REQUEST.get('private_statistics', private_statistics)"> <input type="radio" name="private_statistics:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowp1" /> <label for="allowp1">Yes</label>   <input type="radio" name="private_statistics:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowp0" /> <label for="allowp0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Should email address be encoded to prevent spam-bots?</td> <td><p> <dtml-let allow="REQUEST.get('encode_emaildisplay', encode_emaildisplay)"> <input type="radio" name="encode_emaildisplay:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowe1" /> <label for="allowe1">Yes</label>   <input type="radio" name="encode_emaildisplay:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowe0" /> <label for="allowe0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Show (if applicable) what people were notified when submitting an issue?</td> <td><p> <dtml-let allow="REQUEST.get('show_always_notify_status', show_always_notify_status)"> <input type="radio" name="show_always_notify_status:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allown1" /> <label for="allown1">Yes</label>   <input type="radio" name="show_always_notify_status:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allown0" /> <label for="allown0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Show "Confidential issue" option?</td> <td><p> <dtml-let allow="REQUEST.get('show_confidential_option', show_confidential_option)"> <input type="radio" name="show_confidential_option:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowc1" /> <label for="allowc1">Yes</label>   <input type="radio" name="show_confidential_option:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowc0" /> <label for="allowc0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Show "Hide me" option?</td> <td><p> <dtml-let allow="REQUEST.get('show_hideme_option', show_hideme_option)"> <input type="radio" name="show_hideme_option:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowh1" /> <label for="allowh1">Yes</label>   <input type="radio" name="show_hideme_option:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowh0" /> <label for="allowh0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Show "URL" option?</td> <td><p> <dtml-let allow="REQUEST.get('show_issueurl_option', ShowIssueURLOption())"> <input type="radio" name="show_issueurl_option:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowu1" /> <label for="allowu1">Yes</label>   <input type="radio" name="show_issueurl_option:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowu0" /> <label for="allowu0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Allow adding new sections</td> <td><p> <dtml-let allow="REQUEST.get('can_add_new_sections', can_add_new_sections)"> <input type="radio" name="can_add_new_sections:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowns1" /> <label for="allowns1">Yes</label>   <input type="radio" name="can_add_new_sections:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowns0" /> <label for="allowns0">No</label> </dtml-let> </td> </tr> <tr> <td><p>Use images in the menu</td> <td><p> <dtml-let allow="REQUEST.get('images_in_menu', images_in_menu)"> <input type="radio" name="images_in_menu:int" value="1" <dtml-if allow>checked="checked"</dtml-if> id="allowim1" /> <label for="allowim1">Yes</label>   <input type="radio" name="images_in_menu:int" value="0" <dtml-unless allow>checked="checked"</dtml-unless> id="allowim0" /> <label for="allowim0">No</label> </dtml-let> </td> </tr> </table> <dtml-elif "stage==12"> <p><b>Finished!</b> <p>No, not quite actually. If you want to use <b>Issue Assignment</b> so issues can be assigned to Issue Tracker Users you will have to use the <a href="manage_ManagementUsers"><b>User Management</b></a> management page. <p>Other than that, you can now <a href="index_html">start using this issue tracker</a> or review the <a href="manage_editIssueTrackerPropertiesForm">Properties tab</a> further. </dtml-if> <dtml-if "stage < 12"> <br /><br /> <input type="submit" value=" Save and Continue " onClick="javascript:this.value='Saving properties...'" /> </dtml-if> </form> <dtml-comment> <dtml-unless "REQUEST.has_key('goto')"> <dtml-call "REQUEST.set('goto',1)"> </dtml-unless> <table style="border:1px solid black;" cellpadding=5><tr><td><p> <dtml-if "goto==9"> Complete <dtml-else> Step <dtml-var goto> out of 8 </dtml-if> </p></td></tr></table> <dtml-if "goto==10"> <dtml-call "PropertiesWizardRemember(default_batch_size=REQUEST.get('default_batch_size'), no_fileattachments=REQUEST.get('no_fileattachments'), allow_issueattrchange=REQUEST.get('allow_issueattrchange'))"> <dtml-call "MoveWizardSession2REQUEST()"> <dtml-call "manage_editIssueTrackerProperties(carefulbooleans=1)"> <p>Great! All properies set.</p> <p>To set more properties or change anything, visit the <a style="text-decoration:underline;" href="manage_editIssueTrackerPropertiesForm?r=<dtml-var "_.str(_.int(ZopeTime()))[4:-1]">">Properties again</a>. <br>Or<br> <a style="text-decoration:underline;" href="manage_main">Contents</a> <br>Or<br> <a style="text-decoration:underline;" href="<dtml-var "getRootURL()">">Start using <dtml-var title></a> <a style="text-decoration:underline;" href="<dtml-var "getRootURL()">" target="_blank">(in a new window)</a> <dtml-elif "goto==9"> <dtml-call "PropertiesWizardRemember(sitemaster_name=REQUEST.get('sitemaster_name'), sitemaster_email=REQUEST.get('sitemaster_email'), display_date=REQUEST.get('display_date'))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="9"> <p>Lastly, some simple integer properties:</p> <p>How many issues do you want to display page batched page in List Issues?:<br> <input type="text" name="default_batch_size:int" size="5" value="<dtml-var default_batch_size>"> </p> <p>How many files can be uploaded when you add an issue?:<br> <input type="text" name="no_fileattachments:int" size="5" value="<dtml-var getNoFileattachments>"> ...and for followups <input type="text" name="no_followup_fileattachments:int" size="5" value="<dtml-var getNoFollowupFileattachments>"> </p> <p>Do you want that Managers can change the attributes of an issue after it has been submitted:<br> <input type="checkbox" name="allow_issueattrchange:boolean" <dtml-if "AllowIssueAttributeChange()">CHECKED</dtml-if>> </p> <br><input type="submit" value=" Continue >> "> </form> <dtml-elif "goto==7"> <dtml-call "PropertiesWizardRemember(odd_bgcolor=REQUEST.get('odd_bgcolor'), even_bgcolor=REQUEST.get('even_bgcolor'), issueprefix=REQUEST.get('issueprefix'))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="8"> <p>Sitemaster name:<br> <input type="text" name="sitemaster_name:string" size="35" value="<dtml-var sitemaster_name html_quote>"> </p> <p>Sitemaster email:<br> <input type="text" name="sitemaster_email:string" size="35" value="<dtml-var sitemaster_email html_quote>"> <dtml-if "not ValidEmailAddress(sitemaster_email)"> <span style="color:red;">The one you have now is invalid and this can cause problems with notifcations when people don't want to reveal their own email addresses.</span> </dtml-if> </p> <p>Display dateformat:<br> <select name="display_date:string"> <dtml-in getDisplayDateFormatOptions> <option value="<dtml-var sequence-item>" <dtml-if "display_date==_['sequence-item']">SELECTED</dtml-if> ><dtml-var "ZopeTime('12/13/%s 22:15'%ZopeTime().strftime('%Y')).strftime(_['sequence-item'])"></option> </dtml-in> </select> </p> <br><input type="submit" value=" Continue >> "> </form> <dtml-elif "goto==6"> <dtml-call "PropertiesWizardRemember(always_notify=REQUEST.get('always_notify'), allow_subscription=REQUEST.get('allow_subscription',0), private_statistics=REQUEST.get('private_statistics',0))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="7"> <p>Now, let's set some simple string properties:</p> <p>Which HTML colour do you want to use for Odd (1,3,5...) rows in List Issues?<br> <input type="text" name="odd_bgcolor:string" size="20" value="<dtml-var odd_bgcolor html_quote>"> <table bgcolor="<dtml-var odd_bgcolor html_quote>"><tr><td><dtml-var "' '*10"></td></tr></table></p> <p>Which HTML colour do you want to use for Even (2,4,6...) rows in List Issues?<br> <input type="text" name="even_bgcolor:string" size="20" value="<dtml-var even_bgcolor html_quote>"> <table bgcolor="<dtml-var even_bgcolor html_quote>"><tr><td><dtml-var "' '*10"></td></tr></table></p> <p>Do you want to stick a prefix to the id of issue objects, if not leave empty?<br> E.g. <code>/mytracker/admin-0007</code> instead of just <code>/mytracker/0007</code><br> <input type="text" name="issueprefix:string" size="35" value="<dtml-var issueprefix html_quote>"> </p> <br><input type="submit" value=" Continue >> "> </form> <dtml-elif "goto==5"> <dtml-call "PropertiesWizardRemember(default_type=REQUEST.get('default_type'), default_urgency=REQUEST.get('default_urgency'))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="6"> <p>Are there people who should get an email notification on all added issues?</p> <table> <tr><td valign="top"> <textarea name="always_notify:lines" rows="6" cols="35"><dtml-var "_.string.join(getAlwaysNotify(), '\n')" html_quote></textarea> </td><td valign="top"> <dtml-in getNotifyables> <dtml-if sequence-start> <p>Remember that you already have the following notifyables set up:</p> </dtml-if> <dtml-if alias><dtml-var alias><dtml-else><dtml-var email></dtml-if> <dtml-in "getGroupsByIds(groups)"> <dtml-if sequence-start> (</dtml-if><dtml-var title html_quote><dtml-unless sequence-end>, </dtml-unless><dtml-if sequence-end>)</dtml-if> </dtml-in> <br> </dtml-in> </td> </tr> </table> <p>Should people be able to subscribe to issues?<br> <input type="checkbox" name="allow_subscription:boolean" <dtml-if "AllowIssueSubscription()">CHECKED</dtml-if>> </p> <p>Should only the Manager be able to see the Statistics page?<br> <input type="checkbox" name="private_statistics:boolean" <dtml-if "PrivateStatistics()">CHECKED</dtml-if>> </p> <br><input type="submit" value=" Continue >> "> </form> <dtml-elif "goto==4"> <dtml-call "PropertiesWizardRemember(types=REQUEST.get('types'), urgencies=REQUEST.get('urgencies'))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="5"> <p>Now, select which ones of these are default ones:</p> <table cellpadding=5> <tr> <th>Default type</th> <th>Default urgency</th> </tr> <tr> <td valign="top"> <dtml-let this_types="REQUEST.get('types', types)"> <select name="default_type:string" size="<dtml-var "_.len(this_types)">"> <dtml-in this_types> <option <dtml-if "_['sequence-item']==default_type">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </dtml-let> </td> <td valign="top"> <dtml-let this_urgencies="REQUEST.get('urgencies', urgencies)"> <select name="default_urgency:string" size="<dtml-var "_.len(this_urgencies)">"> <dtml-in this_urgencies> <option <dtml-if "_['sequence-item']==default_urgency">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </dtml-let> </td> </tr> </table> <br><input type="submit" value=" Continue >> "> </form> <dtml-elif "goto==3"> <dtml-call "PropertiesWizardRemember(defaultsections=REQUEST.get('defaultsections'))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="4"> <p>Now enter the types and urgencies you want that issues can have:</p> <table cellpadding=5> <tr> <th>Types</th> <th>Urgencies</th> </tr> <tr> <td> <textarea name="types:lines" rows="6" cols="35"><dtml-var "_.string.join(types, '\n')" html_quote></textarea> </td> <td> <textarea name="urgencies:lines" rows="6" cols="35"><dtml-var "_.string.join(urgencies, '\n')" html_quote></textarea> </td> </tr> </table> <br><input type="submit" value=" Continue >> "> </form> <dtml-elif "goto==2"> <dtml-call "PropertiesWizardRemember(sections_options=REQUEST.get('sections_options'))"> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="3"> <p>Which one (or which ones) of these do you want to be default. I.e. chosen if none is chosen when an issue is added:</p> <dtml-let this_sections_options="REQUEST.get('sections_options',sections_options)"> <select name="defaultsections:list" multiple size="<dtml-var "_.min(7, _.len(this_sections_options))">"> <dtml-in this_sections_options> <option <dtml-if "_['sequence-item'] in defaultsections">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </dtml-let> <br><input type="submit" value=" Continue >> "> </form> <dtml-else> <form action="PropertiesWizard" method="post"> <input type="hidden" name="goto:int" value="2"> <p>Please enter what various Sections you want to employ:</p> <textarea name="sections_options:lines" rows="6" cols="35"><dtml-var "_.string.join(sections_options,'\n')" html_quote></textarea> <br><input type="submit" value=" Continue >> "> </form> </dtml-if> </dtml-comment> <dtml-var manage_page_footer> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/home.css.dtml��������������������������������������������������������������0000644�0001750�0001750�00000001456�11012074373�020226� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=30)"><dtml-call "RESPONSE.setHeader('Content-type','text/css')"> #rest { float:right; width:48%; } #outlook { width:90%; voice-family: "\"}\""; voice-family:inherit; width:46%; } #outlook dd { margin-bottom:9px; } #outlook dt.daylabel { font-size:110%; font-weight:bold;} ul.further { margin: 0 0 0 10px; padding: 0; list-style: none; } ul.further li { margin: 2px 0 6px 0; padding: 0; font-weight: bold; line-height: 24px; /* height of icon */ background-repeat: no-repeat; background-position: 0 50%; } ul.further li a { padding-left: 24px; /* width of icon + whitespace */ } #statistics { background-image: url(/misc_/IssueTrackerProduct/statistics.gif); } #reports { background-image: url(/misc_/IssueTrackerProduct/reports.gif); } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/addIssueUser.dtml����������������������������������������������������������0000644�0001750�0001750�00000010533�11012074373�021103� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <script type="text/javascript"> function validateForm(f) { var p = f.password.value; var c = f.confirm.value; if (p!=c) { alert("Error: Password and confirmation do not match"); return false; } var e = f.email.value; if (!e) { alert("Error: No email provided"); return false; } return true; } </script> <dtml-var "manage_form_title(this(), _, form_title='Add User', help_product='OFSP', help_topic='User-Folder_Add-User.stx' )"> <p class="form-help"> To add a new user, enter the name <dtml-unless remote_user_mode__> ,password, confirmation</dtml-unless> and roles for the new user and click "Add". <em>Domains</em> is an optional list of domains from which the user is allowed to login. </p> <form action="manage_users" method="post" onsubmit="return validateForm(this)"> <table> <tr> <td valign="top"> <table> <tr> <td align="left" valign="top"> <div class="form-label"> Name </div> </td> <td align="left" valign="top"> <input type="text" name="name" size="30" /> </td> </tr> <dtml-if remote_user_mode__> <input type="hidden" name="password" value="password" /> <input type="hidden" name="confirm" value="password" /> <dtml-else> <tr> <td align="left" valign="top"> <div class="form-label"> Password </div> </td> <td align="left" valign="top"> <input type="password" name="password" size="30" /> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> (Confirm) </div> </td> <td align="left" valign="top"> <input type="password" name="confirm" size="30" /> </td> </tr> </dtml-if> <tr> <td align="left" valign="top"> <div class="form-optional"> Domains </div> </td> <td align="left" valign="top"> <input type="text" name="domains:tokens" size="30" value="" /> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> Roles </div> </td> <td align="left" valign="top"> <div class="form-element"> <select name="roles:list" size="5" multiple> <dtml-in valid_roles> <dtml-if expr="_vars['sequence-item'] != 'Authenticated'"> <dtml-if expr="_vars['sequence-item'] != 'Anonymous'"> <dtml-if expr="_vars['sequence-item'] != 'Shared'"> <option value="<dtml-var sequence-item html_quote>"><dtml-var sequence-item> </dtml-if> </dtml-if> </dtml-if> </dtml-in valid_roles> </select> <br /> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> E-Mail </div> </td> <td align="left" valign="top"> <input type="text" name="email" size="30" value="" /> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> Full name </div> </td> <td align="left" valign="top"> <input type="text" name="fullname" size="30" value="" /> </td> </tr> <tr> <td> <input type="submit" name="submit" value="Add" /> </div> </td> </tr> </table> </td> <td>   </td> <td valign="top"> <h3>Explanation</h3> <p><b>IssueTracker Manager</b> only is if the user should be able to change the status of issues (i.e. Take, Complete, Delete, etc.) and see the confidential issues.<br> An IssueTracker Manager can <i>not</i> access the Zope management interface like a native Manager can.</p> <p><b>IssueTracker User</b> is very basic meaning the user can at least view the issue tracker of disable for anonymous users but not much else.</p> <p><b>Manager</b> supersedes IssueTracker Manager so if you want a user to have full access to everything this is the choice.</p> <p>Any user can have multiple roles but it shouldn't be necessary since they are all linearly inherited meaning that there's nothing extra an <b>IssueTracker Manager</b> can do that a <b>Manager</b> can't and so on.</p> <p tal:replace="nothing"><b>'Must change password'</b> means that the first time this user logs in a page will appear for the user asking for a new better password.<br /> This is useful if at this stage set them up with a dummy password (e.g. 'test123') but really want the user to change it to something better.</p> </td> </tr> </table> </form> <dtml-var manage_page_footer> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/addReportScript.dtml�������������������������������������������������������0000644�0001750�0001750�00000004556�11012074373�021624� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-var "manage_form_title(this(), _, form_title='Add Issue Report Script', )"> <p class="form-help"> Report Scripts makes it possible to make a selection of Issues related to very fine-tuned requirements on each issue. </p> <script type="text/javascript"> function rememberformvalues(href) { var f=document.f; var n='?'; if (href.indexOf('?')>-1) n = '&'; if (f.name.value) href += n+'name='+f.name.value; return href; } </script> <form action="manage_addIssueReportScript" method="post" enctype="multipart/form-data" name="f"> <table cellspacing="0" cellpadding="2" border="0"> <tr id="idrow" <dtml-unless showidrow>style="display:none"</dtml-unless>> <td align="left" valign="top"> <div class="form-label"> Id </div> </td> <td align="left" valign="top"> <input type="text" name="id" size="40" /> </td> </tr> <tr id="namerow"> <td align="left" valign="top"> <div class="form-label"> <dtml-if showidrow>Title<dtml-else>Name</dtml-if> </div> </td> <td align="left" valign="top"> <input type="text" name="name" size="40" value="<dtml-var "REQUEST.get('name','')">" /> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-optional"> File </div> </td> <td align="left" valign="top"> <input type="file" name="file" size="25" value="" /> </td> </tr> <tr id="urlrow" <dtml-unless showurlrow>style="display:none"</dtml-unless>> <td align="left" valign="top"> <div class="form-optional"> URL </div> </td> <td align="left" valign="top"> <input type="text" name="url2script" size="45" value="" /> </td> </tr> <tr> <td align="left" valign="top"> </td> <td align="left" valign="top"> <div class="form-element"> <input class="form-element" type="submit" name="submit" value=" Add " /> <input class="form-element" type="submit" name="submit" value=" Add and Edit " />    <dtml-unless showidrow> <a href="addReportScript?showidrow=yes&showurlrow=yes" style="font-size:10px" onclick="this.href=rememberformvalues(this.href)" onkeypress="this.href=rememberformvalues(this.href)">advanced</a> </dtml-unless> </div> </td> </tr> </table> </form> <dtml-var manage_page_footer> ��������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/test_report.dtml�����������������������������������������������������������0000644�0001750�0001750�00000000711�11012074373�021052� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Test')"> <dtml-var manage_tabs> </dtml-with> <p>To test this report, select the method of representation below</p> <p style="font-weight:bold"> <a href="<dtml-var getRootURL>/ListIssues/report-<dtml-var getId url_quote_plus>">List Issues</a> <br /><br /> <a href="<dtml-var getRootURL>/CompleteList/report-<dtml-var getId url_quote_plus>">Complete list</a> </p> <dtml-var manage_page_footer>�������������������������������������������������������IssueTrackerProduct/dtml/screen.css.dtml������������������������������������������������������������0000644�0001750�0001750�00000016460�11012074373�020556� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=30)"><dtml-call "RESPONSE.setHeader('Content-type','text/css')"> html, body { margin:0; padding:0; background: white; color: black; font-family:Arial, sans-serif, verdana; } p,td,th,div,span { font-size: x-small; voice-family: "\"}\""; voice-family: inherit; font-size: small } .status { color:red; } span.id { color:rgb(70,70,70); } h3 span.id { font-size:11pt; } .qbroader { /* broaden your search blurb */ text-align:center; font-style:italic; } h1.head { font:bold 18pt Verdana; } h3.preview { font-style:italic; } .bigger { font-size:120%; } .hidden { display:none; } .smaller, .s { font-size:85%; } .floatL { float:left; } .floatR { float:right; } div.errormessage { border:1px solid #F99; background-color:#FCC; padding-left:10px; padding-right:10px; } .submiterror { color:red; font-weight:bold; } .aeh a { text-decoration:none; } .aeh a:hover { text-decoration:underline; } a.brother { text-decoration:none; } a.backlink { font-weight:bold; } abbr { text-decoration:none; border-bottom:0px solid #efefef; } #tsizer a { border:1px solid #ccc; text-decoration:none; color:#666; padding:1px 3px; margin:5px 0; } .bumf { width:500px; padding-left:20px; } /* For the tabs ------------------------ */ div#tabs { width:800px; } ul#tabnav { font: bold 11px verdana,arial; list-style-type: none; padding-bottom: 24px; /* border-bottom: 1px solid #f9f9f3; this cause a problem in IE */ margin:0; } ul#tabnav li { float: left; height: 21px; background-color: #eee; /*background:#CC9 url(/misc_/IssueTrackerProduct/gradhead.png);*/ margin: 2px 2px 0 2px; border: 1px solid #999966; border-bottom: 1px solid #e4e4c8; } div#tab1 li.tab1, body#tab0 li.tab0 { border-bottom: 1px solid #fff; background-color: #fff; } div#tab1 li.tab1 a, body#tab0 li.tab0 a { text-decoration: none; color: #000; } #tabnav a { float: left; display: block; color: #666; text-decoration: none; padding:4px; } #tabnav a:hover { /*background: #fff;*/ background-color:#f9f9f9; color:#333; } /* Header --------------------------- */ #head { background:#CC9 url(/misc_/IssueTrackerProduct/gradhead.png); margin-bottom:4px; } div#logo { padding-right:10px; float:left; } h1.head { margin-left:10px; float:left; } h2.head { float:left; margin-left:30px; } div#topright { padding-top:10px; padding-right:30px; float:right; } .clearer { clear: left; line-height: 0; height: 0; } .clearerR { clear: right; line-height: 0; height: 0; } #main { padding:7px; padding-bottom:2px; } #foot { clear: both; margin-top: 15px; padding-top:1em; color: #333; border-top:1px solid #999966; } #foot a { font-size:9pt; color:#333; } /* Thread ------------------------ */ .threaddate { float:right; padding:2px 4px; } .threadfiles { padding:0 4px; } .cth { /* ColorThreadChange */ color:#c00; } .threadbox { border:1px solid #999; width:610px; margin-bottom:13px; } .thead { padding:5px 10px; background-color:#ccc; } .trest { background-color:#eee; } .tbody { font-size: 10pt; font-family:verdana,helvetica, arial, sans-serif; padding:2px 20px; } /* List Issues --------------------------- */ .ftitle { text-align:center; font-weight:bold; padding-bottom:3px; } .bd { /* brief description */ color:#828282; text-align:right; } .ilink { font-weight:bold; } .topbar { width:97%; margin-left:auto; margin-right:auto; } tr.lhead { background:#C3C3C3 url(/misc_/IssueTrackerProduct/gradtablehead.png); font-weight:bold; } tr.l_commenthead { border-top:1px solid #666; background-color:#C3C3C3; font-weight:bold; } .leven { background-color:#DFDFDF; } .lodd { background-color:#F1F1F1; } /* Search result ----------------------------------------- */ .q_highlight { /* font-weight:bold; */ background-color:#ffff88; } p .q_highlight {font-weight:bold;} a.highlight-toggle { margin-left:30px; } /* Special options ---------------------------------------- */ #special-options { border-top:1px solid #ccc; margin-top:30px; margin-bottom:20px; padding:14px; } /* Recent history ---------------------------------------- */ #recent { padding-left:14px; border-top:1px solid #ccc; margin:3px; } .rblock { float: left; } div#rblock-issues { width: 45%;} div#rblock-search { width: 25%;} div#rblock-reports{ width: 30%;} ul.r { padding:0; padding-left:20px; margin:0; } /* Forms --------------------------------- */ .submitbutton { font-weight:bold; } .smallbutton { font-size:80%; } /* Show Issue ----------------------- */ .issue { border:1px solid #999; background-color:#eee; padding:0; width:690px; margin-bottom:1.1em; } .ihead { margin:0; background:#ccc url(/misc_/IssueTrackerProduct/gradissuehead.png); border-right:1px solid #999; voice-family: "\"}\""; voice-family:inherit; height:30px; } html>body .ihead { height:31px; } .ihead h3 { font-family:arial,sans-serif; font-size:14px; padding:6px 10px 0 15px; margin:0; } .istatus { padding:8px 12px; float: right; font-family:verdana; font-weight:bold; color:red; background-color: #eee; } .ibody { font-size: 10pt; font-family:verdana; padding:6px 24px; } .sig, .sig a { /* signature in text */ color:#666; } .idetails-table { margin-left:15px; margin-bottom:10px; } .ilabel { font-weight:bold; padding-right:8px; vertical-align:top; } .idetails2 { float:right; text-align:right; margin-right:15px; } .shade { color:#666; } div.assignment { padding:6px; } img.fileatt { vertical-align:middle; padding:3px; } img.captcha { padding:2px; } /* Option buttons ---------------------------- */ ul#navlist { font-size:10pt; text-align:center; padding: 0; margin: 0; list-style-type: none; width: 100%; } ul#navlist li { display:inline;} ul#navlist li a { width: 8em; color: #000; background-color: #eee; padding: 0.2em 1em; text-decoration: none; border: 1px solid #ccc; } ul#navlist li a:hover { background-color: #ccc; color: #000; border: 1px solid #666; } a.assignment { font-size:0.8em; } #issuedrafts { float:right; width:340px; border:1px solid #666; background-color:#F6F6F6; padding:2px 9px; } #issuedrafts p { margin-top:4px;margin-bottom:4px; } /* Misc. -------------------------- */ /* Urgencies */ .ur-0 {color:#666; } .ur-1 { } /* default 'normal' */ .ur-2 {font-weight:bold; } .ur-3 {font-weight:bold; color:red; } .ur-4 {font-weight:bolder; color:red; } div.note { font-size:80%; color:#333; } .extendedoptions { padding:0 4px 4px 4px; float:left; } /* permanent links to followups */ a.plink { color:#ccc; } a.plink:hover { text-decoration:none; color:#999; } /* Holly hack to cure peek-a-boo IE 6 bug http://real.issuetrackerproduct.com/0014 */ /* Hides from IE5-mac \*/ * html .trest {height: 1%;} /* End hide from IE5-mac */ a.fileattachment-tip { text-decoration:none; color:#666; } div.problem-warning-message { position:absolute; top:60px; right:20px; width:350px; border:1px solid red; background-color:#CCCC99; padding:3px; } div.problem-warning-message a.close { float:right; } div.problem-warning-message p.error { font-weight:bold; margin-top:0; }����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/draftissuethread_properties.dtml�������������������������������������������0000644�0001750�0001750�00000001370�11012074373�024317� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">Followup Draft properties</p> <p>The draft objects do not have a manual Properties interface. The following details is known about this draft:</p> <table> <tr> <th>Attribute</th> <th>Value</th> </tr> <dtml-in get__dict__nicely> <tr> <td><p><b><dtml-var "_['sequence-item']['key']"></b></p></td> <td><p><dtml-var "_['sequence-item']['value']" html_quote newline_to_br></p></td> </tr> </dtml-in> </table> <p> </p> <dtml-var manage_page_footer> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/ManagementSpamProtection.dtml����������������������������������������������0000644�0001750�0001750�00000010677�11012074373�023460� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Management')"> <dtml-var manage_tabs> </dtml-with> <script type="text/javascript"> function _select(check_on) { var f = document.getElementById('foundspam'); var box, j; var boxes = [f['issuepaths:list'], f['threadpaths:list']]; for (j in boxes) { box = boxes[j]; for (var i=0,len=box.length; i<len; i++) box[i].checked = check_on; } } function selectAll() { _select(true); } function unselectAll() { _select(false); } </script> <style type="text/css"> #foundspam a:link, #foundspam a:visited { text-decoration:underline; } </style> <dtml-var "ManagementTabs('Spam protection')"> <p>Spam protection is done by you specifying what keywords aren't allowed in issues or followups. If there's a match on any of the keywords you specify here, the issue or the follow will not be saved.</p> <a name="form"></a> <form action="manage_saveSpamKeywords" method="post"> <dtml-let keywords="getSpamKeywords()" expanded_keywords="getSpamKeywordsExpanded()"> <textarea name="keywords:lines" cols="50" <dtml-if "_.len(expanded_keywords)>50">rows="30" <dtml-elif "_.len(expanded_keywords)>20">rows="20" <dtml-else>rows="10" </dtml-if> ><dtml-var "'\n'.join(expanded_keywords)" html_quote></textarea> </dtml-let> <br /> <input type="submit" value="Save spam keywords" /> </form> <br /> <dtml-if "REQUEST.get('findspam')"> <a name="findings"></a> <form action="manage_deleteIssuesAndThreads" id="foundspam" method="post"> <dtml-call "REQUEST.set('totalcount',0)"> <dtml-let issues="manage_findIssuesContainingSpam()"> <dtml-call "REQUEST.set('totalcount',REQUEST['totalcount']+_.len(issues))"> <dtml-in issues> <dtml-if sequence-start><strong>Issue with spam in it</strong><br /></dtml-if> <input type="checkbox" name="issuepaths:list" value="<dtml-var "'/'.join(getPhysicalPath())">" /> <dtml-if "ShowIdWithTitle()">#<dtml-var getId></dtml-if> <a href="<dtml-var absolute_url>" ><dtml-var getTitle html_quote></a> <dtml-let count="countThreads()"> <dtml-if count> <small>(<dtml-var count> followups)</small> </dtml-if> </dtml-let> <br /> </dtml-in> </dtml-let> <dtml-let threads="manage_findThreadsContainingSpam()"> <dtml-call "REQUEST.set('totalcount',REQUEST['totalcount']+_.len(threads))"> <dtml-in threads> <dtml-if sequence-start><strong>Followups with spam in it</strong><br /></dtml-if> <input type="checkbox" name="threadpaths:list" value="<dtml-var "'/'.join(getPhysicalPath())">" /> <dtml-if "ShowIdWithTitle()">#<dtml-var "aq_parent.getId()"></dtml-if> <a href="<dtml-var "aq_parent.absolute_url()">#i<dtml-var "REQUEST['thread_counts'].get(absolute_url_path(),'')">" ><dtml-var getTitle html_quote></a> <small>(of <a href="<dtml-var "aq_parent.absolute_url()">"><dtml-var "aq_parent.getTitle()"></a>)</small> <br /> </dtml-in> </dtml-let> <dtml-if "REQUEST['totalcount']>10"> <input type="button" name="nothing" value="Select all" style="font-size:80%" onclick="if(this.value=='Select all'){selectAll();this.value='Unselect all'}else{unselectAll();this.value='Select all'}" /> </dtml-if> <input type="submit" value="Delete selected" /> </form> </dtml-if> <form action="manage_ManagementSpamProtection#findings"> <dtml-if "REQUEST.get('findspam')"> <input type="submit" name="findspam" value="Find again" /> <dtml-else> <input type="submit" name="findspam" value="Find issues and followups on current config" /> </dtml-if> </form> <br /> <h4>How this works</h4> <p> You write <strong>one keyword per line</strong>. A <strong>keyword can contain spaces</strong>. Any duplicates or blank are filtered out when you save. </p> <p>Combinations and subwords are entered like this: you write first the keyword and then all subwords on the following lines but add a bit of whitespace before the words to indicate the relationship of the first keyword. Combinations are matched if the first word (no whitespace to the left) is found <em>and</em> the <em>any</em> of the following words appear. This is useful if you want to flag up two offending words who on their own are harmless but together is considered spam. It doesn't matter who many spaces you put in front of a subword. All text matching is done <strong>case <em>in</em>sensitively</strong>. </p> <br />  <dtml-var manage_page_footer> �����������������������������������������������������������������IssueTrackerProduct/dtml/mainIssueUser.dtml���������������������������������������������������������0000644�0001750�0001750�00000003247�11012074373�021303� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-var manage_tabs> <form action="manage_users" method="post"> <dtml-if user_names> <p class="form-help"> The following users have been defined. Click on the name of a user to edit that user. </p> <table cellspacing="0" cellpadding="2" border="0"> <dtml-in user_names> <dtml-if sequence-odd> <tr class="row-normal"> <dtml-else> <tr class="row-hilite"> </dtml-if> <td align="left" valign="top"> <input type="checkbox" name="names:list" value="<dtml-var sequence-item html_quote>" /> </td> <td align="left" valign="top"> <div class="list-item"> <a href="manage_users?name=<dtml-var sequence-item fmt=url-quote>&submit=Edit"><img src="<dtml-var BASEPATH1>/p_/User_icon" alt="" border="0" /></a> <a href="manage_users?name=<dtml-var sequence-item fmt=url-quote>&submit=Edit"><dtml-var sequence-item></a> </div> </td> <td> <dtml-var "' '*10"> <a href="manage_passwordReminder?name=<dtml-var sequence-item>" style="font-size:11px">password reminder</a> </td> </tr> </dtml-in user_names> <tr> <td align="left" valign="top">  </td> <td align="left" valign="top" colspan="2"> <div class="form-element"> <input class="form-element" type="submit" name="submit" value="Add..." /> <input class="form-element" type="submit" name="submit" value="Delete" /> </div> </td> </tr> </table> <dtml-else user_names> <p class="std-text"> There are no users defined. </p> <p> <div class="form-element"> <input class="form-element" type="submit" name="submit" value="Add..." /> </div> </p> </dtml-if user_names> </form> <dtml-var manage_page_footer> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/NotImplemented.dtml��������������������������������������������������������0000644�0001750�0001750�00000000104�11012074373�021420� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<p>Do not use this method. Use the View part of the IssueTracker</p>������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/management_tabs.dtml�������������������������������������������������������0000644�0001750�0001750�00000002047�11012074373�021631� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<style type="text/css"> #navcontainer { font-size:13px; margin-left: auto; margin-right: auto; margin-bottom: 40px; border-top: 1px solid #999; z-index: 1; } #navcontainer ul { list-style-type: none; text-align: center; margin-top: -8px; padding: 0; position: relative; z-index: 2; } #navcontainer li { display: inline; text-align: center; margin: 0 5px; } #navcontainer li a { padding: 1px 7px; color: #666; background-color: #fff; border: 1px solid #ccc; text-decoration: none; /*text-decoration: underline; */ } #navcontainer li a:hover { color: #000; border: 1px solid #666; border-top: 2px solid #666; border-bottom: 2px solid #666; } #navcontainer li a#current { background-color:#eee; font-weight:bold; color: #000; border: 1px solid #666; border-top: 2px solid #666; border-bottom: 2px solid #666; } .clearboth { clear: both; } </style> <br class="clearboth"> <div id="navcontainer"> <ul id="navlist"> <dtml-in tabdicts mapping> <li><a <dtml-if current>id="current"</dtml-if> href="<dtml-var href>"><dtml-var name></a> </dtml-in> </ul> </div>�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/followup.js.dtml�����������������������������������������������������������0000644�0001750�0001750�00000012450�11012074373�020765� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=24)"><dtml-call "RESPONSE.setHeader('Content-type','application/x-javascript')"> var modified_timestamp; var _base_url = location.href.split(/[\?\#]/)[0]; if (_base_url.slice(-10)=='index_html') _base_url = _base_url.slice(0,-10); if (_base_url.charAt(_base_url.length-1)=='/') _base_url = _base_url.substring(0, _base_url.length-1); $(function() { $.get(_base_url+'/getModifyTimestamp', {}, function(resp) { if (resp) modified_timestamp = resp; }); }); function checkRefresh() { if (!modified_timestamp) return false; $.get(_base_url+'/getModifyTimestamp',{}, function(resp) { if (resp && modified_timestamp && parseInt(resp) != parseInt(modified_timestamp)) { modified_timestamp = resp; $('#threads').load(_base_url+'/ShowIssueThreads'); $('#issuedata').load(_base_url+'/ShowIssueData'); if (document.title.indexOf("(automatically refreshed) ")==-1) document.title = "(automatically refreshed) " + document.title; refreshinterval = orig_refreshinterval; } }); } function clearAutoRefreshTitle() { document.title = document.title.replace(/\(automatically refreshed\) /,''); } <dtml-if "SaveDrafts() and UseAutoSave()"> function autosave() { $.post(_base_url+'/AutoSaveDraftThread', $(document.form_followup).fastSerialize(), function(resp) { if (resp) $('input[name="draft_followup_id"]').val(resp); }); } var as_timer; var orig_autosaveinterval=<dtml-var getAutosaveInterval>; var autosaveinterval=<dtml-var getAutosaveInterval>; startautosave = function() { autosave(); as_timer=window.setTimeout("startautosave()", autosaveinterval*1000); }; stopautosave=function() { if (as_timer) window.clearTimeout(as_timer); }; </dtml-if> var previous_clairvoyant_note=''; var page_generated_timestamp; function clairvoyant_followups() { var url = _base_url+'/getRecentOtherDraftThreadAuthor?nochache='+(Math.random()+"").substr(2, 5); if ($('input[name=page_generated_timestamp]').size()) page_generated_timestamp = $('input[name=page_generated_timestamp]').val(); if (page_generated_timestamp) url += '&min_timestamp='+page_generated_timestamp; $.get(url, {'only_fromname':1}, function(resp) { try { if (resp && resp != previous_clairvoyant_note) { $('#recent-draftthread-author').text(resp).show(); previous_clairvoyant_note = resp; setTimeout(function() { $('#recent-draftthread-author').fadeOut(500); }, 20*1000); clairvoyantinterval = orig_clairvoyantinterval; } } catch(ex) {alert(ex);} }); } var cf_timer; var clairvoyantinterval,orig_clairvoyantinterval; clairvoyantinterval=orig_clairvoyantinterval=4; start_clairvoyant_followups = function() { clairvoyant_followups(); cf_timer=window.setTimeout("start_clairvoyant_followups()", clairvoyantinterval*1000); clairvoyantinterval+=0.1; } var r_timer; var refreshinterval,orig_refreshinterval; refreshinterval=orig_refreshinterval=3; function startautorefresh() { checkRefresh(); r_timer=window.setTimeout(startautorefresh, refreshinterval*1000); refreshinterval+= refreshinterval*0.08; <dtml-if "EncodeEmailDisplay()">fixEncodedLinks();</dtml-if> } function toggleHighlight() { if ($('span.q_highlight').size()) { $('span.q_highlight').addClass('q_highlight-off').removeClass('q_highlight'); $('a.highlight-toggle').removeClass('q_highlight').text('Turn on highlighting'); } else { $('span.q_highlight-off').addClass('q_highlight').removeClass('q_highlight-off'); $('a.highlight-toggle').addClass('q_highlight').text('Turn off highlighting'); } } $(function() { startautorefresh(); start_clairvoyant_followups(); if ($('span.q_highlight').size() && $('a.backlink')) { // make a javascript link that removes the highlighting $('a.backlink').parent('div').append( $('<a>').attr('href','.').addClass('highlight-toggle').addClass('q_highlight') .click(function() {toggleHighlight();return false}) .text('Turn off highlighting') ); } }); function af(dest, e, ignoreword) { var val=dest.value; if (val.indexOf(e)==-1) { val = val.replace(ignoreword, ""); if ($.trim(val).charAt($.trim(val).length-1)!=',') val = val+", "; val = val+e+", " } else { if (val.indexOf(e+", ")!=-1) { val = val.replace(e+", ", ""); } } dest.value = val; } function softsubmit(action) { if (document.form_followup) { document.form_followup.action.value=action; document.form_followup.submit(); } } function showAssignmentForm() { $('#assignment-form-rest').show(300); $('#assignee').css('color','#000'); } function hideAssignmentForm() { $('#assignment-form-rest').hide(300); $('#assignee').css('color','#666'); } function closeAnnouncement() { $('table.announcement').hide(); return false; } /* FEATURE ON HOLD // If the AJAX doesn't work for some reason, pause the autorefresh for a // very long time and show a little notice message. $(document).ajaxError(function(event, request, settings){ clairvoyantinterval = refreshinterval = 60; //seconds alert(settings.url); showAJAXProblemWarning(settings.url); }); */ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/draftissue_properties.dtml�������������������������������������������������0000644�0001750�0001750�00000001365�11012074373�023133� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">Issue Draft properties</p> <p>The draft objects do not have a manual Properties interface. The following details is known about this draft:</p> <table> <tr> <th>Attribute</th> <th>Value</th> </tr> <dtml-in get__dict__nicely> <tr> <td><p><b><dtml-var "_['sequence-item']['key']"></b></p></td> <td><p><dtml-var "_['sequence-item']['value']" html_quote newline_to_br></p></td> </tr> </dtml-in> </table> <p> </p> <dtml-var manage_page_footer> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/js-core.js.dtml������������������������������������������������������������0000644�0001750�0001750�00000004372�11012074373�020464� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(24)"><dtml-call "RESPONSE.setHeader('Content-Type','application/x-javascript')"> /* Are you still using this template? Then you haven't upgraded your StandardHeaderFooter.zpt template probably. */ // from http://ejohn.org/projects/flexible-javascript-events/ function addEvent(obj, type, fn) { if (obj.attachEvent) { obj["e"+type+fn] = fn; obj[type+fn] = function() { obj["e"+type+fn]( window.event ); } obj.attachEvent("on"+type, obj[type+fn]); } else obj.addEventListener(type, fn, false); } // from http://ejohn.org/projects/flexible-javascript-events/ function removeEvent(obj, type, fn) { if (obj.detachEvent) { obj.detachEvent("on"+type, obj[type+fn]); obj[type+fn] = null; } else obj.removeEventListener(type, fn, false); } function $() { var elements = new Array(); for (var i=0; i < arguments.length; i++) { var element = arguments[i]; if (typeof element == 'string') element=document.getElementById(element); if (arguments.length == 1) return element; elements.push(element); } return elements; } function econvert(s) { return s.replace(/%7E/g,'~').replace(/%28/g,'(').replace(/%29/g,')').replace(/%20/g,' ').replace(/_dot_| dot |_\._|\(\.\)/gi, '.').replace(/_at_|~at~/gi, '@');} function AEHit() { var sp = document.getElementsByTagName("span"); for (i=0; i< sp.length; i++) if (sp[i].className=="aeh") sp[i].innerHTML = econvert(sp[i].innerHTML); } addEvent(window, 'load', AEHit); function form2querystring(f) { var d = ''; for (i=0;i < f.elements.length;i++) { ob = f.elements[i]; if ((ob.type=='text' || ob.type=='textarea' || ob.type=='hidden') || (ob.type=='radio' && ob.checked)) { if (ob.name!='') d += ob.name +'='+ escape(ob.value) +'&'; } else if (ob.type=='select-one') { d += ob.name +'='+ escape(ob.options[ob.selectedIndex].value) +'&'; } else if (ob.type=='select-multiple') { for (y=0;y < ob.options.length;y++) if (ob.options[y].selected) d += ob.name +'='+ escape(ob.options[y].value) +'&'; } else if (ob.type=='checkbox') { if (ob.checked) d += ob.name +'=1&'; else d += ob.name +'=False&'; } } return d; } function G(p) { location.href=p; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/editNotifyableGroupForm.dtml�����������������������������������������������0000644�0001750�0001750�00000001757�11012074373�023316� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">IssueTracker Notifyable Group</p> <dtml-let notify_groups="getNotifyableGroups()"> <form action="<dtml-var URL1>" method="post"> <input type="hidden" name="id" value="<dtml-var id>"> <dtml-if "REQUEST.has_key('back_url')"> <input type="hidden" name="back_url" value="<dtml-var "REQUEST['back_url']">"> </dtml-if> <table> <tr> <td><div class="form-label">Group title</div></td> <td><input name="title" value="<dtml-var getTitle html_quote>" maxlength="50" size="40"></td> </tr> <tr> <td colspan="2" align="center"> <input type="submit" value="Save Changes" name="manage_editNotifyableGroup:method"> </td> </tr> </table> </form> </dtml-let> <dtml-var manage_page_footer> �����������������IssueTrackerProduct/dtml/editIssueTrackerPropertiesForm.dtml����������������������������������������0000644�0001750�0001750�00000122511�11012074373�024656� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <script type="text/javascript" src="tabtastic-combined.js"></script> <style type="text/css"> .tabset_tabs a { font-size:10pt; } /* Taken from Gavin Kistner's Tabtastic (tabtastic.css) */ .tabset_tabs { margin:0; padding:0; list-style-type:none; position:relative; z-index:2; white-space:nowrap } .tabset_tabs li { margin:0; padding:0; display:inline } .tabset_tabs a { color:#999 ! important; background-color:#fff ! important; border:1px solid #99c; text-decoration:none; padding:0 0.6em; border-left-width:1px; border-bottom:none; } .tabset_tabs a:hover { color:#000 ! important; background-color:#fff ! important } .tabset_tabs a.active { color:black ! important; background-color:#efefef ! important; border-color:black; border-left-width:1px; cursor:default; padding-top:1px; padding-bottom:1px; border-bottom:1px solid #efefef; /*border-bottom:none;*/ } /* the last border-bottom should ideally be value 'none' but since MSIE fails on that we have to set it to something. */ .tabset_tabs li.firstchild a { border-left-width:1px } .tabset_content { border:1px solid black; background-color:#efefef; position:relative; z-index:1; padding:0.5em 1em; display:none } .tabset_label { display:none } .tabset_content_active { display:block } @media aural{ .tabset_content, .tabset_label { display:block } } </style> <div style="text-align:right;font-size:10px; padding:6px"> <a href="manage_PropertiesWizard" style="text-decoration:underline">Properties Wizard</a> | <a href="manage_configureMenuForm" style="text-decoration:underline">Configure the menu</a> </div> <form action="<dtml-var URL1>" method="post"> <dtml-unless activetab> <dtml-comment>'activetab' defines which tab should be enabled when you first load the Properties tab. This can be set from the REQUEST or by parameter (what's called 'options' in ZPT). This dtml-unless and REQUEST.set asserts that we have the variable available for testing. </dtml-comment> <dtml-call "REQUEST.set('activetab', 'basic')"> </dtml-unless> <ul class="tabset_tabs"> <li><a href="#basic" <dtml-if "activetab=='basic'">class="active"</dtml-if> onclick="document.forms[0].activetab.value='basic';return false">Basic</a></li> <li><a href="#advanced" <dtml-if "activetab=='advanced'">class="active"</dtml-if> onclick="document.forms[0].activetab.value='advanced';return false">Advanced</a></li> <li><a href="#custom" <dtml-if "activetab=='custom'">class="active"</dtml-if> onclick="document.forms[0].activetab.value='custom';return false">Custom</a></li> </ul> <input type="hidden" name="activetab" value="<dtml-var activetab>" /> <div id="basic" class="tabset_content"> <h3 class="tabset_label">Basic properties</h3> <table cellspacing="0" cellpadding="3" border="0" summary="Basic properties"> <dtml-with "propertysheets.get('properties')"> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-title">Title</label> </div> </td> <td align="left" valign="top"> <input type="text" name="title:<dtml-var UNICODE_ENCODING>:ustring" size="35" value="<dtml-var title html_quote>"> </td> <td align="left" valign="top"> <p> The name of the whole instance </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-sections_options">Section options</label> </div> </td> <td align="left" valign="top"> <textarea name="sections_options:<dtml-var UNICODE_ENCODING>:ulines" rows="6" cols="37"><dtml-var "_.string.join(sections_options,'\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p> Enter as many different sections with which this issue tracker might be used. Further down on this page you can enable new ones to be added outside the Properties tab with the <b>Allow adding new sections</b> option. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-defaultsections">Default sections</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <select name="defaultsections:<dtml-var UNICODE_ENCODING>:ulist" multiple size="<dtml-var "_.min(7, _.len(sections_options))">"> <dtml-in sections_options> <option <dtml-if "_['sequence-item'] in defaultsections">selected="selected"</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> </td> <td align="left" valign="top"> <p> If somebody enters an issue without specifying sections, these are used. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-types">Types</label> </div> </td> <td align="left" valign="top"> <textarea name="types:<dtml-var UNICODE_ENCODING>:ulines" rows="6" cols="37"><dtml-var "_.string.join(types, '\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p> </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-default_type">Default type</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <select name="default_type:<dtml-var UNICODE_ENCODING>:ustring"> <dtml-in types> <option <dtml-if "_['sequence-item']==default_type">selected="selected"</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> </td> <td align="left" valign="top"> <p> If a type is not selected when entering an issue this is used. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-urgencies">Urgencies</label> </div> </td> <td align="left" valign="top"> <textarea name="urgencies:<dtml-var UNICODE_ENCODING>:ulines" rows="6" cols="37"><dtml-var "_.string.join(urgencies, '\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p> </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-default_urgency">Default urgency</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <select name="default_urgency:<dtml-var UNICODE_ENCODING>:ustring"> <dtml-in urgencies> <option <dtml-if "_['sequence-item']==default_urgency">selected="selected"</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> </td> <td align="left" valign="top"> <p> If an urgency is not selected when entering an issue this is used. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-always_notify">Always notify</label> </div> </td> <td align="left" valign="top"> <textarea name="always_notify:lines" rows="6" cols="37"><dtml-var "_.string.join(getAlwaysNotify(), '\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p> The people who get notifications about everything, one per line. Enter either a username or an email address. If what you enter is not an email address but a username, the system will try to find the user and instead show the username and fullname. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-allow_issuesubscription">Allow issue subscription</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="allow_subscription:boolean" <dtml-if "AllowIssueSubscription()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> With this there's a form when viewing an issue to subscribe to changes to it. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-use_tellafriend">Use Tell a Friend</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="use_tellafriend:boolean" <dtml-if "UseTellAFriend()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If you enable the Tell a Friend widget people will be able to quickly inform other people about issues simply by entering their email address or username. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-save_drafts">Save drafts</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="save_drafts:boolean" <dtml-if "SaveDrafts()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If this is true, when adding issues (or followups) it becomes possible to pre-save them as draft objects. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_id_with_title">Show Id with title</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="show_id_with_title:boolean" value="1" <dtml-if "ShowIdWithTitle()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p> If true, the Id is always shown next to the title </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-default_sortorder">Default sort order</label> </div> </td> <td align="left" valign="top"> <select name="default_sortorder:string"> <dtml-in getDefaultSortorderOptions> <option value="<dtml-var sequence-item>" <dtml-if "getDefaultSortorder()==_['sequence-item']">selected="selected"</dtml-if> ><dtml-var "translateSortorderOption(_['sequence-item'])"></option> </dtml-in> </select> </td> <td align="left" valign="top"> <p>If by modification date, follow ups to old issues pushes the issue towards the top of List Issues. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-display_date">Display date format</label> </div> </td> <td align="left" valign="top"> <select name="display_date:string"> <dtml-in getDisplayDateFormatOptions> <option value="<dtml-var sequence-item>" <dtml-if "display_date==_['sequence-item']">selected="selected"</dtml-if> ><dtml-var "ZopeTime('%s-12-13 22:15'%ZopeTime().strftime('%Y')).strftime(_['sequence-item'])"></option> </dtml-in> </select> </td> <td align="left" valign="top"> <div class="list-item"> The usual syntax. </div> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-sitemaster_name">Sitemaster name</label> </div> </td> <td align="left" valign="top"> <input type="text" name="sitemaster_name:<dtml-var UNICODE_ENCODING>:ustring" size="35" value="<dtml-var sitemaster_name html_quote>"> </td> <td align="left" valign="top"> <p> Admin, webmaster of this IssueTracker instance. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-sitemaster_email">Sitemaster email</label> </div> </td> <td align="left" valign="top"> <input type="text" name="sitemaster_email:string" size="35" value="<dtml-var sitemaster_email html_quote>"> </td> <td align="left" valign="top"> <p>  </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-default_batch_size">Default batch size</label> </div> </td> <td align="left" valign="top"> <input type="text" name="default_batch_size:int" size="3" value="<dtml-var default_batch_size>"> </td> <td align="left" valign="top"> <p> How many issues to list in List Issues </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-no_fileattachments">No. file attachments</label> </div> </td> <td align="left" valign="top"> <input type="text" name="no_fileattachments:int" size="3" value="<dtml-var getNoFileattachments>"> </td> <td align="left" valign="top"> <p> Number of possible file attachments when adding issue. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-no_followup_fileattachments">...and for followups</label> </div> </td> <td align="left" valign="top"> <input type="text" name="no_followup_fileattachments:int" size="3" value="<dtml-var getNoFollowupFileattachments>"> </td> <td align="left" valign="top"> <p> Number of possible file attachments when adding some sort of followup. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-allow_issueattrchange">Allow issue attribute change</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="allow_issueattrchange:boolean" <dtml-if "AllowIssueAttributeChange()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> Whether you, as Manager, can change the attributes of an issue once created or not. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-use_issue_assignment">Use Issue Assignment</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="use_issue_assignment:boolean" <dtml-if "UseIssueAssignment()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> Use the <a href="manage_ManagementUsers">User Management page</a> instead. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-images_in_menu">Show images in menu</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="images_in_menu:boolean" <dtml-if "imagesInMenu()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> When true, the menu items will be shown together with a little icon image. Makes every page 1.9Kb bigger in size. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-can_add_new_sections">Allow adding new sections</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="can_add_new_sections:boolean" <dtml-if "CanAddNewSections()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> When true makes it possible to add a new section when adding an issue instead of being restricted to the existing list of sections. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-signature_text">Email signature</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <textarea name="signature_text:<dtml-var UNICODE_ENCODING>:utext" rows="6" cols="37"><dtml-var getSignature html_quote></textarea> </div> </td> <td align="left" valign="top"> <p> Templating variables available are:<br /> <code>[title]</code>, <code>[url]</code>, <code>[date]</code>,<br /> <code>[sitemaster name]</code>, <code>[sitemaster email]</code> <br /> This is used for email send outs and the variables are replaced with their real values just before the email is sent out. </p> </td> </tr> </table> <div class="submitarea" align="center"> <input type="submit" value="Save Changes" name="manage_editIssueTrackerProperties:method"> </div> </div><!-- /basic --> <div id="advanced" class="tabset_content"> <h3 class="tabset_label">Advanced properties</h3> <table cellspacing="0" cellpadding="3" border="0" summary="Advanced properties"> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-outlook_batch_size">Homepage batch size</label> </div> </td> <td align="left" valign="top"> <input type="text" name="outlook_batch_size:int" size="3" value="<dtml-var getOutlookBatchSize>"> </td> <td align="left" valign="top"> <p> How many issues to list in the summary on the home page </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-disallow_duplicate_issue_subjects">Disallow duplicate issue subjects</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="disallow_duplicate_issue_subjects:boolean" <dtml-if "DisallowDuplicateIssueSubjects()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If enabled, won't allow new issues with the exact (case insensitive) same subject if it has been used before </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-use_estimated_time">Use 'Estimated time'</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="use_estimated_time:boolean" <dtml-if "UseEstimatedTime()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> <dtml-if "AllowIssueAttributeChange()">When you click change the issue attributes you can enter a numeral for the number of hours you estimate <em>it will</em> <dtml-if "getStatuses()[-1].lower()=='completed'"> take to become completed. <dtml-else> take to become "<dtml-var "getStatuses()[-1].capitalize()">". </dtml-if> <dtml-else> This property only applies if you have "Allow issue attribute change" enabled. </dtml-if> </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-use_actual_time">Use 'Actual time'</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="use_actual_time:boolean" <dtml-if "UseActualTime()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> <dtml-if "AllowIssueAttributeChange()">When you click change the issue attributes you can enter a numeral for the number of hours <em>it took</em> <dtml-if "getStatuses()[-1].lower()=='completed'"> take to become completed. <dtml-else> take to become "<dtml-var "getStatuses()[-1].capitalize()">". </dtml-if> <dtml-else> This property only applies if you have "Allow issue attribute change" enabled. </dtml-if> </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-statuses">Statuses</label> </div> </td> <td align="left" valign="top"> <textarea name="statuses-and-verbs:<dtml-var UNICODE_ENCODING>:ulines" rows="6" cols="37"><dtml-var "_.string.join(getStatusesMerged(cleaned=1), '\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p> Here you define the statuses issue can have and the captions for going into that status. Each line should be separated by a , comma like this <code><state>, <verb></code>.<br /> <a href="manage_PropertiesStatusScores">Set a score on each status as a measure of progress</a> </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-manage_roles">Manager roles</label> </div> </td> <td align="left" valign="top"> <textarea name="manager_roles:lines" rows="6" cols="37"><dtml-var "_.string.join(getManagerRoles(), '\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p> What makes a user a "IssueTracker Manager" </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-default_display_format">Default display format</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <select name="default_display_format:string"> <dtml-in display_formats> <option <dtml-if "_['sequence-item']==default_display_format">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> </td> <td align="left" valign="top"> <p> </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_spambot_prevention">Enable spambot prevention</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_spambot_prevention:boolean" <dtml-if "ShowSpambotPrevention()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If enabled, posting issues or followups won't be possible unless the user verifies being human by recognizing the text in a set of images. Like captchas. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-dispatch_on_submit">Dispatch on submit</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="dispatch_on_submit:boolean" <dtml-if "doDispatchOnSubmit()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> Send out emails after notifications have been created. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-private_statistics">Private statistics</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="private_statistics:boolean" <dtml-if "PrivateStatistics()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If this is true, then only Managers can see the statistics. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-private_reports">Private reports</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="private_reports:boolean" <dtml-if "PrivateReports()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If this is true, then only Managers can see the reports. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_dates_cleverly">Use "clever" date display</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_dates_cleverly:boolean" <dtml-if "ShowDatesCleverly()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If on when listing or viewing issues it will show the date cleverly depending on how far it is from today's date in a more human form. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-stop_cache">Stop cache</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="stop_cache:boolean" <dtml-if "doStopCache()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If true, headers are set so that no caching happens. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_confidential_option">Show "Confidential issue" option</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_confidential_option:boolean" <dtml-if "ShowConfidentialOption()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If true, in Add Issue, users can set if an issue should be confidential or not. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_hideme_option">Show "Hide me" option</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_hideme_option:boolean" <dtml-if "ShowHideMeOption()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If true, in Add Issue, users can set if their name and email should be hidden. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_issueurl_option">Show "URL" option</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_issueurl_option:boolean" <dtml-if "ShowIssueURLOption()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If true, in Add Issue, you can specify a URL which is related to the issue. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_download_button">Show "Download" button</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_download_button:boolean" <dtml-if "ShowDownloadButton()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If true, when viewing an issue users can click the Download button to get the issue as a text file. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-encode_emaildisplay">Encode email links</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="encode_emaildisplay:boolean" <dtml-if "EncodeEmailDisplay()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If true, all hyperlinked email addresses are display as an encoded JavaScript script. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-use_tellafriend_for_anonymous">Use 'Tell a friend' for anonymous users</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="use_tellafriend_for_anonymous:boolean" <dtml-if "UseTellAFriendForAnonymous()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> If you use the Tell a friend feature, unsetting this will disallow users who have not logged in to use the Tell a friend feature.<br /> Tip! If you're running a public issue tracker instance, spambots can use the Tell a friend to send spam unless you untick this option. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_always_notify_status">Show Always notify status</label> </div> </td> <td align="left" valign="top"> <div class="form-element"> <input type="checkbox" name="show_always_notify_status:boolean" <dtml-if "doShowAlwaysNotifyStatus()">checked="checked"</dtml-if> /> </div> </td> <td align="left" valign="top"> <p> When true, a list is displayed after an issue is added to say who has been notified. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-randomid_length">Id length</label> </div> </td> <td align="left" valign="top"> <input type="text" name="randomid_length:int" size="3" value="<dtml-var randomid_length>"> </td> <td align="left" valign="top"> <p> How many numbers in the Id of issues. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-issueprefix">Issueprefix</label> </div> </td> <td align="left" valign="top"> <input type="text" name="issueprefix:string" size="35" value="<dtml-var issueprefix html_quote>"> </td> <td align="left" valign="top"> <p> String you might want issues id to start with. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_use_accesskeys_option">Accesskeys option</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="show_use_accesskeys_option:boolean" value="1" <dtml-if "ShowAccessKeysOption()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p> With this on users can enable access keys to keyboard shortcuts </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_remember_savedfilter_persistently_option">Persistent filters option</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="show_remember_savedfilter_persistently_option:boolean" value="1" <dtml-if "ShowRememberSavedfilterPersistentlyOption()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p>Allows acl users of an Issue User Folder to set their filters to be remembered persistently, meaning their last used filter is still on even if they restart their browser. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-use_autosave">Autosave drafts</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="use_autosave:boolean" value="1" <dtml-if "UseAutoSave()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p> Only applies if you enable Save drafts. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-show_csvexport_link">CSV export link</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="show_csvexport_link:boolean" value="1" <dtml-if "ShowCSVExportLink()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p> If enabled will show a link at the bottom of <a href="ListIssues">List Issues</a> to export the current unbatched issues to CSV. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-include_description_in_notifications">Include issue description in notifications</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="include_description_in_notifications:boolean" value="1" <dtml-if "IncludeDescriptionInNotifications()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p>If checked, email notifications will be sent out with the issue description included in the email. <br /> An advantage with having this checked is that email notifyees don't have to visit the issuetracker to read the whole text. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-allow_show_all">Allow 'Show all'</label> </div> </td> <td align="left" valign="top"> <input type="checkbox" name="allow_show_all:boolean" value="1" <dtml-if "AllowShowAll()">checked="checked"</dtml-if> /> </td> <td align="left" valign="top"> <p>If checked, on the List Issues and Complete List you can click to list all the first 1000.<br /> This can get really really slow on a high traffic issue tracker and its slow loading time can cause Zope to run high in CPU and memory. </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-plugin_paths">Plugin paths</label> </div> </td> <td align="left" valign="top"> <textarea name="plugin_paths:lines" rows="4" cols="37"><dtml-var "_.string.join(getPluginPaths(), '\n')" html_quote></textarea> </td> <td align="left" valign="top"> <p>A plugin is an adjacent instance object that can be linked to from the issuetracker. Enter the path of the plugin (eg. somefolder/someobject) </p> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> <label for="cb-brother_issuetracker_paths">Join-in issue trackers</label> </div> </td> <td align="left" valign="top"> <dtml-let potentialbrothers="manage_findPotentialBrothers()" brotherpaths="getBrotherPaths()"> <dtml-if potentialbrothers> <select name="brother_issuetracker_paths:list" multiple="multiple" size="<dtml-var "_.min(5, _.len(potentialbrothers))">" <dtml-if "_.len(potentialbrothers)>5"> onfocus="this.size=<dtml-var "_.len(potentialbrothers)">" onblur="this.size=5"</dtml-if> > <dtml-in potentialbrothers> <option value="<dtml-var "'/'.join(getPhysicalPath())">" <dtml-if "'/'.join(getPhysicalPath()) in brotherpaths">selected="selected"</dtml-if> ><dtml-var "getTitle()"> (<dtml-var "absolute_url_path()">)</option> </dtml-in> </select> <dtml-else> <input type="hidden" name="brother_issuetracker_paths:lines" value="" /> <em>No adjacent issue trackers found</em> </dtml-if> </dtml-let> </td> <td align="left" valign="top"> <p>These are issue trackers found in the proximity. You can select several issue trackers from this list and then all of their issues will be counted, listed and shown next to the issues of this issue tracker. Useful if you want to monitor several issue trackers from one single instance. </p> </td> </tr> </table> </dtml-with> <div class="submitarea" align="center"> <input type="submit" value="Save Changes" name="manage_editIssueTrackerProperties:method"> </div> </div><!-- /advanced --> <div id="custom" class="tabset_content"> <h3 class="tabset_label">Custom properties</h3> <dtml-in propertyMap mapping> <dtml-if sequence-start> <table cellspacing="0" cellpadding="3" border="0" summary="Custom properties"> <tr class="list-header"> <td align="left" valign="top" width="8">   </td> <td align="left" valign="top"> <div class="form-label"> Name </div> </td> <td align="left" valign="top"> <div class="form-label"> Value </div> </td> <td align="left" valign="top"> <div class="form-label"> Info </div> </td> </tr> </dtml-if> <dtml-unless "id in native_properties"> <tr> <td align="left" valign="top" width="8"> <dtml-if "'d' in _['sequence-item'].get('mode', 'awd')"> <input type="checkbox" name="ids:list" value="<dtml-var id html_quote>" id="cb-<dtml-var id>"> <dtml-else> </dtml-if> </td> <td align="left" valign="top"> <div class="form-label"> <label for="cb-<dtml-var id>"><dtml-var "propertyLabel(id)"></label> </div> </td> <td align="left" valign="top"> <dtml-if "'w' in _['sequence-item'].get('mode', 'awd')"> <dtml-if "type == 'int'"> <input type="text" name="<dtml-var id>:<dtml-var type>" size="35" value="<dtml-if "hasProperty(id)"><dtml-var "'%s' % getProperty(id)" html_quote></dtml-if>"> <dtml-elif "type == 'long'"> <input type="text" name="<dtml-var id>:<dtml-var type>" size="35" value="<dtml-if "hasProperty(id)"><dtml-var "('%s' % getProperty(id))[:-1]" html_quote></dtml-if>"> <dtml-elif "type in ('float', 'date')"> <input type="text" name="<dtml-var id>:<dtml-var type>" size="35" value="<dtml-var "getProperty(id)" html_quote>"> <dtml-elif "type=='string'"> <input type="text" name="<dtml-var id>:string" size="35" value="<dtml-var "getProperty(id)" html_quote>"> <dtml-elif "type=='boolean'"> <input type="checkbox" name="<dtml-var id>:boolean" size="35" <dtml-if "getProperty(id)">CHECKED</dtml-if>> <dtml-elif "type=='tokens'"> <input type="text" name="<dtml-var id>:tokens" size="35" value="<dtml-in "getProperty(id)"><dtml-var sequence-item html_quote> </dtml-in>"> <dtml-elif "type=='text'"> <textarea name="<dtml-var id>:text" rows="6" cols="37"><dtml-var "getProperty(id)" html_quote></textarea> <dtml-elif "type=='lines'"> <textarea name="<dtml-var id>:lines" rows="6" cols="37"><dtml-in "getProperty(id)"><dtml-var sequence-item html_quote><dtml-if sequence-end><dtml-else><dtml-var "'\n'"></dtml-if></dtml-in></textarea> <dtml-elif "type=='selection'"> <dtml-if "hasProperty(select_variable)"> <div class="form-element"> <select name="<dtml-var id>"> <dtml-in "getProperty(select_variable)"> <option <dtml-if "_['sequence-item']==getProperty(id)">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> <dtml-elif "_.has_key(select_variable)"> <div class="form-element"> <select name="<dtml-var id>"> <dtml-in "_[select_variable]"> <option <dtml-if "_['sequence-item']==getProperty(id)">SELECTED</dtml-if> ><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> <dtml-else> <div class="form-text"> No value for <dtml-var select_variable>. </div> </dtml-if> <dtml-elif "type=='multiple selection'"> <dtml-if "hasProperty(select_variable)"> <div class="form-element"> <select name="<dtml-var id>:list" multiple size="<dtml-var "_.min(7, _.len(getProperty(select_variable)))">"> <dtml-in "getProperty(select_variable)"> <option<dtml-if "getProperty(id) and (_['sequence-item'] in getProperty(id))" > SELECTED</dtml-if >><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> <dtml-elif "_.has_key(select_variable)"> <div class="form-element"> <select name="<dtml-var id>:list" multiple size="<dtml-var "_.min(7, _.len(_[select_variable]))">"> <dtml-in "_[select_variable]"> <option<dtml-if "getProperty(id) and (_['sequence-item'] in getProperty(id))" > SELECTED</dtml-if >><dtml-var sequence-item html_quote></option> </dtml-in> </select> </div> <dtml-else> <div class="form-text"> No value for <dtml-var select_variable>. </div> </dtml-if> <dtml-else> <em>Unknown property type</em> </dtml-if> <dtml-else> <table border="1"> <tr><td><dtml-var "getProperty(id)" html_quote></td></tr> </table> </dtml-if> </td> <td align="left" valign="top"> <div class="list-item"> &dtml-type; </div> </td> <td align="left" valign="top"> <div class="list-item"> </div> </td> </tr> </dtml-unless> <dtml-if sequence-end> <tr> <td colspan="2"> </td> <td align="left" valign="top" colspan="2"> <div class="form-element"> <input name="manage_editProperties:method" type="submit" class="form-element" value="Save Changes" /> <input name="manage_delOtherProperties:method" type="submit" class="form-element" value="Delete" /> </div> </td> </tr> </table> </dtml-if> </dtml-in> <p class="form-help"> To add a new property, enter a name, type and value for the new property and click the "Add" button. </p> <table> <tr> <td align="left" valign="top"> <div class="form-label"> Name </div> </td> <td align="left" valign="top"> <input type="text" name="id" size="30" value=""/> </td> <td align="left" valign="top" class="form-label"> Type </td> <td align="left" valign="top"> <div class="form-element"> <select name="type"> <option>boolean</option> <option>date</option> <option>float</option> <option>int</option> <option>lines</option> <option>long</option> <option selected>string</option> <option>text</option> <option>tokens</option> <option>selection</option> <option>multiple selection</option> </select> </div> </td> </tr> <tr> <td align="left" valign="top"> <div class="form-label"> Value </div> </td> <td colspan=2 align="left" valign="top"> <input type="text" name="value" size="30" /> </td> <td align="right" valign="top"> <div class="form-element"> <input class="form-element" type="submit" name="manage_addOtherProperty:method" value=" Add " /> </div> </td> </tr> </table> </div> </form> <dtml-var manage_page_footer> <dtml-call "RESPONSE.setHeader('Content-Type','text/html; charset=%s'%UNICODE_ENCODING)">���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/AddIssueJavascript.dtml����������������������������������������������������0000644�0001750�0001750�00000002533�11012074373�022234� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=48)"><dtml-call "RESPONSE.setHeader('Content-type','application/x-javascript')"> // Useful functions for Adding issue function askNoti(checkit, youare) { el=$.id('asi-noti'); var extrahtml = "<input type=\"checkbox\" name=\"notify-assignee\" "; if (checkit!=youare) extrahtml += "checked=\"checked\" "; extrahtml += "value=\"1\"/>"; extrahtml += "Send notification to assignee"; extrahtml += "<input type=\"hidden\" name=\"asked-notify-assignee\" value=\"1\"/>"; el.innerHTML=extrahtml; } function shownewsection() { var a=$.id('sections');a.size = a.size-1; var b=$.id('newsection');b.className=""; } <dtml-if "SaveDrafts() and UseAutoSave()"> function autosave() { $.post('AutoSaveDraftIssue', $(document.ai).fastSerialize(), function(resp) { if (resp) { $('input[name="draft_issue_id"]').val(resp); } }); } var as_timer; stopautosave=function() { if (as_timer) clearTimeout(as_timer); }; startautosave=function() { autosave(); as_timer=window.setTimeout("startautosave()", <dtml-var getAutosaveInterval>*1000); }; </dtml-if> function deldraft(draftid) { $('p.draft-'+draftid).remove(); $.post('DeleteDraftIssue', {id:draftid, return_show_drafts_simple:1}, function(resp) { $('#issuedraftsouter').html(resp); }); return false; } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/editIssueUser.dtml���������������������������������������������������������0000644�0001750�0001750�00000006533�11012074373�021305� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <script type="text/javascript"> function validateForm(f) { var p = f.password.value; var c = f.confirm.value; if (p!='password' && c !='pconfirm' && p!=c) { alert("Error: Password and confirmation do not match"); return false; } return true; } </script> <dtml-var "manage_form_title(this(), _, form_title='Change User', help_product='OFSP', help_topic='User-Folder_Edit-User.stx' )"> <form action="manage_users" method="post" onsubmit="return validateForm(this)"> <table> <tr> <td valign="top"> <div class="form-label"> Name </div> </td> <td valign="top"> <div class="form-text"> <dtml-var expr="user.name"> </div> </td> </tr> <tr><td colspan=2> </td></tr> <dtml-if remote_user_mode__> <input type="hidden" name="password" value="<dtml-var password html_quote>" /> <input type="hidden" name="confirm" value="<dtml-var password html_quote>" /> <dtml-else> <tr> <td valign="top"> <div class="form-label"> New Password </div> </td> <td valign="top"> <input type="password" name="password" size="30" value="password" /> </td> </tr> <tr> <td valign="top"> <div class="form-label"> (Confirm) </div> </td> <td valign="top"> <input type="password" name="confirm" size="30" value="pconfirm" /> </td> </tr> </dtml-if> <tr> <td valign="top"> <div class="form-optional"> Domains </div> </td> <td valign="top"> <input type="text" name="domains:tokens" size="30" value="<dtml-if expr="user.domains"><dtml-in expr="user.domains"><dtml-var sequence-item html_quote> </dtml-in></dtml-if>" /> </td> </tr> <tr> <td valign="top"> <div class="form-label"> Roles </div> </td> <td valign="top"> <div class="form-element"> <select name="roles:list" size="5" multiple> <dtml-in valid_roles> <dtml-if expr="_vars['sequence-item'] != 'Authenticated'"> <dtml-if expr="_vars['sequence-item'] != 'Anonymous'"> <dtml-if expr="_vars['sequence-item'] != 'Shared'"> <dtml-if expr="_vars['sequence-item'] in user.roles"> <option value="<dtml-var sequence-item html_quote>" selected><dtml-var sequence-item> <dtml-else> <option value="<dtml-var sequence-item html_quote>"><dtml-var sequence-item> </dtml-if> </dtml-if> </dtml-if> </dtml-if> </dtml-in valid_roles> </select> </td> </tr> <tr> <td valign="top"> <div class="form-optional"> E-Mail </div> </td> <td valign="top"> <input type="text" name="email" size="30" value="<dtml-if expr="user.email"><dtml-var expr="user.email"></dtml-if>" /> </td> </tr> <tr> <td valign="top"> <div class="form-optional"> Full name </div> </td> <td valign="top"> <input type="text" name="fullname" size="30" value="<dtml-if expr="user.fullname"><dtml-var expr="user.fullname"></dtml-if>" /> </td> </tr> <dtml-comment> <tr> <td valign="top"> <div class="form-optional"> Must change password </div> </td> <td valign="top"> <input type="checkbox" name="must_change_password:boolean" value="1" value="<dtml-if "user.mustChangePassword()">checked="checked"</dtml-if>" /> </td> </tr> </dtml-comment> <tr> <td> <input type="hidden" name="name" value="<dtml-var expr="user.name" html_quote>" /> <br /><br /> <input class="form-element" type="submit" name="submit" value="Change" /> </div> </td> </tr> </table> </form> <dtml-var manage_page_footer> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/NotifyableManagementPartForm.dtml������������������������������������������0000644�0001750�0001750�00000011445�11012074373�024252� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<style type="text/css"> a.link { text-decoration:underline; font-size:90%; } </style> <p class="form-help"><i>Notifyables</i> are the kind of entities that can be <i>notified</i> upon submitting an issue. <br>Having no notifyables is the same as switching this feature off.</p> <dtml-call convertOldGroups2Objects> <dtml-if "isGlobalHere()"> <dtml-call "REQUEST.set('only','')"> <dtml-else> <dtml-call "REQUEST.set('only','local')"> </dtml-if> <dtml-let notify_groups="getNotifyableGroups(only=only)"> <table cellspacing=0 cellpadding=0 border=0><tr><td valign="top"> <form action="<dtml-var URL1>" method="post"> <table width="100%" cellpadding="4" border=0> <tr class="list-header"> <th colspan="2"> <img src="/misc_/IssueTrackerProduct/issuetracker_notifyablegroup.gif" alt="Issue Tracker Notifyable Group" title="Issue Tracker Notifyable Group" border="0" align="left" />Notifyable groups</th> </tr> <tr> <dtml-if notify_groups> <td> <table cellpadding="4" cellspacing=0> <dtml-in notify_groups> <tr> <td> </td> <td><input type="checkbox" name="notify_groups:list" value="<dtml-var id>"></td> <td><a href="<dtml-var absolute_url>/manage_editNotifyableGroupForm?back_url=<dtml-var URL>" class="link"><dtml-var title html_quote></a></td> </tr> </dtml-in> </table> <input type="submit" value="Delete selected groups" name="manage_delNotifyGroups:method"> </td> <dtml-else> <td><p><i>None yet</i></p></td> </dtml-if> <td> <table> <tr> <td> </td> <td> <input name="notify_group" size="30" maxlength="30"> </td> <td> <input type="submit" value="Add" name="manage_saveNotifyGroup:method"> </td> </tr> </table> </td> </tr> </table> </form> <br>  </td></tr> <tr><td> <form action="<dtml-var URL1>" method="post"> <dtml-let len_notify_groups="_.len(notify_groups)"> <table border=0 cellpadding="4" cellspacing=0> <dtml-let CurrentURL=URL> <dtml-in "getNotifyables(only=only)"> <dtml-if sequence-start> <tr class="list-header"> <th><img src="/misc_/IssueTrackerProduct/issuetracker_notifyable.gif" alt="Issue Tracker Notifyable" title="Issue Tracker Notifyable" border="0" /></th> <th colspan="3">Existing notifyables</th> </tr> <tr> <th> </th> <td><p><b>Email address and name/alias</b></td> <td colspan="2"><p> <b>Belongs to</b></td> </tr> </dtml-if> <tr> <td><input type="checkbox" name="del_notify_ids:list" value="<dtml-var id>"> <input type="hidden" name="notify_ids:list" value="<dtml-var id>"></td> <td> <a href="<dtml-var absolute_url>/manage_editNotifyableForm?back_url=<dtml-var URL>" class="link" title="Click to edit the this notifyable" ><dtml-if alias><dtml-var alias>, </dtml-if> <dtml-var email></a> </td> <dtml-if len_notify_groups> <td colspan="2">   <dtml-if groups> <dtml-in "getGroupsByIds(groups)"> <a href="<dtml-var absolute_url>/manage_editNotifyableGroupForm" class="link"><dtml-var title html_quote></a><dtml-unless sequence-end>, </dtml-unless> </dtml-in> <dtml-else> <em>no groups</em> </dtml-if> </td> </dtml-if> </tr> <dtml-if sequence-end> <tr> <td colspan="4"> <input name="manage_delNotifyables:method" type="submit" value="Delete selected notifyables" /> <br>  </td> </tr> </dtml-if> </dtml-in> </dtml-let> <dtml-let addHowMany="REQUEST.get('addHowMany',1)"> <tr class="list-header"> <th><img src="/misc_/IssueTrackerProduct/issuetracker_notifyable.gif" alt="Issue Tracker Notifyable" title="Issue Tracker Notifyable" border="0" /></th> <th colspan="3">Add notifyable</th> </tr> <tr> <th> </th> <th>Name/Alias</th> <th>Email address</th> <dtml-if len_notify_groups> <th> <i>Groups</i> </th> </dtml-if> </tr> <dtml-in "_.range(addHowMany)"> <tr> <td> </td> <td><input name="new_alias:list" size="30" maxlength="75"></td> <td><input name="new_email:list" size="30" maxlength="75"></td> <dtml-if len_notify_groups> <dtml-if sequence-start> <td rowspan="<dtml-var addHowMany>" valign="top"><select name="new_groups:list" multiple size="<dtml-var "_.len(notify_groups)">"> <dtml-in notify_groups> <option value="<dtml-var id>"><dtml-var title></option> </dtml-in> </select> </td> </dtml-if> </dtml-if> </tr> </dtml-in> <tr> <td colspan="4" align="center"> <input type="submit" value=" Save " name="manage_addNotifyables:method"> </td> </tr> </dtml-let> </table> </dtml-let> </form> </td></tr></table> </dtml-let> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/home.js.dtml���������������������������������������������������������������0000644�0001750�0001750�00000004354�11012074373�020052� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=0)"><dtml-call "RESPONSE.setHeader('Content-type','application/x-javascript')"> var modified_timestamp; var _base_url = location.href.split(/[\?\#]/)[0]; if (_base_url.slice(-10)=='index_html') _base_url = _base_url.slice(0,-10); if (_base_url.charAt(_base_url.length-1)=='/') _base_url = _base_url.substring(0, _base_url.length-1); function clearAutoRefreshTitle() { document.title = document.title.replace(/\(automatically refreshed\) /,''); } function checkRefresh() { if (!modified_timestamp) return false; $.get(_base_url+'/getModifyTimestamp?nocache='+(Math.random()+"").substr(2, 5),{}, function(resp) { if (resp && modified_timestamp && parseInt(resp) != parseInt(modified_timestamp)) { modified_timestamp = resp; $('#outlook-outer').load(_base_url+'/show_outlook?nocache='+(Math.random()+"").substr(2, 5)); if (document.title.indexOf("(automatically refreshed) ")==-1) document.title = "(automatically refreshed) " + document.title; refreshinterval = orig_refreshinterval; $('#outlook-outer').click(clearAutoRefreshTitle); } }); } var refreshinterval,orig_refreshinterval; refreshinterval=orig_refreshinterval=5; // seconds function startautorefresh() { checkRefresh(); r_timer=window.setTimeout(startautorefresh, refreshinterval*1000); refreshinterval+= refreshinterval*0.08; } $(function() { // Initiall set modified_timestamp $.get(_base_url+'/getModifyTimestamp', {}, function(resp) { if (resp) modified_timestamp = resp; }); startautorefresh(); }); function _deldraft(draftid, method) { $('li.draft-'+draftid).remove(); $.post(method, {id:draftid, return_show_drafts:1}, function(resp) { $('#issuedraftsouter').html(resp); }); return false; } function deldraft(draftid) { return _deldraft(draftid, 'DeleteDraftIssue'); } function deldraftfollowup(draftid) { return _deldraft(draftid, 'DeleteDraftFollowup'); } /* FEATURE ON HOLD // If the AJAX doesn't work for some reason, pause the autorefresh for a // very long time and show a little notice message. $('').ajaxError(function(event, request, settings){ refreshinterval = 60; //seconds showAJAXProblemWarning(settings.url); }); */������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/tw-sack.js.dtml������������������������������������������������������������0000644�0001750�0001750�00000012430�11012074373�020465� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-call "doCache(hours=24)"><dtml-call "RESPONSE.setHeader('Content-type','application/x-javascript')"> /* Are you still using this template? Then you haven't upgraded your StandardHeaderFooter.zpt template probably. */ /* Simple AJAX Code-Kit (SACK) v1.6.1 */ /* ©2005 Gregory Wild-Smith */ /* www.twilightuniverse.com */ /* Software licenced under a modified X11 licence, see documentation or authors website for more details */ function sack(file) { this.xmlhttp = null; this.resetData = function() { this.method = "POST"; this.queryStringSeparator = "?"; this.argumentSeparator = "&"; this.URLString = ""; this.encodeURIString = true; this.execute = false; this.element = null; this.elementObj = null; this.requestFile = file; this.vars = new Object(); this.responseStatus = new Array(2); }; this.resetFunctions = function() { this.onLoading = function() { }; this.onLoaded = function() { }; this.onInteractive = function() { }; this.onCompletion = function() { }; this.onError = function() { }; this.onFail = function() { }; }; this.reset = function() { this.resetFunctions(); this.resetData(); }; this.createAJAX = function() { try { this.xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e1) { try { this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e2) { this.xmlhttp = null; } } if (! this.xmlhttp) { if (typeof XMLHttpRequest != "undefined") { this.xmlhttp = new XMLHttpRequest(); } else { this.failed = true; } } }; this.setVar = function(name, value){ this.vars[name] = Array(value, false); }; this.encVar = function(name, value, returnvars) { if (true == returnvars) { return Array(encodeURIComponent(name), encodeURIComponent(value)); } else { this.vars[encodeURIComponent(name)] = Array(encodeURIComponent(value), true); } } this.processURLString = function(string, encode) { encoded = encodeURIComponent(this.argumentSeparator); regexp = new RegExp(this.argumentSeparator + "|" + encoded); varArray = string.split(regexp); for (i = 0; i < varArray.length; i++){ urlVars = varArray[i].split("="); if (true == encode){ this.encVar(urlVars[0], urlVars[1]); } else { this.setVar(urlVars[0], urlVars[1]); } } } this.createURLString = function(urlstring) { if (this.encodeURIString && this.URLString.length) { this.processURLString(this.URLString, true); } if (urlstring) { if (this.URLString.length) { this.URLString += this.argumentSeparator + urlstring; } else { this.URLString = urlstring; } } // prevents caching of URLString this.setVar("rndval", new Date().getTime()); urlstringtemp = new Array(); for (key in this.vars) { if (false == this.vars[key][1] && true == this.encodeURIString) { encoded = this.encVar(key, this.vars[key][0], true); delete this.vars[key]; this.vars[encoded[0]] = Array(encoded[1], true); key = encoded[0]; } urlstringtemp[urlstringtemp.length] = key + "=" + this.vars[key][0]; } if (urlstring){ this.URLString += this.argumentSeparator + urlstringtemp.join(this.argumentSeparator); } else { this.URLString += urlstringtemp.join(this.argumentSeparator); } } this.runResponse = function() { eval(this.response); } this.runAJAX = function(urlstring) { if (this.failed) { this.onFail(); } else { this.createURLString(urlstring); if (this.element) { this.elementObj = document.getElementById(this.element); } if (this.xmlhttp) { var self = this; if (this.method == "GET") { totalurlstring = this.requestFile + this.queryStringSeparator + this.URLString; this.xmlhttp.open(this.method, totalurlstring, true); } else { this.xmlhttp.open(this.method, this.requestFile, true); try { this.xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") } catch (e) { } } this.xmlhttp.onreadystatechange = function() { switch (self.xmlhttp.readyState) { case 1: self.onLoading(); break; case 2: self.onLoaded(); break; case 3: self.onInteractive(); break; case 4: self.response = self.xmlhttp.responseText; self.responseXML = self.xmlhttp.responseXML; self.responseStatus[0] = self.xmlhttp.status; self.responseStatus[1] = self.xmlhttp.statusText; if (self.execute) { self.runResponse(); } if (self.elementObj) { elemNodeName = self.elementObj.nodeName; elemNodeName.toLowerCase(); if (elemNodeName == "input" || elemNodeName == "select" || elemNodeName == "option" || elemNodeName == "textarea") { self.elementObj.value = self.response; } else { self.elementObj.innerHTML = self.response; } } if (self.responseStatus[0] == "200") { self.onCompletion(); } else { self.onError(); } self.URLString = ""; break; } }; this.xmlhttp.send(this.URLString); } } }; this.reset(); this.createAJAX(); } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/ManagementForm.dtml��������������������������������������������������������0000644�0001750�0001750�00000012366�11012074373�021411� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Management')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <style type="text/css"> div.area { background-color:#efefef; padding:2px 15px; margin:7px; } #userstory { width:50%;left:50%;border:1px solid green; padding:0 8px; background-color:#efefef; } </style> <dtml-var "ManagementTabs('Main')"> <dtml-if "REQUEST.get('userstory')"> <div align="center"> <div id="userstory" align="left"> <a href="<dtml-var "REQUEST['URL1']">/manage_ManagementForm" style="float:right;padding-top:3px;" ><img src="/misc_/IssueTrackerProduct/close.gif" alt="Close annoucement" border="0" /></a> <h3>Dear IssueTrackerProduct administrator,</h3> <p> Please excuse this potentially intrusive message but the IssueTrackerProduct is a free Open Source project that relys heavily on user community participation. <br /> Unless you haven't already done so, please take a few minutes to enter your user story on the <a href="http://www.issuetrackerproduct.com/UserStoryForm" style="font-weight:bold">IssueTrackerProduct.com User Story Form</a> </p> </div> </div> </dtml-if> <form action="<dtml-var getRootURL>/UpdateEverything"> <div class="area"> <p><strong>Update Everything</strong></p> <input type="hidden" name="DestinationURL" value="<dtml-var URL>">   <input type="submit" value="Update Everything"> <p class="form-help"> Deploys standards, Updates catalog and makes sure all objects have the attributes they should have.<br> Press this button whenever you have installed a new version of the IssueTrackerProduct or made any major changes that will effect the ZCatalog such as importing/pasting issues or if you have manually deleted issues in the Zope management interface.<br> It doesn't hurt to press this button. It will only fix things that needs to be fixed. </p> </div> </form> <form action="<dtml-var getRootURL>/ReplaceEmail"> <div class="area"> <p><strong>Replace email address occurance</strong></p> <input type="hidden" name="DestinationURL" value="<dtml-var URL>"> <p>Old: <input name="old" value="" size="30">   New: <input name="new" size="30">  <input type="checkbox" name="casesensitive:int" value="1"> case sensitive <br>   <input type="submit" value="Replace all occurances"> <p class="form-help">Replaces all occurances of a particular email address in issues and issue threads.</p> </div> </form> <script type="text/javascript"><!-- function checkAreYouSure(buttonnid) { var checkbox = document.getElementById('areyousure_toggle'); if (checkbox.checked) { return true; } else { alert("Please check the \"Check to confirm...\" checkbox"); var submitbutton = document.getElementById('submitbutton_toggle'); submitbutton.value = document.getElementById('buttontext').value; return false; } return true; } //--> </script> <dtml-if "manage_canUseBTreeFolder()"> <form action="<dtml-var getRootURL>/" onsubmit="return checkAreYouSure();"> <div class="area"> <p><strong>Use BTreeFolder</strong></p> <dtml-let count="countIssueObjects()"> <dtml-if "manage_isUsingBTreeFolder()"> <p>You are using a BTreeFolder to store your <dtml-var count> issues.</p> <p>If you like you can convert back to storing all issues in the Issue Tracker root<br /> <input type="hidden" name="buttontext" id="buttontext" value="Convert back to plain storage" /> <input type="submit" name="manage_convertFromBTreeFolder:method" id="submitbutton_toggle" value="Convert back to plain storage" onclick="this.value='Please wait...'" onkeypress="this.value='Please wait...'"> <input type="checkbox" name="areyousure:boolean" value="1" id="areyousure_toggle"> Check to confirm that you really want to do this. </p> <dtml-else> <p>You currently have <dtml-var count> issues, <dtml-if "count>=500"> which is quite a lot. Using a BTreeFolder2 can be good for you. <dtml-else> which isn't very much, but you might intend to have many more. </dtml-if> <br> If you want to store all your issues inside a <a href="http://hathawaymix.org/Software/BTreeFolder2">BTreeFolder2</a> instead of your Issue Tracker instance you can do so. The advantage is that BTreeFolders can handle loads of objects all stored in one place and the ZMI isn't slow to use because the list of issues would be too long.<br> The disadvantage is that you will have slightly longer URLs. </p> <p> <input type="hidden" name="buttontext" id="buttontext" value="Convert to using BTreeFolder2" /> <input type="submit" name="manage_convert2BTreeFolder:method" id="submitbutton_toggle" value="Convert to using BTreeFolder2" onclick="this.value='Please wait...'" onkeypress="this.value='Please wait...'"> <input type="checkbox" name="areyousure:boolean" value="1" id="areyousure_toggle"> Check to confirm that you really want to do this. </p> </dtml-if> </p> </dtml-let> </div> </form> </dtml-if> <br />  <dtml-var manage_page_footer> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/dtml/tabtastic-combined.js.dtml�������������������������������������������������0000644�0001750�0001750�00000016673�11012074373�022665� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������//*** This code is copyright 2002-2003 by Gavin Kistner and Refinery; www.refinery.com //*** It is covered under the license viewable at http://phrogz.net/JS/_ReuseLicense.txt //*** Reuse or modification is free provided you abide by the terms of that license. //*** (Including the first two lines above in your source code satisfies the conditions.) // --- cut from addclasskillclass.js ------------------------------------------- //***Adds a new class to an object, preserving existing classes function AddClass(obj,cName){ KillClass(obj,cName); return obj && (obj.className+=(obj.className.length>0?' ':'')+cName); } //***Removes a particular class from an object, preserving other existing classes. function KillClass(obj,cName){ return obj && (obj.className=obj.className.replace(new RegExp("^"+cName+"\\b\\s*|\\s*\\b"+cName+"\\b",'g'),'')); } //***Returns true if the object has the class assigned, false otherwise. function HasClass(obj,cName){ return (!obj || !obj.className)?false:(new RegExp("\\b"+cName+"\\b")).test(obj.className) } // --- cut from attachevent.js ------------------------------------------------- //***Cross browser attach event function. For 'evt' pass a string value with the leading "on" omitted //***e.g. AttachEvent(window,'load',MyFunctionNameWithoutParenthesis,false); function AttachEvent(obj,evt,fnc,useCapture){ if (!useCapture) useCapture=false; if (obj.addEventListener){ obj.addEventListener(evt,fnc,useCapture); return true; } else if (obj.attachEvent) return obj.attachEvent("on"+evt,fnc); else{ MyAttachEvent(obj,evt,fnc); obj['on'+evt]=function(){ MyFireEvent(obj,evt) }; } } //The following are for browsers like NS4 or IE5Mac which don't support either //attachEvent or addEventListener function MyAttachEvent(obj,evt,fnc){ if (!obj.myEvents) obj.myEvents={}; if (!obj.myEvents[evt]) obj.myEvents[evt]=[]; var evts = obj.myEvents[evt]; evts[evts.length]=fnc; } function MyFireEvent(obj,evt){ if (!obj || !obj.myEvents || !obj.myEvents[evt]) return; var evts = obj.myEvents[evt]; for (var i=0,len=evts.length;i<len;i++) evts[i](); } // --- cut from addcss.js ------------------------------------------------------ // Add a new stylesheet to the document; // url [optional] A url to an external stylesheet to use // idx [optional] The index in document.styleSheets to insert the new sheet before function AddStyleSheet(url,idx){ var css,before=null,head=document.getElementsByTagName("head")[0]; if (document.createElement){ if (url){ css = document.createElement('link'); css.rel = 'stylesheet'; css.href = url; } else css = document.createElement('style'); css.media = 'all'; css.type = 'text/css'; if (idx>=0){ for (var i=0,ct=0,len=head.childNodes.length;i<len;i++){ var el = head.childNodes[i]; if (!el.tagName) continue; var tagName = el.tagName.toLowerCase(); if (ct==idx){ before = el; break; } if (tagName=='style' || tagName=='link' && (el.rel && el.rel.toLowerCase()=='stylesheet' || el.type && el.type.toLowerCase()=='text/css') ) ct++; } } head.insertBefore(css,before); return document.styleSheets[before?idx:document.styleSheets.length-1]; } else return alert("I can't create a new stylesheet for you. Sorry."); } // e.g. var newBlankSheetAfterAllOthers = AddStyleSheet(); // e.g. var newBlankSheetBeforeAllOthers = AddStyleSheet(null,0); // e.g. var externalSheetAfterOthers = AddStyleSheet('http://phrogz.net/JS/Classes/docs.css'); // e.g. var externalSheetBeforeOthers = AddStyleSheet('http://phrogz.net/JS/Classes/docs.css',0); // Cross-browser method for inserting a new rule into an existing stylesheet. // ss - The stylesheet to stick the new rule in // selector - The string value to use for the rule selector // styles - The string styles to use with the rule function AddRule(ss,selector,styles){ if (!ss) return false; if (ss.insertRule) return ss.insertRule(selector+' {'+styles+'}',ss.cssRules.length); if (ss.addRule){ ss.addRule(selector,styles); return true; } return false; } // e.g. AddRule( document.styleSheets[0] , 'a:link' , 'color:blue; text-decoration:underline' ); // e.g. AddRule( AddStyleSheet() , 'hr' , 'display:none' ); // --- cut from tabtastic.js --------------------------------------------------- //*** Tabtastic -- see http://phrogz.net/JS/Tabstatic/index.html //*** Version 1.0 20040430 Initial release. //*** 1.0.2 20040501 IE5Mac, IE6Win compat. //*** 1.0.3 20040501 Removed IE5Mac/Opera7 compat. (see http://phrogz.net/JS/Tabstatic/index.html#notes) //*** 1.0.4 20040521 Added scroll-back hack to prevent scrolling down to page anchor. Then commented out :) AttachEvent(window,'load',function(){ var tocTag='ul',tocClass='tabset_tabs',tabTag='a',contentClass='tabset_content'; function FindEl(tagName,evt){ if (!evt && window.event) evt=event; if (!evt) return DebugOut("Can't find an event to handle in DLTabSet::SetTab",0); var el=evt.currentTarget || evt.srcElement; while (el && (!el.tagName || el.tagName.toLowerCase()!=tagName)) el=el.parentNode; return el; } function SetTabActive(tab){ if (tab.tabTOC.activeTab){ if (tab.tabTOC.activeTab==tab) return; KillClass(tab.tabTOC.activeTab,'active'); if (tab.tabTOC.activeTab.tabContent) KillClass(tab.tabTOC.activeTab.tabContent,'tabset_content_active'); //if (tab.tabTOC.activeTab.tabContent) tab.tabTOC.activeTab.tabContent.style.display=''; if (tab.tabTOC.activeTab.prevTab) KillClass(tab.tabTOC.activeTab.previousTab,'preActive'); if (tab.tabTOC.activeTab.nextTab) KillClass(tab.tabTOC.activeTab.nextTab,'postActive'); } AddClass(tab.tabTOC.activeTab=tab,'active'); if (tab.tabContent) AddClass(tab.tabContent,'tabset_content_active'); //if (tab.tabContent) tab.tabContent.style.display='block'; if (tab.prevTab) AddClass(tab.prevTab,'preActive'); if (tab.nextTab) AddClass(tab.nextTab,'postActive'); } function SetTabFromAnchor(evt){ //setTimeout('document.documentElement.scrollTop='+document.documentElement.scrollTop,1); SetTabActive(FindEl('a',evt).semanticTab); } function Init(){ window.everyTabThereIsById = {}; var anchorMatch = /#([a-z][\w.:-]*)$/i,match; var activeTabs = []; var tocs = document.getElementsByTagName(tocTag); for (var i=0,len=tocs.length;i<len;i++){ var toc = tocs[i]; if (!HasClass(toc,tocClass)) continue; var lastTab; var tabs = toc.getElementsByTagName(tabTag); for (var j=0,len2=tabs.length;j<len2;j++){ var tab = tabs[j]; if (!tab.href || !(match=anchorMatch.exec(tab.href))) continue; if (lastTab){ tab.prevTab=lastTab; lastTab.nextTab=tab; } tab.tabTOC=toc; everyTabThereIsById[tab.tabID=match[1]]=tab; tab.tabContent = document.getElementById(tab.tabID); if (HasClass(tab,'active')) activeTabs[activeTabs.length]=tab; lastTab=tab; } AddClass(toc.getElementsByTagName('li')[0],'firstchild'); } for (var i=0,len=activeTabs.length;i<len;i++){ SetTabActive(activeTabs[i]); } for (var i=0,len=document.links.length;i<len;i++){ var a = document.links[i]; if (!(match=anchorMatch.exec(a.href))) continue; if (a.semanticTab = everyTabThereIsById[match[1]]) AttachEvent(a,'click',SetTabFromAnchor,false); } if ((match=anchorMatch.exec(location.href)) && (a=everyTabThereIsById[match[1]])) SetTabActive(a); //Comment out the next line and include the file directly if you need IE5Mac or Opera7 support. //AddStyleSheet('tabtastic.css',0); } Init(); },false); ���������������������������������������������������������������������IssueTrackerProduct/dtml/editPOP3PropertiesForm.dtml������������������������������������������������0000644�0001750�0001750�00000002042�11012074373�022767� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<dtml-var manage_page_header> <dtml-with "_(management_view='Properties')"> <dtml-var manage_tabs> </dtml-with> <dtml-if Principia-Version> <p> <em>You are currently working in version <dtml-var Principia-Version> </em> </p> </dtml-if Principia-Version> <p class="form-title">IssueTracker POP3 Settings</p> <form action="manage_savePOP3Properties"> Hostname: <input name="pop3_hostname" value="<dtml-var "getPOP3Host(default='')">"><br> Username: <input name="pop3_username" value="<dtml-var "getPOP3Username(default='')">"><br> Password: <input name="pop3_password" value="<dtml-if pop3_password><dtml-if "pop3_password!=''">password</dtml-if></dtml-if>"><br> Confirm: <input name="pop3_confirm" value="<dtml-if pop3_password><dtml-if "pop3_password!=''">pconfirm</dtml-if></dtml-if>"><br> Email addresses: <textarea name="pop3_email_addresses:list" rows="6" cols="35"><dtml-in "_.string.join(getPOP3AcceptEmails(default=[]), '\n')"></textarea> <br><br> <input type="submit" value="Save"> </form> <dtml-var manage_page_footer> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/Utils.py������������������������������������������������������������������������0000755�0001750�0001750�00000127571�11012074373�016351� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: iso-8859-1 -* # Peter Bengtsson <mail@peterbe.com> # Utility scripts for IssueTrackerProduct # # python import string from string import translate, maketrans import sys, re, os import codecs from random import shuffle from math import floor from htmlentitydefs import entitydefs import warnings # import line used by textify import formatter, htmllib, StringIO entitydefs_inverted = {} for k,v in entitydefs.items(): entitydefs_inverted[v] = k # zope from Products.PythonScripts.standard import html_quote, newline_to_br, \ url_quote, url_quote_plus from StructuredText.StructuredText import HTML def structured_text(txt): return HTML(txt, level=int(os.environ.get('STX_DEFAULT_LEVEL',3)), header=0) from addhrefs import addhrefs, improveURL, __version__ as addhrefs_version _major, _minor = addhrefs_version.split('.')[:2] try: _major = int(_major) except ValueError: pass try: _minor = int(_minor) except ValueError: pass assert _minor >= 6, "You don't have the latest addhrefs.py module" try: import itertools def anyTrue(pred, seq): return True in itertools.imap(pred,seq) except ImportError: def anyTrue(pred, seq): for e in seq: if pred(e): return True return False from Constants import UNICODE_ENCODING def unicodify(s, encodings=(UNICODE_ENCODING, 'latin1', 'utf8')): if isinstance(s, str): if not isinstance(encodings, (tuple, list)): encodings = [encodings] for encoding in encodings: try: return unicode(s, encoding) except UnicodeDecodeError: pass raise UnicodeDecodeError, \ "Unable to unicodify %r with these encodings %s" % (s, encodings) return s def SimpleTextPurifier(text): text = text.replace('<p> </p>','') text = text.replace(' ',' ') text = textify(text) return text.strip() def highlightCarefully(word, text, highlightfunction, word_boundary=True): """ return the word carefully highlighted using highlightfunction in a text. The word is only matched if wordsplitted on both sides. The highlightfunction (eg. hlf()) can be something simple like:: hlf = lambda x: '<span>%s</span>'%x By carefully we mean that the highlighting shouldn't need to be done if the word found is inside a HTML tag. If this is the word: 'bug' and this is the text: I saw a bug on <a href=# title="a bug">this page</a> then this is the expected result:: I saw a <span>bug</span> on <a href=# title="a bug">this page</a> """ if word_boundary: regex = re.compile(r'\b(%s)\b' % re.escape(word), re.I) else: regex = re.compile(r'(%s)' % re.escape(word), re.I) def matchTester(match): before = text[:match.start()] if before.rfind('<') > before.rfind('>'): return match.group(1) else: return highlightfunction(match.group(1)) return regex.sub(matchTester, text) def filenameSplitter(filename): """ return a list of parts of the filename. The whole filename is always included first in the list of parts. The parts are splitted from the filename by caMel notation, [._-] and digits. see http://www.peterbe.com/plog/filename-splitter """ def _cleanSplit(splitted): splitted = [x for x in splitted if len(x) > 1 or x.isdigit()] for i in range(len(splitted)): if splitted[i][0] == '.': splitted.append(splitted[i][1:]) if splitted[i][0] in ('-','_'): splitted[i]=splitted[i][1:] if splitted[i][-1] in ('.','-','_'): splitted[i]=splitted[i][:-1] return splitted keys = [filename] camel_regex = re.compile('([A-Z][a-z0-9]+)') keys.extend(_cleanSplit(camel_regex.split(filename))) for point in ('\.','_','-','\d+','\s+'): keys.extend(_cleanSplit(re.compile('(%s)'%point).split(filename))) return uniqify(keys) def textify(html_snippet): return re.sub('<.*?>', '', html_snippet) ##def textify(html_snippet, maxwords=None): ## """ Thank you Fredrik Lundh ## http://online.effbot.org/2003_08_01_archive.htm#20030811 ## """ ## ## ## class Parser(htmllib.HTMLParser): ## def anchor_end(self): ## self.anchor = None ## ## class Formatter(formatter.AbstractFormatter): ## pass ## ## class Writer(formatter.DumbWriter): ## def send_label_data(self, data): ## self.send_flowing_data(data) ## self.send_flowing_data(" ") ## ## o = StringIO.StringIO() ## p = Parser(Formatter(Writer(o))) ## p.feed(html_snippet) ## p.close() ## ## words = o.getvalue().split() ## ## ## if maxwords: ## if len(words) <= 2*maxwords: ## return string.join(words) ## ## return string.join(words[:maxwords]) + " ..." ## else: ## return string.join(words) # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/436833 def printu(ustr): print ustr.encode('raw_unicode_escape') # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/436833 def saveu(ustr, filename='output.txt'): file(filename,'wb').write(codecs.BOM_UTF8 + ustr.encode('utf8')) def parseFlowFormattedResult(flow): """ return (replytext, origtext) """ replylines = [] origlines = [] for e in flow: if e[0]['quotedepth']==0: replylines.append(e[1].strip()) elif e[0]['quotedepth']==1: origlines.append(e[1].strip()) while '' in replylines: replylines.remove('') while '' in origlines: origlines.remove('') replytext = '\n'.join(replylines) origtext = '\n'.join(origlines) return replytext, origtext _bad_file_id = re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# ]') def badIdFilter(fileid, replacement=''): for badchar in _bad_file_id.findall(fileid): fileid = fileid.replace(badchar, replacement) return fileid def getFindIssueLinksRegex(zfill, trackerids=None, prefix=None): """ return a compiled regular expression that can be used to find references to other issues in a text TODO: When it starts a string eg. '#1234 bla bla' doesn't work because the \b gadget doesn't seem to work in conjunction with '\b#'. If I put the regex like '^<regex>|<regex>' it does find with or without starting the string but returns an inconvenient regex pattern which is clumpsy. Investigate how to properly (alternative to using ^) find the start of the string. Either that or find out how to use \b together with a # symbol. """ assert isinstance(zfill, int), "zfill param must be integer" _use_I_ = False if prefix: if re.findall('[^\d]', prefix): # the prefix contains non-numericals, use case insensitive # search _use_I_ = True regex = r'#%s%s|#%s' % (prefix, '\d'*zfill, '\d'*zfill) else: regex = r'#%s|\B#[1-9][0-9]{0,%s}' % (('\d'*zfill), zfill-1) if trackerids: _use_I_ = True if isinstance(trackerids, basestring): trackerids = [trackerids] _inner = [] for trackerid in trackerids: _inner.append(r'\b%s%s' % (trackerid, regex)) regex = r'(%s|\B%s)\b' % ('|'.join(_inner), regex) else: regex = r'\B(%s)\b' % regex if _use_I_: return re.compile(regex, re.I) else: return re.compile(regex) def sum(seq): return reduce(lambda x,y: x+y, seq) def splitTerms(s): """ if s = 'peter "anders bengt" bengtsson' return ['peter','"anders bengt"','bengtsson'] """ words = [] if s.count('"') % 2 or s.count('"')==0: # the quotes don't balance out, pointless doing this return [x.strip() for x in s.split()] in_quote = None for word in s.split(' '): if word.count('"') % 2: if in_quote is None: in_quote = [word] else: in_quote.append(word) elif in_quote: words.append(' '.join(in_quote)) in_quote = None words.append(word) else: words.append(word) if in_quote: words.append(' '.join(in_quote)) return words r= re.compile('".*?"') wordcomps = r.findall(s) for wordcomp in wordcomps: words.append(wordcomp[1:-1]) s = s.replace(wordcomp, '') return words def createStandaloneWordRegex(word): """ return a regular expression that can find 'peter' only if it's written alone (next to space, start of string, end of string, comma, etc) but not if inside another word like peterbe """ return re.compile(r'\b%s\b' % word, re.I) def dict_popper(dict, key): """ simulate what {}.pop() does in Python 2.3 """ if not dict: raise KeyError, 'dict_popper(): dictionary is empty' if not dict.has_key(key): raise KeyError, repr(key) new_dict = {} for k, v in dict.items(): if k == key: value = v else: new_dict[k] = v return value, new_dict hex_entity_regex = re.compile('&#\d+;') def AwareLengthLimit(string, maxsize=50, append='...'): """ instead of just chopping off a bit of a string we do it more carefully. """ count = 0 maxsize_orig = maxsize for each in hex_entity_regex.findall(string): count += 1 if count > maxsize_orig: break maxsize += len(each) - 1 if len(string) > maxsize: try: if string[maxsize-1:maxsize+1] == '&#': maxsize += 1 except IndexError: pass shortened = string[:maxsize] # avoid this 'Ӓऩř'. Rather have 'Ӓऩ' if shortened.rfind('&#') > shortened.rfind(';'): shortened = shortened[:shortened.rfind('&#')] return shortened.rstrip() + append else: return string def tag_quote(text): """ similiar to html_quote but only fix < and > """ return text.replace('<','<').replace('>','>') destroyed_hex_entities = re.compile('&#(\d+);') def safe_html_quote(text): """ like html_quote but allow things like Ӓ """ text = html_quote(text) text = destroyed_hex_entities.sub(r'&#\1;', text) return text def highlight_signature(text, attribute='style="color:#ccc"', tag="span", use_newline_to_br=False): text = text.replace('<p>--\n','<p>\n--\n') signature_regex = re.compile('(^--\s*(\n|<br\s*/>|<br>))', re.MULTILINE|re.I) found_signatures = signature_regex.findall(text) if found_signatures: whole, linebreak = found_signatures[-1] parts = text.split(whole) signature = parts[-1] if use_newline_to_br: whole = newline_to_br(whole) signature = newline_to_br(signature) double_endbreaks = re.compile(r'<br\s*/><br\s*/>$', re.M) whole = double_endbreaks.sub('<br />', whole) signature = double_endbreaks.sub('<br />', signature) whitespace = signature.replace(signature.strip(), '') signature = '%s<%s %s>%s%s</%s>'%(whitespace, tag, attribute, whole, signature.strip(), tag) return whole.join(parts[:-1])+signature else: signature_regex = re.compile('(^|<p>)(--\s*)', re.MULTILINE|re.I) splitted = signature_regex.split(text) if len(splitted)==4: part1, start, dashes, part2 = splitted part1 += start if start =='<p>': _p_splitted = part2.split('</p>') joined = '%s<%s %s>'%(part1, tag, attribute) joined += '%s%s</%s>%s</p>'%(dashes, _p_splitted[0], tag, _p_splitted[1]) else: joined = '%s<%s %s>%s%s</%s>'%(part1, tag, attribute, dashes, part2, tag) return joined return text def niceboolean(value): falseness = ('','no','off','false','none','0', 'f') return str(value).lower().strip() not in falseness _badchars_regex = re.compile('|'.join(entitydefs.values())) _been_fixed_regex = re.compile('&\w+;|&#[0-9]+;') def html_entity_fixer(text, skipchars=[], extra_careful=1): """ return a text properly html fixed """ if not text: # then don't even begin to try to do anything return text # if extra_careful we don't attempt to do anything to # the string if it might have been converted already. if extra_careful and _been_fixed_regex.findall(text): return text if isinstance(skipchars, basestring): skipchars = [skipchars] keyholder= {} for x in _badchars_regex.findall(text): if x not in skipchars: keyholder[x] = 1 text = text.replace('&','&') text = text.replace(u'\x80', '€') for each in keyholder.keys(): if each == '&': continue try: better = entitydefs_inverted[each] if not better.startswith('&#'): better = '&%s;'%entitydefs_inverted[each] except KeyError: continue text = text.replace(each, better) return text def uniqify(seq, idfun=None): # Alex Martelli ******* order preserving if idfun is None: def idfun(x): return x seen = {} result = [] for item in seq: marker = idfun(item) # in old Python versions: # if seen.has_key(marker) # but in new ones: ##if marker in seen: continue if seen.has_key(marker): continue seen[marker] = 1 result.append(item) return result def iuniqify(seq): """ return a list of strings unique case insensitively. If the input is ['foo','bar','Foo'] return ['foo','bar'] """ def idfunction(x): if isinstance(x, basestring): return x.lower() else: return x return uniqify(seq, idfunction) def moveUpListelement(element, xlist): """ move an element in a _mutable_ list up one position if possible. If the element is a list, then the function is self recursivly called for each subelement. """ assert type(xlist)==type([]), "List to change not of list type "\ "(%r)"%type(xlist) if type(element)==type([]): for subelement in element: moveUpListelement(subelement, xlist) if element==xlist[0]: pass elif element in xlist: i=xlist.index(element) xlist[i], xlist[i-1] = xlist[i-1], xlist[i] def encodeEmailString(email, title=None, nolink=0): """ just write the email like <span class="aeh">peter_AT_peterbe_._com</span> and a 'onLoad' Javascript will convert it to look nice. The way we show the email address must match the Javascript that converts it on the fly. """ methods = ['_dot_','%20dot%20', '_._'] shuffle(methods) # replace . after @ if '@' in list(email): afterbit = email.split('@')[1] newbit = afterbit.replace('.', methods[0]) email = email.replace(afterbit, newbit) methods = ['_', '~'] shuffle(methods) atsigns = ['at','AT'] shuffle(atsigns) # replace @ with *AT* email = email.replace('@','%s%s%s'%(methods[0], atsigns[0], methods[0])) if title is None or title == email: title = email spantag = '<span class="aeh">%s</span>' spantag_link = '<a class="aeh" href="mailto:%s">%s</a>' if nolink: return spantag % email else: return spantag_link % (email, title) def safebool(value): try: return not not int(value) except ValueError: return 0 def XXX_NO_LONGER_USED_encodeEmailString(email, title=None, nolink=0): """ if encode_emaildisplay then use JavaScript to encode it """ if title is None: title = email basic = email.replace('@','(at)').replace('.',' dot ') if title != email: basic = "%s, %s"%(title, basic) if nolink: js_string = """document.write('%s')"""%email else: js_string = """document.write('<a href="mailto:%s">"""%email js_string += """%s</a>')"""%title hexed = _hex_string(js_string) js_script = """<script language="JavaScript" type="text/javascript">eval(unescape('""" js_script += hexed + """'))</script>""" js_script += "<noscript>%s</noscript>"%basic return js_script def _hex_string(oldstring): """ hexify a string """ # Taken from http://www.happysnax.com.au/testemail.php # See Credits def _tohex(n): hs='0123456789ABCDEF' return hs[int(floor(n/16))]+hs[n%16] newstring='' length=len(oldstring) for i in range(length): newstring=newstring+'%'+_tohex(ord(oldstring[i])) return newstring def same_type(one, two): """ use this because 'type' as variable can be used elsewhere """ warnings.warn("use isinstance(object, type) instead", DeprecationWarning, 2) return type(one)==type(two) def safeId(id, nospaces=0): """ Just make sure it contains no dodgy characters """ lowercase = 'abcdefghijklmnopqrstuvwxyz' digits = '0123456789' specials = '_-.' allowed = lowercase + lowercase.upper() + digits + specials if not nospaces: allowed = ' ' + allowed n_id=[] allowed_list = list(allowed) for letter in list(id): if letter in allowed_list: n_id.append(letter) return ''.join(n_id) def ShowFilesize(bytes): """ Return nice representation of size """ if bytes < 1024: return "1 Kb" elif bytes > 1048576: mb_bytes = '%0.02f'%(bytes / 1048576.0) return "%s Mb"%mb_bytes else: return "%s Kb"%int(bytes / 1024) def preParseEmailString(es, names2emails={}, aslist=0): """ Take any string and strip out only a string of email addresses delimited by 'sep' "Peter <mail@peterbe.com>, John;peter@grenna.net, Joe" => "mail@peterbe.com; peter@grenna.net" But suppose names2email={'joe','joey@host.com') then you would expect: "mail@peterbe.com; peter@grenna.net; joey@host.com" Bare in mind that names2emails can have values that are lists, like this: {'group: Friends':['foo@bar.com',...]} """ sep = ',' real_emails=[] #es = es.replace(';',' ').replace(',',' ') # first remove any junk es = es.replace(';',sep) es = es.replace('>',' ').replace('<',' ') if isinstance(es, str): # transtab's don't work with unicode transtab = maketrans('/ ',' ') es = translate(es, transtab, '?&!()<=>*#[]{}') # fix so that, the keys are lower case and 'group:' gone n2e = {} for k, v in names2emails.items(): n2e[k.lower().replace('group:', '').strip()] = v grand_list = [] for chunk in es.split(sep): subchunks = [] _found_one_valid = 0 for subchunk in chunk.split(): # by space subchunks.append(subchunk) if ValidEmailAddress(subchunk): grand_list.append(subchunk) _found_one_valid = 1 break if not _found_one_valid: grand_list.extend(subchunks) grand_list.append(chunk) # expand the names2emails for item in grand_list[:]: if n2e.has_key(ss(item)): value = n2e.get(ss(item)) if isinstance(value, list): for each in value: if each: grand_list.append(each.strip()) elif value: grand_list.append(value) # uniqify grand_list = uniqify(grand_list) # filter on valid email address real_emails = [] mentioned = [] for e in grand_list: if '@' in e and ValidEmailAddress(e) and ss(e) not in mentioned: real_emails.append(e) mentioned.append(ss(e)) if real_emails: if aslist: return real_emails else: return sep.join(real_emails) else: if aslist: return [] else: return None def AddParam2URL(url, params={}, unicode_encoding='utf8', plus_quote=False, **kwargs): """ return url and append params but be aware of existing params """ if plus_quote: url_quoter = url_quote_plus else: url_quoter = url_quote if kwargs: params.update(kwargs) p='?' if p in url: p = '&' url = url + p for key, value in params.items(): if isinstance(value, (list, tuple)): for e in value: if isinstance(e, unicode): e = e.encode(unicode_encoding) url += '%s=%s&'%(key, url_quoter(e)) else: if isinstance(value, unicode): value = value.encode(unicode_encoding) url += '%s=%s&'%(key, url_quoter(value)) return url[:-1] def fixDictofLists(dict): " throw it a dictionary and it returns the values lowercased " for key,value in dict.items(): if isinstance(value, list): dict[key] = _lowercaseList(value) elif isinstance(value, basestring): dict[key] = value.lower() return dict def _lowercaseList(lst): " lowercase and strip all items in a list " return [ss(x) for x in lst] def getRandomString(length=10, loweronly=1, numbersonly=0): """ return a very random string """ if numbersonly: l = list('0123456789') else: lowercase = 'abcdefghijklmnopqrstuvwxyz'+'0123456789' l = list(lowercase + lowercase.upper()) shuffle(l) s = string.join(l,'') if len(s) < length: s = s + getRandomString(loweronly=1) s = s[:length] if loweronly: return s.lower() else: return s # Language constants MINUTE = 'minute' MINUTES = 'minutes' HOUR = 'hour' HOURS = 'hours' YEAR = 'year' YEARS = 'years' MONTH = 'month' MONTHS = 'months' WEEK = 'week' WEEKS = 'weeks' DAY = 'day' DAYS = 'days' AND = 'and' def timeSince(firstdate, seconddate, afterword=None, minute_granularity=False, max_no_sections=3): """ Use two date objects to return in plain english the difference between them. E.g. "3 years and 2 days" or "1 year and 3 months and 1 day" Try to use weeks when the no. of days > 7 If less than 1 day, return number of hours. If there is "no difference" between them, return false. """ def wrap_afterword(result, afterword=afterword): if afterword is not None: return "%s %s" % (result, afterword) else: return result fdo = firstdate sdo = seconddate day_difference = int(abs(sdo-fdo)) years = day_difference/365 months = (day_difference % 365)/30 days = (day_difference % 365) % 30 minutes = ((day_difference % 365) % 30) % 24 if days == 0 and months == 0 and years == 0: # use hours hours=int(round(abs(sdo-fdo)*24, 2)) if hours == 1: return wrap_afterword("1 %s" % (HOUR)) elif hours > 0: return wrap_afterword("%s %s" % (hours, HOURS)) elif minute_granularity: minutes = int(round(abs(sdo-fdo) * 24 * 60, 3)) if minutes == 1: return wrap_afterword("1 %s" % MINUTE) elif minutes > 0: return wrap_afterword("%s %s" % (minutes, MINUTES)) else: # if the differnce is smaller than 1 minute, # return 0. return 0 else: # if the difference is smaller than 1 hour, # return it false return 0 else: s = [] if years == 1: s.append('1 %s'%(YEAR)) elif years > 1: s.append('%s %s'%(years,YEARS)) if months == 1: s.append('1 %s'%MONTH) elif months > 1: s.append('%s %s'%(months,MONTHS)) if days == 1: s.append('1 %s'%DAY) elif days == 7: s.append('1 %s'%WEEK) elif days == 14: s.append('2 %s'%WEEKS) elif days == 21: s.append('3 %s'%WEEKS) elif days > 14: weeks = days / 7 days = days % 7 if weeks == 1: s.append('1 %s'%WEEK) else: s.append('%s %s'%(weeks, WEEKS)) if days % 7 == 1: s.append('1 %s'%DAY) elif days > 0: s.append('%s %s'%(days % 7,DAYS)) elif days > 1: s.append('%s %s'%(days,DAYS)) s = s[:max_no_sections] if len(s)>1: return wrap_afterword("%s" % (string.join(s,' %s '%AND))) else: return wrap_afterword("%s" % s[0]) def cookIdAndTitle(s): """ if s='image1~Image One.gif' then return ['image1.gif','Image One'] Testwork: s='image1~Image One.gif' => ['image1.gif','Image One'] s='image1.gif' => ['image1.gif',''] s='image1~.gif' => ['image1.gif',''] s='image1~Image One.gif.gif'=> ['image1.gif','Image One.gif'] """ if s.find('~') == -1: return s, '' splitted = s.split('~',1) id = splitted[0] rest = s.replace(id+'~','') if rest.rfind('.') == -1: return id, rest else: pos_last_dot = rest.rfind('.') ext = rest[pos_last_dot:] id = id.strip() + ext.strip() title = rest[0:pos_last_dot].strip() return id, title def LineIndent(text, indent, maxwidth=None): """ indent each new line with 'indent' """ if maxwidth: parts = [] for part in text.split('\n'): words = part.split(' ') lines = [] tmpline = '' for word in words: if len(tmpline+' '+word) > maxwidth: lines.append(tmpline.strip()) tmpline = word else: tmpline += ' ' + word lines.append(tmpline.strip()) start = "\n%s"%indent parts.append(indent + start.join(lines)) return "\n".join(parts) else: text = indent+text text = text.replace('\n','\n%s'%indent) return text def ShowDescription(text, display_format='', emaillinkfunction=None, urllinkfunction=None): """ Display text, using harmless HTML """ if not text: # blank or None return "" if urllinkfunction is None: # add one that is able to truncate really long URLs def urllinkfunction(url, maxlength=80): if len(url) > maxlength: title = url[:47] + '...' + url[-30:] tooltip = 'Right click to copy the whole URL' return '<a href="%s" title="%s">%s</a>' % \ (improveURL(url), tooltip, title) else: return '<a href="%s">%s</a>' % (improveURL(url), url) if display_format == 'structuredtext': #st=_replace_special_chars(text) st=text # if the text is just a number (and a full stop), then # structured_text is going to make this the first of a numbered # HTML list. Prevent that with this "hack". found_only_number = re.compile('\d[\d \.]+').findall(st) if found_only_number: if found_only_number[0] == st: return st for k,v in {'<':'<', '>':'>', '[':'|[|'}.items(): st = st.replace(k,v) if isinstance(st, str): st = html_entity_fixer(st, skipchars=('"',)) st = structured_text(st) for k,v in {'&lt;':'<', '&gt;':'>', '|[|':'['}.items(): st = st.replace(k,v) st = addhrefs(st, emaillinkfunction=emaillinkfunction, urllinkfunction=urllinkfunction) return st elif display_format == 'html': return text else: t = '<p>%s</p>'%safe_html_quote(text) t = t.replace('&lt;','<').replace('&gt;','>') t = addhrefs(t, emaillinkfunction=emaillinkfunction, urllinkfunction=urllinkfunction) t = newline_to_br(t) return t def _replace_special_chars(text, simplify=1, html_encoding=0): """ Replace special characters with placeholder keys back and forth. The reason for doing this is that structured_text() doesn't support special characters such as umlats. """ # THIS NEEDS WORK return text #if simplify: #for k, v in reps.items(): #if html_encoding: #k='&%s;'%k #else: #k='__%s__'%k #text = text.replace(v,k) #else: #for k, v in reps.items(): #k='__%s__'%k #text = text.replace(k,v) #return text #def getNowdate(): # """ # This method determines the format # with which new objects get a date property # """ # return DateTime() def _ShouldBeNone(result): return result is not None def _ShouldNotBeNone(result): return result is None tests = ( # Thank you Bruce Eckels for these (some modifications by Peterbe) (re.compile("^[0-9a-zA-Z\.\'\+\-\_]+\@[0-9a-zA-Z\.\-\_]+$"), _ShouldNotBeNone, "Failed a"), (re.compile("^[^0-9a-zA-Z]|[^0-9a-zA-Z]$"), _ShouldBeNone, "Failed b"), (re.compile("([0-9a-zA-Z\_]{1})\@."), _ShouldNotBeNone, "Failed c"), (re.compile(".\@([0-9a-zA-Z]{1})"), _ShouldNotBeNone, "Failed d"), (re.compile(".\.\-.|.\-\..|.\.\..|.\-\-."), _ShouldBeNone, "Failed e"), (re.compile(".\.\_.|.\-\_.|.\_\..|.\_\-.|.\_\_."), _ShouldBeNone, "Failed f"), (re.compile(".\.([a-zA-Z]{2,3})$|.\.([a-zA-Z]{2,4})$"), _ShouldNotBeNone, "Failed g"), # no underscore just left of @ sign or _ after the @ (re.compile("\_@|@[a-zA-Z0-9\-\.]*\_"), _ShouldBeNone, "Failed h"), ) def ValidEmailAddress(address, debug=None): for test in tests: if test[1](test[0].search(address)): if debug: return test[2] return 0 return 1 def ss(s): """ simple string """ return s.strip().lower() def test(): print "----------\n" print "TEST AddParam2URL()" print AddParam2URL('http://www.peterbe.com') print AddParam2URL('http://www.peterbe.com',{'a':'A','b':'B'}) message="""Line1 Line2""" print AddParam2URL('http://www.peterbe.com?o=O',{'a':message,'b':'B spaced'}) print "----------\n" print "TEST preParseEmailString()" print preParseEmailString("mail@peterbe.com;JOE,peter@grenna.net,Peter Bengtsson<ppp@ppp.se>; Peter <pete@peterbe.com", \ names2emails={'Joe':'joey@peterbe.com', 'Peter bengtsson':'ppp@ppp.se'}) print preParseEmailString("mail@ ", \ names2emails={'Joe':'joey@peterbe.com', 'Peter bengtsson':'ppp@ppp.se'}) print " ++ cookIdAndTitle() ++ " print cookIdAndTitle('image1~Image One.gif'), " AND ['image1.gif','Image One']" print cookIdAndTitle('image1.gif'), " AND ['image1.gif','']" print cookIdAndTitle('image1~.gif'), " AND ['image1.gif','']" print cookIdAndTitle('image1~Image One.gif.gif'), " AND ['image1.gif','Image One.gif']" print cookIdAndTitle('monk ~ Monkey.dtml'), " AND ['monk.dtml','Monkey']" print " ++ ValidEmailAddress() ++ " print ValidEmailAddress(''), " AND 0" print ValidEmailAddress('issuetracker@peterbe.com'), " AND 1" print ValidEmailAddress('issuetracker @peterbe.com'), " AND 0" print " ++ getRandomString() ++ " print getRandomString(), " " print getRandomString(length=50), " AND (length=50)" print getRandomString(loweronly=1), " AND (loweronly=1)" def benchmark_ShowDescription(): from time import time text = open('text.txt','r').read() sections = text.split('-'*20) sections = [x.strip() for x in sections] print len(sections) total_time = 0 total_count = 0 for i in range(2): for section in sections: t0 = time() stx = ShowDescription(section, 'structuredtext') t1 = time()-t0 total_time += t1 total_count += 1 print "Average time:", total_time/total_count def test__preParseEmailString(): def T(s, d={}, expect=None): if d: print "From: %r, with %r"%(s,d) r = preParseEmailString(s, names2emails=d, aslist=1) print "To: %r" % r else: print "From: %r"%s r = preParseEmailString(s, aslist=1) print "To: %r" % r if expect is not None: assert r == expect, "Not what we expected" print es = 'mail@peterbe.com, peter@grenna.net' # T(es) es = 'mail@peterbe.com, will@.not.work' T(es, expect=['mail@peterbe.com']) es = 'Mail mail@peterbe.com; Peter peter@grenna.net, Sven <sven@peterbe.com>; SVEN@Peterbe.com ,' T(es, expect=['mail@peterbe.com', 'peter@grenna.net', 'sven@peterbe.com']) es = '' T(es, expect=[]) es = 'peter; jOhn' d = {'JOHN':'John@peterbe.com', 'peter be':'PeterBe@peterbe.com'} T(es, d, expect=['John@peterbe.com']) es = 'peter <peter@grena..net>, john <john@ok.com>; group: Friends' d['group:friends'] = ['abc@def.com',' PETER@grenna.NET'] T(es, d, expect=['john@ok.com', 'abc@def.com', 'PETER@grenna.NET']) es = 'peter <peter@grena..net>, john <john@ok.com>;GROUP: Friends' d['friends'] = ['abc@def.com',' PETER@grenna.NET'] T(es, d, expect=['john@ok.com','abc@def.com','PETER@grenna.NET']) # ------------ es ='Ed Leafe, Ed Leafe' names2emails={'Ed Leafe': 'Ed.Leafe@peterbe.com', 'Ed Leafe, Ed Leafe': 'Ed.Leafe@peterbe.com', 'Ed Leafe (Ed Leafe)': 'Ed.Leafe@peterbe.com'} T(es, names2emails, expect=['Ed.Leafe@peterbe.com']) def test__addhrefs(): br='-'*78+'\n' print br t="this some text http://www.peterbe.com/ with links www.peterbe.com in it" # print addhrefs(t) # print br t='this <a href="http://www.google.com">some</a> text http://www.peterbe.com/ '\ 'with links www.peterbe.com in it '\ '<a href="http://www.example.com">Example</a>' print addhrefs(t) print br # t='this <a href="http://www.google.com">some</a> text '\ # "http://www.peterbe.com/ with links www.peterbe.com in it "\ # '<a href="http://www.example.com">Example</a>' # print addhrefs(t) # print br t="this some text http://www.peterbe.com/ with links www.peterbe.com in it" # print addhrefs(t) # print br t='Starts <a href="bajs.com">bajs.com</a> '\ "this some text http://www.peterbe.com/ with links www.peterbe.com in it "\ '<a href="http://www.example.com">Example</a>' # print addhrefs(t) #print br def test__LineIndent(): t='''There seems to be a problem with paragraphs that are long and multiple. Thanks for taking a few moments to join us here at Rebel Solutions. We're a new business managed by old hands in the hospitality sector. After months of market research, planning, and development, we're ready to offer you the best menu of internet enabled tools and solutions. To learn how your business can benefit from a Rebel Solution, click here.''' print LineIndent(t, ' '*4, maxwidth=50) def test_niceboolean(): def x(inp): print "%r -> %r"%(inp, niceboolean(inp)) for e in [False, True, 1, 0, '1', '0', 'On','Off','False','No','Yes','T','F']: x(e) def test_ShowDescription(): # what happens if it's None or blank print repr(ShowDescription("", 'structuredtext')) print repr(ShowDescription(None, 'structuredtext')) def test_highlight_signature(): sign1='''bla bla bla and -- this bla bla -- Signature here''' print highlight_signature(sign1) sign2='''<b>Hej</b> --<br /> signature here''' print "-+"*20 print highlight_signature(sign2, tag='div', attribute="class='hej'") sign3='<p>In this email I use -- double hyphens or like Jan<br />\n--<br />\ndoes with his emails. That must work.<br />\n<br />\n-- <br />\nPeter Bengtsson, <a href="http://www.peterbe.com">www.peterbe.com</a> </p>' print "-+"*20 print highlight_signature(sign3) sign4='''Bla bla bla -- Peter B''' print "-+"*20 print highlight_signature(sign4) sign5 = '''<p>You can also use\n<a href="http://www.issuetrackerproduct.com/Demo/What-is-StructuredText">StructuredText</a>\nif you put the word "stx" or "structuredtext" in the subjectline\nbefore the colon.</p>\n<p>-- \nPeter Bengtsson, <a href="http://www.fry-it.com">http://www.fry-it.com</a> </p>\n''' print "-+"*20 print highlight_signature(sign5) def test_ValidEmailAddress(): def T(s, expect): assert ValidEmailAddress(s) == expect, s T('peter@peterbe.com', True) T('peter @peterbe.com', False) T('peter+julika@peterbe.com', True) T("peter'julika@peterbe.com", True) invalids='''_invalid@foo.com invalid_@foo.com invalid.@foo.com inv@lid@foo.com inv#lid@foo.com invalid@foo..com invalid@-foo.com invalid@foo-.com invalid@foocom invalid@f#o.com invalid@foo.commie invalid@foo.c0m invalid@foo.b@r.com invalid@foo.b#r.com invalid@foo. invalid@foo_bar.com''' invalids = [x.strip() for x in invalids.split()] oks = '''ok@911.com ok@foo.b-r.com o.k@foo.com ok7@foo.com ok@dmv.ca.us ''' oks = [x.strip() for x in oks.split()] map(lambda i: T(i, True), oks) map(lambda i: T(i, False), invalids) def test_safe_html_quote(): def x(s): print s print safe_html_quote(s) print x("<input name=t /> Ӓ ©") def test_AwareLengthLimit(): def T(s,L): print AwareLengthLimit(s, L) print T("Peter Bengtsson", 5) T("Peter Bengtsson", 25) T("", 100) T("", 0) T("oÞōƼȫʚ̉͸ϧ", 10) T("oÞōƼȫʚ̉͸ϧ", 6) T("oÞōƼȫʚ̉͸ϧ", 5) T("oÞōƼȫʚ̉͸ϧ", 4) T("oÞōƼȫʚ̉͸ϧ", 3) def test_ShowDescription(): s="0201023120" print ShowDescription(s, 'structuredtext') s="0201023120." print ShowDescription(s, 'structuredtext') s="0201 023 120." print ShowDescription(s, 'structuredtext') s="1." print ShowDescription(s, 'structuredtext') s="0123123123," print ShowDescription(s, 'structuredtext') def test_timeSince(): def T(x, y, minute_granularity=0): print timeSince(x,y, minute_granularity=minute_granularity) T(0, 0) T(0, 1) T(0, 1.0) T(0, 1.5) T(0, 6.9999) T(0, 0, 1) T(0, 1, 1) T(0, 1.0, 1) T(0, 1.5, 1) T(0, 6.9999, 1) T(0, 1/24.0, 1) T(0, (1/24.0)/3, 1) def test_createStandaloneWordRegex(): def T(word, text): print createStandaloneWordRegex(word).findall(text) T("peter", "So Peter Bengtsson wrote this") T("peter", "peter") T("peter bengtsson", "So Peter Bengtsson wrote this") def test_getFindIssueLinksRegex(): def T(text, z, t=None, p=None): compiled_regex = getFindIssueLinksRegex(z, t, p) return compiled_regex.findall(text) t='This is #1234 a text #666 like #5421 this' assert T(t, 4)==['#1234', '#5421'], T(t, 4) t='Remember Real#1234? or #9876 but not Demo#2468 or then:#1235' assert T(t, 4, 'Real')==['Real#1234', '#9876', '#1235'], T(t, 4, 'Real') t='#123 is what is starts with and ends with #432' assert T(t, 3)==['#123', '#432'], T(t, 3) t='prefixed with 000- is #000-0103 but not Real#000-0104' assert T(t, 4, p='000-')== ['#000-0103'] t='In brackets (#1234) with tracker id demo like (demo#0987)' assert T(t, 4, 'Demo')==['#1234', 'demo#0987'] t = '#111 or (#113) or (Real#115) but not (#1122) or (Real#8989) or (OReal#116)' print T(t, 3, 'real') t = '#111 or (#113) or (Real#115) but not (#1122) or (Demo#898)' print T(t, 3, ('real','demo')) def test_html_entity_fixer(): def T(x): print repr(x), "becomes", repr(html_entity_fixer(x)) T("header & footer") T('& £ öå') def test_filenameSplitter(): def T(x): print x, filenameSplitter(x) T('Error-02September2005.log') T('Someting.log') T('Some-ting.log') T('issue_listing.gif') assert 'listing.gif' in filenameSplitter('issue_listing.gif') T('issue listing.gif') def test_highlightCarefully(): def T(word, text): hlf = lambda x: '<span class="h">%s</span>' % x print highlightCarefully(word, text, hlf) T("bug", '''I saw a bug on <a href=# title="a bug">this page</a>''') T("bug", '''I saw a <bug> on <a href=# title="a bug">this page</a>''') T("bug", '''I <em>saw a bug</em> on <a href=# title="a bug">this page</a>''') T("bug", '''<p>I saw a bug on <a href=# title="a bug">this page</a></p>''') def test_iuniqify(): def T(words, expect=None): print iuniqify(words) T(['foo','bar','Foo']) T(['foo','bar','foo']) T(['foo',None,'fOO', 4]) if __name__=='__main__': #test() #benchmark_ShowDescription() #test__preParseEmailString() #test__addhrefs() #test__LineIndent() #test_niceboolean() #test_highlight_signature() #test_ValidEmailAddress() #test_safe_html_quote() #test_AwareLengthLimit() #test_ShowDescription() #test_timeSince() #test_createStandaloneWordRegex() #test_getFindIssueLinksRegex() #test_html_entity_fixer() #test_filenameSplitter() #test_highlightCarefully() test_iuniqify() ���������������������������������������������������������������������������������������������������������������������������������������IssueTrackerProduct/IssueTracker.py�����������������������������������������������������������������0000644�0001750�0001750�00001724505�11012074373�017653� 0����������������������������������������������������������������������������������������������������ustar �peterbe�������������������������peterbe����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# IssueTrackerProduct # # www.issuetrackerproduct.com # Peter Bengtsson <mail@peterbe.com> # License: ZPL # __doc__="""IssueTrackerProduct is the easiest bug/issue tracker system to use for Zope. By Peter Bengtsson <mail@peterbe.com> Credits: Gregory Wild-Smith, sack, http://twilightuniverse.com issuetracker-development mailinglist community Gavin Kistner for the the tabbed Properties tab Danny W. Adair of Asterisk Ltd for getRolesInContext(self) bug report and patch. """ # python import string, os, re, sys import random import poplib from urlparse import urlparse try: from poplib import POP3, POP3_SSL _has_pop3_ssl = True except ImportError: from poplib import POP3 _has_pop3_ssl = False import cgi import cStringIO import inspect from time import time from socket import error as socket_error from urllib import urlopen try: import transaction except ImportError: # we must be in an older than 2.8 version of Zope transaction = None try: import csv except: csv = None try: from sets import Set except ImportError: # must be old python Set = None from email.MIMEText import MIMEText from email.MIMEMultipart import MIMEMultipart from email.Header import Header from email.Utils import parseaddr, formataddr try: import email.Parser as email_Parser import email.Header as email_Header except ImportError: email_Parser = None try: from stripogram import html2safehtml except ImportError: html2safehtml = None try: from PIL import Image except ImportError: try: import Image except ImportError: Image = None try: from Products.ExternalEditor import ExternalEditor _has_ExternalEditor = True except ImportError: _has_ExternalEditor = False try: from formatflowed import decode as formatflowed_decode _has_formatflowed_ = True except ImportError: _has_formatflowed_ = False # Zope from Products.PageTemplates.PageTemplateFile import PageTemplateFile as PTF from Globals import Persistent, InitializeClass, package_home, DTMLFile from OFS import SimpleItem, Folder, PropertyManager from DocumentTemplate import sequence from AccessControl import ClassSecurityInfo, getSecurityManager from Products.ZCatalog.CatalogAwareness import CatalogAware from Acquisition import aq_inner, aq_parent, aq_base from zLOG import LOG, ERROR, INFO, PROBLEM, WARNING from DateTime import DateTime from App.ImageFile import ImageFile from ZPublisher.HTTPRequest import record from zExceptions import NotFound, Unauthorized # Is CMF installed? try: from Products.CMFCore.utils import getToolByName as CMF_getToolByName except ImportError: CMF_getToolByName = None try: from Products.ZCTextIndex.ParseTree import ParseError _has_ZCTextIndex = 1 except: class ParseError(Exception): # make it up ourselfs pass _has_ZCTextIndex = 0 # Zope >=2.7 has OrderedFolder baked into the core, oldies have to install it manually try: from OFS.OrderedFolder import OrderedFolder as ZopeOrderedFolder except ImportError: try: from Products.OrderedFolder.OrderedFolder import OrderedFolder as ZopeOrderedFolder except ImportError: m = "OrderedFolder not installed. Reports can not be ordered" LOG("IssueTrackerProduct", WARNING, m) del m from OFS.Folder import Folder as ZopeOrderedFolder # Product from I18N import _ from upgrade import VersionController from TemplateAdder import addTemplates2Class, CTP import Notifyables import Utils from Utils import unicodify from Webservices import IssueTrackerWebservices from Permissions import * from Constants import * from Errors import * #---------------------------------------------------------------------------- import logging logger = logging.getLogger('IssueTrackerProduct') __version__=open(os.path.join(package_home(globals()), 'version.txt')).read().strip() ## https://bugs.launchpad.net/zope2/+bug/142399 def safe_hasattr(obj, name, _marker=object()): """Make sure we don't mask exceptions like hasattr(). We don't want exceptions other than AttributeError to be masked, since that too often masks other programming errors. Three-argument getattr() doesn't mask those, so we use that to implement our own hasattr() replacement. """ return getattr(obj, name, _marker) is not _marker def base_hasattr(obj, name): """Like safe_hasattr, but also disables acquisition.""" return safe_hasattr(aq_base(obj), name) #---------------------------------------------------------------------------- def manage_hasAquirableMailHost(self): """ return if there is a MailHost object in the aqcuisition path """ return len(self.superValues(['Mail Host', 'Secure Mail Host'])) > 0 manage_addIssueTrackerForm = PTF('zpt/addIssueTrackerForm', globals()) def manage_addIssueTracker(dispatcher, id, title='', REQUEST=None): """ add IssueTracker instance via the web """ dest = dispatcher.Destination() issuetracker = IssueTracker(id, title.strip(), sitemaster_name=title) dest._setObject(id, issuetracker) self = dest._getOb(id) self.DeployStandards() self.InitZCatalog() # set that 'IssueTracker Manager' and 'IssueTracker User' should by # default have 'Access IssueTracker' permission if these are defined roles_4_view = [IssueTrackerManagerRole, IssueTrackerUserRole] self.manage_permission('View', roles=roles_4_view, acquire=1) if REQUEST is not None: # whereto next? redirect = REQUEST.RESPONSE.redirect if REQUEST.has_key('addandedit'): url = self.absolute_url() url += '/manage_PropertiesWizard?stage=0&firsttime=1' redirect(url) elif REQUEST.has_key('addandgoto'): redirect(self.absolute_url()+'/manage_workspace') elif REQUEST.has_key('DestinationURL'): redirect(REQUEST.DestinationURL+'/manage_workspace') else: redirect(REQUEST.URL1+'/manage_workspace') #---------------------------------------------------------------------------- class IssueTrackerFolderBase(Folder.Folder, Persistent): """ A base class for the IssueTracker class """ def doDebug(self): """ return True if we're in debug mode """ return DEBUG def getAutosaveInterval(self): """ return the seconds interval of how often the autosaving function should submit. """ return AUTOSAVE_INTERVAL_SECONDS def ValidEmailAddress(self, email): """ wrap script """ script = Utils.ValidEmailAddress return script(email) def html_entity_fixer(self, text, skipchars=[], extra_careful=1): """ wrap script """ return Utils.html_entity_fixer(text, skipchars=skipchars, extra_careful=extra_careful) def newline_to_br(self, text): """ wrap script """ script = Utils.newline_to_br return script(text) def encodeEmailString(self, email, title=None, nolink=0): """ wrap script """ script = Utils.encodeEmailString return script(email, title, nolink=nolink) def sortSequence(self, seq, params): """ this is useful because Python Scripts don't allow sequence.sort """ return sequence.sort(seq, params) def getOrdinalth(self, daynr, html=0): """ what Utils script """ return Utils.ordinalth(daynr, html=html) def timeSince(self, date1, date2, afterword=None, minute_granularity=False): """ wrap Utils.timeSince() """ return Utils.timeSince(date1, date2, afterword=afterword, minute_granularity=minute_granularity) def ShowFilesize(self, bytes): """ pass on to utilities module """ return Utils.ShowFilesize(bytes) def LineIndent(self, text, indent): """ wrap script """ return Utils.LineIndent(text, indent) def getFileIconpath(self, filename): """ Try to find a suitable file icon """ default = '/misc_/OFSP/File_icon.gif' extension = filename.lower()[filename.rfind('.')+1:] if extension.endswith('~'): extension = extension[:-1] if ICON_ASSOCIATIONS.has_key(extension): return '/%s/%s'%(ICON_LOCATION,ICON_ASSOCIATIONS[extension]) else: return default def getRandomString(self, length=5, loweronly=0, numbersonly=0): """ return a completely random piece of string """ script = Utils.getRandomString return script(length, loweronly, numbersonly) def lengthLimit(self, string, maxsize=45, append='...'): """ show only the first 'maxsize' characters of the string """ return Utils.AwareLengthLimit(string, maxsize, append) def safe_html_quote(self, text): """ wrap this improvement to Zope's html_quote in Utils """ return Utils.safe_html_quote(text) def ascii_url_quote(self, s): """ return a string url quoted even it's it a unicode string """ if isinstance(s, unicode): return Utils.url_quote(s.encode(UNICODE_ENCODING)) else: return Utils.url_quote(s) def ascii_url_quote_plus(self, s): """ return a string url quoted (with +) even it's it a unicode string """ if isinstance(s, unicode): return Utils.url_quote_plus(s.encode(UNICODE_ENCODING)) else: return Utils.url_quote_plus(s) def tag_quote(self, text): """ wrap Utils """ return Utils.tag_quote(text) def splitTerms(self, term): """ wrap Utils script because it's need in ZPTs """ return Utils.splitTerms(term) def getContentType(self, content_type='text/html', charset=UNICODE_ENCODING): """ return the content type header value """ return '%s; charset=%s' % (content_type, charset) def getAndSetContentType(self, content_type='text/html', charset=UNICODE_ENCODING): """ return the content type header value and set it on self.REQUEST.RESPONSE """ value = self.getContentType(content_type=content_type, charset=charset) self.REQUEST.RESPONSE.setHeader('Content-Type', value) return value def unsafe_unicode_dict_getitem(self, dictionary, item): """ Return the value of this item in a dictionary object. Simply call the __getitem__ of this dictionary to pluck out an item. Why call this unsafe_...() ? If you try to do this in a guarded context (e.g. Script (Python) (or Page Template)) you'll get an Unauthorized error: d = {u'\xa3':1} d[u'\xa3'] # will raise an Unauthorized error # this works however d = {u'\xa3':1, u'asciiable':1} d[u'asciiable'] Why? I don't know. The place where it happens is the parental guardian function guarded_getitem() from ZopeGuards.py By instead calling the __getitem__ from here in unrestricted python we can bypass this. """ return dictionary[item] #---------------------------------------------------------------------------- # Misc stuff ss = lambda s: s.strip().lower() # to save some typing space def ss_remove(list_, element): correct_element = None element = ss(element) for item in list_: if ss(item) == element: correct_element = item break if correct_element is not None: list_.remove(correct_element) signature_patterns = {'url':re.compile('\[url\]', re.I), 'title':re.compile('\[title\]', re.I), 'sitemaster name':re.compile('\[sitemaster name\]', re.I), 'sitemaster email':re.compile('\[sitemaster email\]', re.I), 'date':re.compile('\[date\]', re.I), } def debug(s, tabs=0, steps=(1,), f=False): if DEBUG or f: inspect_dbg = [] if type(steps)==type(1): steps = range(1, steps+1) for i in steps: try: #caller_module = inspect.stack()[i][1] caller_method = inspect.stack()[i][3] caller_method_line = inspect.stack()[i][2] except IndexError: break inspect_dbg.append("%s:%s"%(caller_method, caller_method_line)) out = "\t"*tabs + "%s (%s)"%(s, ", ".join(inspect_dbg)) # XXX this needs attention. Consider implementing a ObserverProxy from # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/413701 print out open('issuetracker-debug.log','a').write(out+"\n") class Empty: pass #---------------------------------------------------------------------------- class IssueTracker(IssueTrackerFolderBase, CatalogAware, Notifyables.Notifyables, IssueTrackerWebservices ): """ IssueTracker class """ meta_type = ISSUETRACKER_METATYPE security = ClassSecurityInfo() security.setPermissionDefault(AddIssuesPermission, (IssueTrackerManagerRole, IssueTrackerUserRole, 'Anonymous', 'Owner', 'Manager')) manage_options = Folder.Folder.manage_options[:2] + \ ({'label':'Properties', 'action':'manage_editIssueTrackerPropertiesForm'}, {'label':'Management', 'action':'manage_ManagementForm'}, \ {'label':'POP3', 'action':'manage_POP3ManagementForm'}) \ + Folder.Folder.manage_options[3:] native_properties = NATIVE_PROPERTIES # used by CheckoutableTemplates to filter templates this_package_home = package_home(globals()) # used for some templates project_homepage = 'http://www.issuetrackerproduct.com' def __init__(self, id, title='', sitemaster_name=DEFAULT_SITEMASTER_NAME, sitemaster_email=DEFAULT_SITEMASTER_EMAIL): """ Init IssueTracker class """ self.id = str(id) self.title = str(title) self.types = list(DEFAULT_TYPES) self.urgencies = list(DEFAULT_URGENCIES) self.sections_options = list(DEFAULT_SECTIONS_OPTIONS) self.defaultsections = list(DEFAULT_SECTIONS) self.when_ignore_word = DEFAULT_WHEN_IGNORE_WORD self.display_date = DEFAULT_DISPLAY_DATE self.always_notify = DEFAULT_ALWAYS_NOTIFY self.sitemaster_name = sitemaster_name self.sitemaster_email = sitemaster_email self.default_type = DEFAULT_TYPE self.default_urgency = DEFAULT_URGENCY self.manager_roles = DEFAULT_MANAGER_ROLES self.default_batch_size = DEFAULT_DEFAULT_BATCH_SIZE self.allow_show_all = DEFAULT_ALLOW_SHOW_ALL self.issueprefix = DEFAULT_ISSUEPREFIX self.no_fileattachments = DEFAULT_NO_FILEATTACHMENTS self.no_followup_fileattachments = DEFAULT_NO_FOLLOWUP_FILEATTACHMENTS self.statuses = list(DEFAULT_STATUSES) self.statuses_verbs = list(DEFAULT_STATUSES_VERBS) self.display_formats = list(DEFAULT_DISPLAY_FORMATS) self.default_display_format = DEFAULT_DEFAULT_DISPLAY_FORMAT self.dispatch_on_submit = DEFAULT_DISPATCH_ON_SUBMIT self.randomid_length = DEFAULT_RANDOMID_LENGTH self.allow_issueattrchange = DEFAULT_ALLOW_ISSUEATTRCHANGE self.stop_cache = DEFAULT_STOP_CACHE self.allow_subscription = DEFAULT_ALLOW_SUBSCRIPTION self.use_tellafriend = DEFAULT_USE_TELLAFRIEND self.use_tellafriend_for_anonymous = DEFAULT_USE_TELLAFRIEND_FOR_ANONYMOUS self.show_dates_cleverly = DEFAULT_SHOW_DATES_CLEVERLY self.private_statistics = DEFAULT_PRIVATE_STATISTICS self.private_reports = DEFAULT_PRIVATE_REPORTS self.save_drafts = DEFAULT_SAVE_DRAFTS self.show_confidential_option = DEFAULT_SHOW_CONFIDENTIAL_OPTION self.show_hideme_option = DEFAULT_SHOW_HIDEME_OPTION self.show_issueurl_option = DEFAULT_SHOW_ISSUEURL_OPTION self.show_download_button = DEFAULT_SHOW_DOWNLOAD_BUTTON self.encode_emaildisplay = DEFAULT_ENCODE_EMAILDISPLAY self.show_always_notify_status = DEFAULT_SHOW_ALWAYS_NOTIFY_STATUS self.images_in_menu = DEFAULT_IMAGES_IN_MENU self.use_issue_assignment = DEFAULT_USE_ISSUE_ASSIGNMENT self._assignment_blacklist = [] self.signature_text = DEFAULT_SIGNATURE_TEXT self.default_sortorder = DEFAULT_SORTORDER self.can_add_new_sections = DEFAULT_CAN_ADD_NEW_SECTIONS self.show_id_with_title = DEFAULT_SHOW_ID_WITH_TITLE self.show_use_accesskeys_option = DEFAULT_SHOW_USE_ACCESSKEYS_OPTION self.show_remember_savedfilter_persistently_option = DEFAULT_SHOW_REMEMBER_SAVEDFILTER_PERSISTENTLY_OPTION self.outlook_batch_size = DEFAULT_OUTLOOK_BATCH_SIZE self.use_autosave = DEFAULT_USE_AUTOSAVE self.disallow_duplicate_issue_subjects = DEFAULT_DISALLOW_DUPLICATE_ISSUE_SUBJECTS self.use_estimated_time = DEFAULT_USE_ESTIMATED_TIME self.use_actual_time = DEFAULT_USE_ACTUAL_TIME self.include_description_in_notifications = DEFAULT_INCLUDE_DESCRIPTION_IN_NOTIFICATIONS self.spam_keywords = DEFAULT_SPAM_KEYWORDS self.show_spambot_prevention = DEFAULT_SHOW_SPAMBOT_PREVENTION self.acl_cookienames = {} self.acl_cookieemails = {} self.acl_cookiedisplayformats = {} self.menu_items = DEFAULT_MENU_ITEMS self.btreefolder_storage = False self.brother_issuetracker_paths = [] self.plugin_paths = [] ## Getting basic attributes def getId(self): """ return id """ return self.id def getTitle(self): """ return title """ return self.title security.declareProtected('View', 'getModifyTimestamp') def getModifyTimestamp(self): """ return the modify date of the issuetracker as a whole as an integer timestamp. The latest modify date is the issue with the latest modify date. """ issues = self.getIssueObjects() issues.sort(lambda x,y: cmp(y.getModifyDate(), x.getModifyDate())) if issues: return issues[0].getModifyTimestamp() return int(self.bobobase_modification_time()) def relative_url(self, url=None): """ shorter than absolute_url """ if url: return url.replace(self.REQUEST.BASE0, '') path = self.absolute_url_path() if path == '/': # urls should always be return not ending in a slash # so that you can be garanteed this in the templates return '' else: return path def XXXglobal_relative_url(self, object_or_url): """ return a simpler url of any object """ if isinstance(object_or_url, basestring): url = object_or_url else: url = object_or_url.absolute_url() return url.replace(self.REQUEST.BASE0, '') def getStatusesVerbs(self): """ return statuses_verbs """ return getattr(self, 'statuses_verbs', DEFAULT_STATUSES_VERBS) def getStatuses(self): """ return statuses """ return self.statuses def getStatusesMerged(self, aslist=0, asdict=0, verb_first=0, cleaned=False): """ return statuses and statuses_verbs next to each other So it looks like this ['taken, take', 'rejected, reject', ...] If the 'cleaned' property is set to true, we clean up all the values carefully. This is off by default so that the cleaning only happens on rare occasions such as when you're on the Properties tab. """ statuses = self.getStatuses() verbs = self.getStatusesVerbs() if cleaned: statuses = [unicodify(x.strip()) for x in statuses if x.strip()] verbs = [unicodify(x.strip()) for x in verbs if x.strip()] _big_warning = False if len(statuses) > len(verbs): _big_warning = True _add_to_verbs = [] for i in range(len(statuses)-len(verbs)): _add_to_verbs.append(statuses[len(verbs)+i]) verbs.extend(_add_to_verbs) elif len(verbs) > len(statuses): _big_warning = True _add_to_statuses = [] for i in range(len(verbs)-len(statuses)): _add_to_statuses.append(verbs[len(statuses)+i]) statuses.extend(_add_to_statuses) if _big_warning: msg = "The status list (statuses and verbs) is out of sync and "\ "has had to be temporarily merged to work. Please revisit "\ "the Properties tab." logger.warn(msg) self.statuses = statuses self.statuses_verbs = verbs nl=[] nldict = {} delimiter = ', ' for i in range(len(statuses)): if verb_first: nldict[verbs[i].strip()] = statuses[i].strip() else: nldict[statuses[i].strip()] = verbs[i].strip() if aslist: nl.append([statuses[i], verbs[i]]) else: nl.append(statuses[i]+delimiter+verbs[i]) if asdict: return nldict else: return nl def splitStatusesAndVerbs(self, statuses_and_verbs): """ list might be ['open, open', 'taken, take', ...] then split this up into two lists. Raise a ValueError if no delimeter is found or if any value is empty. """ statuses = [] verbs = [] for each in [x.strip() for x in statuses_and_verbs if x.strip()]: found_delim = max(each.find(','), each.find(';'), each.find('|')) if found_delim > -1: splitted = [each[:found_delim], each[found_delim+1:]] if not splitted[0].strip(): raise ValueError, "Status item entered blank (%r)" % each if not splitted[1].strip(): raise ValueError, "Verb item entered blank (%r)" % each statuses.append(splitted[0].strip()) verbs.append(splitted[1].strip()) elif each.strip() != '': raise ValueError, "Line contains no delimeter (%r)" % each return statuses, verbs def getSectionOptions(self): """ return section options """ return self.sections_options def getTypeOptions(self): """ return types """ return self.types def getUrgencyOptions(self): """ return urgencies """ return self.urgencies def getDefaultSections(self): """ return default sections """ return self.defaultsections def getDefaultType(self): """ return default type """ return self.default_type def getDefaultUrgency(self): """ return default urgency """ return self.default_urgency def getDefaultDisplayFormat(self): """ return default_display_format """ return getattr(self, 'default_display_format', DEFAULT_DEFAULT_DISPLAY_FORMAT) def AllowIssueAttributeChange(self): """ Determine if the allow_issueattrchange is True """ return getattr(self, 'allow_issueattrchange', DEFAULT_ALLOW_ISSUEATTRCHANGE) def AllowIssueSubscription(self): """ Determine if the allow_subscription is True """ return getattr(self, 'allow_subscription', DEFAULT_ALLOW_SUBSCRIPTION) def UseTellAFriend(self): """ Determine if we're going to use the tell-a-friend feature on the issue view """ return getattr(self, 'use_tellafriend', DEFAULT_USE_TELLAFRIEND) def UseTellAFriendForAnonymous(self): """ Determine if we're going to use the tell-a-friend feature on the issue view even for anonymous users """ return getattr(self, 'use_tellafriend_for_anonymous', DEFAULT_USE_TELLAFRIEND_FOR_ANONYMOUS) def ShowDatesCleverly(self): """ Determine if we're going to show dates differently depending on when the date is. What happens is that dates that are today are shown as 'Today 11:25' and really old dates are shown without the time part. """ return getattr(self, 'show_dates_cleverly', DEFAULT_SHOW_DATES_CLEVERLY) def PrivateStatistics(self): """ Determine if private_statistics is False """ default = DEFAULT_PRIVATE_STATISTICS return getattr(self, 'private_statistics', default) def PrivateReports(self): """ Determine if private_reports is False """ default = DEFAULT_PRIVATE_REPORTS return getattr(self, 'private_reports', default) def SaveDrafts(self): """ Return if we allow for saving drafts """ default = DEFAULT_SAVE_DRAFTS return getattr(self, 'save_drafts', default) def UseAutoSave(self): """ return if we're going to use autosave """ default = DEFAULT_USE_AUTOSAVE return getattr(self, 'use_autosave', default) def DisallowDuplicateIssueSubjects(self): """ return disallow_duplicate_issue_subjects """ default = DEFAULT_DISALLOW_DUPLICATE_ISSUE_SUBJECTS return getattr(self, 'disallow_duplicate_issue_subjects', default) def UseEstimatedTime(self): """ return use_estimated_time """ default = DEFAULT_USE_ESTIMATED_TIME return getattr(self, 'use_estimated_time', default) def AllowShowAll(self): """ return allow_show_all """ default = DEFAULT_ALLOW_SHOW_ALL return getattr(self, 'allow_show_all', default) def UseActualTime(self): """ return use_actual_time """ default = DEFAULT_USE_ACTUAL_TIME return getattr(self, 'use_actual_time', default) def _setUseActualTime(self, toggle_to=True): """ set use_actual_time """ self.use_actual_time = bool(toggle_to) def IncludeDescriptionInNotifications(self): """ return include_description_in_notifications """ default = DEFAULT_INCLUDE_DESCRIPTION_IN_NOTIFICATIONS return getattr(self, 'include_description_in_notifications', default) def getSpamKeywords(self): """ return spam_keywords if possible """ return getattr(self, 'spam_keywords', DEFAULT_SPAM_KEYWORDS) def getSpamKeywordsExpanded(self): """ the property 'spam_keywords' is a list that contains potentially sublists like this: ['foo', 'bar', ['kung', 'fu'], ] Then, return it like this: ['foo', 'bar', '\tkung', '\tfu', ] """ padding_template = ' %s' L = self.getSpamKeywords()[:] listtest = lambda x: isinstance(x, list) for item in L: if listtest(item): i = L.index(item) L.pop(i) item.reverse() for subitem in item: L.insert(i, padding_template % subitem) return L def ShowConfidentialOption(self): """ return show_confidential_option """ default = DEFAULT_SHOW_CONFIDENTIAL_OPTION return getattr(self, 'show_confidential_option', default) def ShowHideMeOption(self): """ return show_hideme_option """ default = DEFAULT_SHOW_HIDEME_OPTION return getattr(self, 'show_hideme_option', default) def ShowIssueURLOption(self): """ return show_issueurl_option """ # the default is probably False but because we don't want to surprise people # with existing issuetracker instance we resolve to True if it # hasn't been set. if hasattr(self, 'show_issueurl_option'): return self.show_issueurl_option else: #default = DEFAULT_SHOW_ISSUEURL_OPTION default = True return default def ShowDownloadButton(self): """ return show_download_button """ default = DEFAULT_SHOW_DOWNLOAD_BUTTON return getattr(self, 'show_download_button', default) def EncodeEmailDisplay(self): """ return encode_emaildisplay """ default = DEFAULT_ENCODE_EMAILDISPLAY return getattr(self, 'encode_emaildisplay', default) def getNoFileattachments(self): """ return no_fileattachments or default """ return getattr(self, 'no_fileattachments', DEFAULT_NO_FILEATTACHMENTS) def getNoFollowupFileattachments(self): """ return no_followup_fileattachments or default """ return getattr(self, 'no_followup_fileattachments', DEFAULT_NO_FOLLOWUP_FILEATTACHMENTS) def doDispatchOnSubmit(self): """ Check if we shall dispatch emails out """ return getattr(self, 'dispatch_on_submit', DEFAULT_DISPATCH_ON_SUBMIT) def doStopCache(self): """ return the stop_cache property """ return getattr(self, 'stop_cache', DEFAULT_STOP_CACHE) def doShowAlwaysNotifyStatus(self): """ return show_always_notify_status """ return getattr(self, 'show_always_notify_status', DEFAULT_SHOW_ALWAYS_NOTIFY_STATUS) def imagesInMenu(self): """ return if the images_in_menu attribute is True """ return getattr(self, 'images_in_menu', DEFAULT_IMAGES_IN_MENU) def CanAddNewSections(self): """ return if can_add_new_sections is True """ return getattr(self, 'can_add_new_sections', DEFAULT_CAN_ADD_NEW_SECTIONS) def ShowIdWithTitle(self): """ return show_id_with_title """ return getattr(self, 'show_id_with_title', DEFAULT_SHOW_ID_WITH_TITLE) def ShowCSVExportLink(self): """ return show_csvexport_link """ return getattr(self, 'show_csvexport_link', DEFAULT_SHOW_CVSEXPORT_LINK) def ShowAccessKeysOption(self): """ return show_use_accesskeys_option """ default=DEFAULT_SHOW_USE_ACCESSKEYS_OPTION return getattr(self, 'show_use_accesskeys_option', default) def ShowRememberSavedfilterPersistentlyOption(self): """ return show_remember_savedfilter_persistently_option """ default=DEFAULT_SHOW_REMEMBER_SAVEDFILTER_PERSISTENTLY_OPTION return getattr(self, 'show_remember_savedfilter_persistently_option', default) def getOutlookBatchSize(self): """ return outlook_batch_size (used in zpt/index_html.zpt) """ default = DEFAULT_OUTLOOK_BATCH_SIZE return getattr(self, 'outlook_batch_size', default) def ShowSpambotPrevention(self): """ return show_spambot_prevention """ default = DEFAULT_SHOW_SPAMBOT_PREVENTION return getattr(self, 'show_spambot_prevention', default) def getSitemasterEmail(self): """ return sitemaster_email """ return self.sitemaster_email def getSitemasterName(self): """ return sitemaster_name """ return self.sitemaster_name def getSitemasterFromField(self): """ return a combination of sitemaster_name and sitemaster_email """ name = self.getSitemasterName() email = self.getSitemasterEmail() assert email.strip(), "Must have email for sitemaster" if name.strip(): return "%s <%s>" % (name, email) else: return email def UseIssueAssignment(self): """ return use_issue_assignment """ return getattr(self, 'use_issue_assignment', DEFAULT_USE_ISSUE_ASSIGNMENT) def UseExtendedOptions(self): """ return if we should allow for extended options to an issue """ #### XXXXXXX more work needed here return 0 def getIssueAssignmentBlacklist(self, check_each=False): """ return _assignment_blacklist """ list = getattr(self.getRoot(), '_assignment_blacklist',[]) if check_each: checked = [] for each in list: acl_path, username = each.split(',') try: userfolder = self.unrestrictedTraverse(acl_path) except: continue if userfolder.data.has_key(username): checked.append(each) return checked else: return list def ShowDescription(self, text, display_format=''): """ pass on to utilities module """ script = Utils.ShowDescription if self.EncodeEmailDisplay(): return script(text, display_format, emaillinkfunction=self.encodeEmailString) else: return script(text, display_format) def getSignature(self): """ return signature_text """ return getattr(self, 'signature_text', DEFAULT_SIGNATURE_TEXT) def showSignature(self): """ return getSignature() with the variables replaced with real stuff """ text = self.getSignature() patterns = signature_patterns if patterns['url'].findall(text): text = re.sub(patterns['url'], self.getRootURL(), text) if patterns['title'].findall(text): text = re.sub(patterns['title'], self.getRoot().getTitle(), text) if patterns['date'].findall(text): date = DateTime().strftime(self.display_date) text = re.sub(patterns['date'], date, text) if patterns['sitemaster name'].findall(text): _v = self.getSitemasterName() text = re.sub(patterns['sitemaster name'], _v, text) if patterns['sitemaster email'].findall(text): _v = self.getSitemasterName() text = re.sub(patterns['sitemaster email'], _v, text) return text def showDate(self, date, today=None): """ return the date formatted nicely """ if self.ShowDatesCleverly(): # The whole reason why today is a parameter is because # if this function is called 20 times in one page # eg. richList.zpt then it'd be a shame to create a new # DateTime object every time. By creating it once and # passing it every time to this function we save some # CPU and memory default_fmt = self.display_date def abbr(label, date): fmt = default_fmt.replace('%H:%M','').strip() return '<abbr title="%s">%s</abbr>' % (date.strftime(fmt), label) if today is None: today = DateTime() if date.strftime('%Y%m%d') == today.strftime('%Y%m%d'): return abbr(_("Today"), date) + date.strftime(" %H:%M") elif (date+1).strftime('%Y%m%d') == today.strftime('%Y%m%d'): return abbr(_("Yesterday"), date) + date.strftime(" %H:%M") elif date.strftime('%Y%W') == today.strftime('%Y%W'): return abbr(date.strftime('%A'), date) + date.strftime(' %H:%M') elif (date+7).strftime('%Y%W') == today.strftime('%Y%W'): return abbr(_("Last week") + date.strftime(' %A'), date) + date.strftime(' %H:%M') #elif date.strftime('%Y%m') == today.strftime('%Y%m'): # return date.strftime(default_fmt) else: # skip the hour part fmt = default_fmt.replace('%H:%M','').strip() return date.strftime(fmt) # default thing return date.strftime(self.display_date) def getDefaultSortorder(self): """ return the default sort order """ return getattr(self, 'default_sortorder', DEFAULT_SORTORDER) # new def doShowThreads(self): """ return if threads should be shown after the issue(s) """ default = True try: return Utils.niceboolean(self.REQUEST.get('show-threads', default)) except: return default def getForcedStylesheet(self): """ return which if any forced stylesheet to use """ v = self.REQUEST.get('forced-stylesheet') if not v: return None else: if v.startswith('/') or v.startswith('http'): return v else: return "%s/%s" % (self.getRootURL(), v) def getPluginPaths(self): """ return plugin_paths """ return getattr(self, 'plugin_paths', []) def getPluginObjects(self): """ return a list of Zope objects which are plugins to the issuetracker instance like the MoreStatistics or FileArchive """ objects = [] for path in self.getPluginPaths(): if path: try: object = self.restrictedTraverse(path) objects.append(object) except: pass return objects ## ## Getting the issue objects ## def _getIssueContainer(self): root = self.getRoot() if root._isUsingBTreeFolder(): return getattr(root, BTREEFOLDER2_ID) else: return root def getBrotherPaths(self): """ return the paths of the brother issuetrackers we have """ return getattr(self, 'brother_issuetracker_paths',[]) def _getBrothers(self): """ return a list of Issue Tracker instance objects that we have defined as brothers """ paths = self.getBrotherPaths() trackers = [self.restrictedTraverse(x) for x in paths] trackers = [x for x in trackers if x.meta_type == ISSUETRACKER_METATYPE] return trackers def isFromBrother(self, issue): """ return true if the passed issue doesn't belong to this issuetracker """ return not issue.absolute_url_path().startswith(self.getRoot().absolute_url_path()) def getBrotherFromIssue(self, issue): """ return the issuetracker instance this issue belongs to """ parent = aq_parent(aq_inner(issue)) if parent.meta_type == 'BTreeFolder2': parent = aq_parent(aq_inner(parent)) return parent def getIssueObjects(self): """ return what objectValues does but with varying container """ container = self._getIssueContainer() all = list(container.objectValues(ISSUE_METATYPE)) try: brothers = self._getBrothers() if brothers: for brother in brothers: all.extend(brother.getIssueObjects()) except KeyError, msg: tmpl = 'Reference to join-in issue trackers (%s) is broken in %s' paths = ', '.join(self.getBrotherPaths()) logger.warn(tmpl % (paths, self.absolute_url_path())) return all def getIssueItems(self): """ return what objectItems does but with varying container """ container = self._getIssueContainer() brothers = self._getBrothers() if brothers: all = list(container.objectValues(ISSUE_METATYPE)) for brother in brothers: all.extend(list(brother.getIssueItems())) return all else: return container.objectItems(ISSUE_METATYPE) def getIssueIds(self): """ return what objectIds does but with varying container """ container = self._getIssueContainer() brothers = self._getBrothers() if brothers: all = list(container.objectIds(ISSUE_METATYPE)) for brother in brothers: all.extend(list(brother.getIssueIds())) return all else: return container.objectIds(ISSUE_METATYPE) def countIssueObjects(self): """ return what objectValues does """ return len(self.getIssueObjects()) def hasAnyIssues(self): """ return if there are any issues in the root at all """ return self.countIssueObjects() > 0 def ageOfOldestIssue(self): """ return the datetime object of the oldest issue """ oldest = DateTime() for issue in self.getIssueObjects(): if issue.getIssueDate() < oldest: oldest = issue.getIssueDate() return oldest def hasIssue(self, issueid): """ see if this issue exists """ return hasattr(self._getIssueContainer(), issueid) def getIssueObject(self, issueid): """ because a plain getattr() wasn't enough """ return getattr(self._getIssueContainer(), issueid) def _isUsingBTreeFolder(self): """ return if we're using a BTreeFolder2 for storing all issues """ if not hasattr(self, 'btreefolder_storage'): root = self.getRoot() self.btreefolder_storage = BTREEFOLDER2_ID in root.objectIds('BTreeFolder2') return self.btreefolder_storage ## Editing the IssueTracker def getDisplayDateFormatOptions(self): """ return a list of a different formats """ return ['%d/%m %Y', '%d/%m %Y %H:%M', '%m/%d %Y', '%m/%d %Y %H:%M', # US style '%d %b %Y', '%d %b %Y %H:%M', '%d %B %Y', '%d %B %Y %H:%M', '%d-%m-%Y', '%d-%m-%Y %H:%M', '%m-%d-%Y', '%m-%d-%Y %H:%M', # US style '%d-%b %Y', '%d-%b %Y %H:%M', '%d-%B %Y', '%d-%B %Y %H:%M', '%Y/%m/%d', '%Y/%m/%d %H:%M', '%d/%m/%Y', '%d/%m/%Y - %H:%M', '%m/%d/%Y', '%m/%d/%Y - %H:%M', ] def getDefaultSortorderOptions(self): """ return which default sort orders we can have """ return SORTORDER_ALTERNATIVES def translateSortorderOption(self, variable): """ return a nice representation of the variable for the Properties tab. """ if variable == 'modifydate': return _(u"Modification date") elif variable == 'issuedate': return _(u"Creation date") else: return variable.capitalize() security.declareProtected(VMS, 'manage_findPotentialBrothers') def manage_findPotentialBrothers(self): """ return a list of all issue tracker instances that can be found in the proximity """ all = [] root = self.getRoot() root_parent = aq_parent(aq_inner(root)) all = self._getPotentialBrothers(root_parent, skip_id=root.getId()) all.sort(lambda x,y: cmp(x.getTitle(), y.getTitle())) return all def _getPotentialBrothers(self, inobject, skip_id=None): """ recursively return all issuetracker instances """ found = [] for obj in inobject.objectValues(): # Check that the found object is something sane try: obj.meta_type except: continue try: obj.isPrincipiaFolderish except: continue if obj.meta_type==ISSUETRACKER_METATYPE: if skip_id and skip_id == obj.getId(): continue found.append(obj) elif obj.isPrincipiaFolderish: found.extend(self._getPotentialBrothers(obj, skip_id=skip_id)) return found def _savePluginPaths(self, paths): """ filter and save the paths list """ if isinstance(paths, basestring): paths = [paths] paths = [x.strip() for x in paths if x.strip()] ok = [] for each in paths: try: obj = self.restrictedTraverse(each) except: continue if each not in ok: ok.append(each) self.plugin_paths = ok security.declareProtected(VMS, 'manage_savePluginPath') def manage_savePluginPath(self, path): """ add one plugin path to this instance """ assert path, "Path can't be empty" all_paths = self.getPluginPaths() + [path] self._savePluginPaths(all_paths) security.declareProtected(VMS, 'manage_editIssueTrackerProperties') def manage_editIssueTrackerProperties(self, carefulbooleans=False, REQUEST=None): """ save all IssueTracker related issues Since booleans are controlled from checkboxes where non-existance is the same as False. This is not good because sometimes you don't even ask for these checkboxes like in the PropertiesWizard. When carefulbooleans=True, non-existant booleans are not set to False. """ hk = self.REQUEST.has_key get = self.REQUEST.get strings = ['display_date', 'sitemaster_email', 'issueprefix', 'default_display_format', 'default_sortorder', ] unicodes = ['title','sitemaster_name', 'default_type','default_urgency', 'signature_text'] lists = ['types','urgencies','sections_options','defaultsections', 'statuses','statuses_verbs','display_formats', 'manager_roles',] ints = ['default_batch_size','randomid_length','no_fileattachments', 'no_followup_fileattachments', 'outlook_batch_size'] booleans = ['dispatch_on_submit','allow_issueattrchange','stop_cache', 'allow_show_all', 'allow_subscription', 'use_tellafriend', 'use_tellafriend_for_anonymous', 'private_statistics', 'private_reports', 'show_confidential_option','show_hideme_option', 'show_issueurl_option', 'show_download_button','encode_emaildisplay', 'show_always_notify_status', 'images_in_menu', 'use_issue_assignment', 'save_drafts', 'can_add_new_sections', 'show_id_with_title', 'show_use_accesskeys_option', 'show_remember_savedfilter_persistently_option', 'use_autosave', 'show_csvexport_link', 'disallow_duplicate_issue_subjects', 'use_estimated_time', 'use_actual_time', 'include_description_in_notifications', 'show_dates_cleverly', 'show_spambot_prevention', ] dict = self.__dict__ for each in strings: if hk(each) and isinstance(get(each), basestring): dict[each] = get(each).strip() for each in unicodes: if hk(each) and isinstance(get(each), basestring): dict[each] = unicodify(get(each).strip()) for each in ints: if hk(each): if isinstance(get(each), int): dict[each] = get(each) else: logger.warn('%s not integer' % get(each)) for each in lists: if hk(each) and isinstance(get(each), list): dict[each] = Utils.uniqify(get(each)) for each in booleans: if hk(each) and get(each): dict[each] = True elif not carefulbooleans: dict[each] = False # now for a special one if hk('statuses-and-verbs'): if isinstance(get('statuses-and-verbs'), list): L1, L2 = self.splitStatusesAndVerbs(get('statuses-and-verbs')) self.statuses = L1 self.statuses_verbs = L2 else: logger.warn("Statuses and verbs not list type") # another special one if hk('always_notify'): # Every item must be recognized properly always_notify = get('always_notify') # clean upp the variable a bit always_notify = Utils.uniqify(always_notify) always_notify = [x.strip() for x in always_notify if x.strip()] checked = [] for each in always_notify: valid, better_spelling = self._checkAlwaysNotify(each) if valid: checked.append(better_spelling) self.always_notify = checked # another special one if get('brother_issuetracker_paths'): # every item must be recognized properly as an issuetracker instance paths = get('brother_issuetracker_paths') paths = [x.strip() for x in paths if x.strip()] # this will raise an error if it can't be reached trackers = [self.restrictedTraverse(x) for x in paths] # this will assert the meta_type trackers = [y for y in trackers if y.meta_type == ISSUETRACKER_METATYPE] self.brother_issuetracker_paths = paths else: self.brother_issuetracker_paths = [] # another special one self._savePluginPaths(get('plugin_paths',[])) # for the custom properties if REQUEST is not None: self.manage_editProperties(REQUEST) return self.manage_editIssueTrackerPropertiesForm(self.REQUEST, manage_tabs_message='IssueTracker properties updated.') def _checkAlwaysNotify(self, item, format='show'): """ return a tuple of (validity, spelling). An item is valid if it is a valid email address, an exising notifyable or an exisitng notifyable group. 'format' can either be 'show' or list (e.g. [name, email])""" item_lower = ss(item) # check the acl_users for iuf in self.superValues(ISSUEUSERFOLDER_METATYPE): for username, userdata in iuf.data.items(): showname = "%s, %s"%(userdata.getFullname(), username) if format == 'list': display = [userdata.getFullname(), userdata.getEmail()] else: display = showname if ss(showname) == item_lower: return True, display elif ss(username) == item_lower: return True, display elif ss(userdata.getFullname()) == item_lower: return True, display elif ss(userdata.getEmail()) == item_lower: return True, display elif item_lower.find(ss("(%s)"%username)) > -1: # fragmented possibly because fullname has changed return True, display elif not not re.search("\w\s*,\s*%s$"%username, item_lower, re.I): return True, display # check the notifyables all_notifyables = self.getNotifyables() for notifyable in all_notifyables: if notifyable.getName(): showname = "%s, %s"%(notifyable.getName(), notifyable.getEmail()) if format == 'list': display = [notifyable.getName(), notifyable.getEmail()] else: display = showname else: showname = notifyable.getEmail() if format == 'list': display = ['', notifyable.getEmail()] else: display = showname if item_lower == ss(showname): return True, display elif notifyable.getName().lower()==item_lower or \ notifyable.getEmail().lower()==item_lower: return True, display # check all groups if item.startswith('group: '): item_lower = item_lower[len('group:'):].strip() all_groups = self.getNotifyableGroups() for group in all_groups: if group.getId().lower() == item_lower or \ group.getTitle().lower() == item_lower: if format == 'list': return True, ["group: %s"%group.getTitle(), ""] else: return True, "group: %s"%group.getTitle() # check if it's a plain email address if Utils.ValidEmailAddress(item): if format == 'list': return True, ["", item] else: return True, item # default is to deny if format == 'list': return False, [] else: return False, item security.declareProtected(VMS, 'manage_editMenuItems') def manage_editMenuItems(self, hrefs, inurls, labels, reset_to_default=False, REQUEST=None): """ wrap up the values and save it to _setMenuItems(). _setMenuItems() accepts a list of dicts. Each inurl can be either a string or a tuple, consider it a token. """ if reset_to_default: menu_items = DEFAULT_MENU_ITEMS else: menu_items = [] assert len(hrefs)==len(inurls)==len(labels), \ "Missmatch of no. of hrefs, inurls, labels" for i in range(len(hrefs)): href = hrefs[i].strip() inurl = inurls[i].strip() label = labels[i].strip() if href+inurl+label == "": continue elif not label and href: label = href.split('/')[-1] elif not href and label: href = "/" + label if len(inurl.split()) > 1: inurl = tuple(inurl.split()) menu_items.append( dict(href=href, inurl=inurl, label=label)) # nothing can really go wrong, # load it in! self._setMenuItems(menu_items) # for the custom properties if REQUEST is not None: return self.manage_configureMenuForm(self.REQUEST, manage_tabs_message='Menu changed.') security.declareProtected(VMS, 'manage_addOtherProperty') def manage_addOtherProperty(self, id, value, type): """ Add arbitrary property """ self.manage_addProperty(id, value, type) page = self.manage_editIssueTrackerPropertiesForm return page(self.REQUEST, manage_tabs_message='Other property added.', activetab='custom' # used by the CSS magic on the Properties tab ) security.declareProtected(VMS, 'manage_delOtherProperties') def manage_delOtherProperties(self, ids): """ remove arbitrary properties """ self.manage_delProperties(ids) page = self.manage_editIssueTrackerPropertiesForm return page(self.REQUEST, manage_tabs_message='Property deleted', activetab='custom' # See comment about this parameter above ) ## General IssueTracker maintenance security.declareProtected(VMS, 'manage_canUseBTreeFolder') def manage_canUseBTreeFolder(self): """ return True if the BTreeFolder2 product is installed """ if self.filtered_meta_types(): all = self.filtered_meta_types() for each in all: if each.get('product')=='BTreeFolder2': return True return False security.declareProtected(VMS, 'manage_isUsingBTreeFolder') def manage_isUsingBTreeFolder(self): """ just a wrapping """ return self._isUsingBTreeFolder() security.declareProtected(VMS, 'manage_convert2BTreeFolder') def manage_convert2BTreeFolder(self, REQUEST=None): """ change where we store issues, before they were stored in the issue tracker root (i.e. self.getRoot()) but now we want to store them inside a container of kind BTreeFolder2. """ # 1. Do some basic tests assert self.manage_canUseBTreeFolder(), "BTreeFolder2 not installed" assert not self.manage_isUsingBTreeFolder(), "BTreeFolder already in use" # 1. Set up the container root = self.getRoot() _adder = root.manage_addProduct['BTreeFolder2'].manage_addBTreeFolder _adder(id=BTREEFOLDER2_ID) container = getattr(self, BTREEFOLDER2_ID) # 2. Transfer all issues cut = root.manage_cutObjects(ids=root.objectIds(ISSUE_METATYPE)) container.manage_pasteObjects(cut) # 3. Persistently remember this so that we don't have to look # for a BTreeFolder2 instance every time to deduce if we're # storing the issues in a BTree root.btreefolder_storage = True # 4. Copy the internal ID counter dest_key = '_nextid_%s' % ss(container.meta_type).replace(' ','') source_key = '_nextid_%s' % ss(root.meta_type).replace(' ','') if hasattr(root, source_key) and getattr(root, source_key) >= getattr(container, dest_key, 0): # do the copy! container.__dict__[dest_key] = getattr(root, source_key) # 5. Update the ZCatalog and everything else self.UpdateEverything() msg = "Converted to storing issues in BTreeFolder" if REQUEST is None: return msg else: url = root.absolute_url()+'/manage_ManagementForm' url = Utils.AddParam2URL(url, {'manage_tabs_message':msg}) REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'manage_convertFromBTreeFolder') def manage_convertFromBTreeFolder(self, REQUEST=None): """ change back to storing the issues right inside the issue tracker itself""" # 1. Do some basic tests assert self.manage_canUseBTreeFolder(), "BTreeFolder2 not installed" assert self.manage_isUsingBTreeFolder(), "BTreeFolder already in use" # 2. Transfer all issues root = self.getRoot() container = getattr(root, BTREEFOLDER2_ID) cut = container.manage_cutObjects(ids=container.objectIds(ISSUE_METATYPE)) root.manage_pasteObjects(cut) # 3. Persistently remember this so that we don't have to look # for a BTreeFolder2 instance every time to deduce if we're # storing the issues in a BTree root.btreefolder_storage = False # 4. Copy the internal ID counter dest_key = '_nextid_%s' % ss(root.meta_type).replace(' ','') source_key = '_nextid_%s' % ss(container.meta_type).replace(' ','') if hasattr(container, source_key) and getattr(container, source_key) >= getattr(root, dest_key, 0): # do the copy! root.__dict__[dest_key] = getattr(container, source_key) # 5. Remove the Btreefolder if possible if len(container.objectValues()) == 0: root.manage_delObjects([BTREEFOLDER2_ID]) # 6. Update the ZCatalog and everything else root.UpdateEverything() msg = "Converted back to store issues in Issue Tracker instead of BTreeFolder" if REQUEST is None: return msg else: url = root.absolute_url()+'/manage_ManagementForm' url = Utils.AddParam2URL(url, {'manage_tabs_message':msg}) REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'ReplaceEmail') def ReplaceEmail(self, old, new, caseinsensitive=1, REQUEST=None): """ Method that lets you change an occurance of an email address to another. Useful if a frequence user has changed email accout or something. """ if caseinsensitive: old = old.lower() root = self.getRoot() nochanges_issues = 0 nochanges_threads = 0 for issue in root.getIssueObjects(): iemail = issue.email if caseinsensitive: iemail = iemail.lower() if iemail == old: issue.email = new nochanges_issues = nochanges_issues + 1 for thread in issue.objectValues(ISSUETHREAD_METATYPE): temail = thread.email if caseinsensitive: temail = temail.lower() if temail == old: thread.email = new nochanges_threads = nochanges_threads + 1 msg = "Changed %s issues and %s threads"%\ (nochanges_issues, nochanges_threads) if REQUEST is None: return msg else: method = Utils.AddParam2URL desturl = root.absolute_url()+"/manage_ManagementForm" url = method(desturl,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'ManagementTabs') def ManagementTabs(self, whichon='main'): """ return a HTML chunk with tabs """ tabs = (('manage_ManagementForm','Main'), ('manage_ManagementNotifyables','Notifyables'), ('manage_ManagementUsers','Users'), ('manage_ManagementUpgrade','Upgrade'), ('manage_ManagementSpamProtection','Spam protection'), ) tabdicts = [] for tab in tabs: item = {} url, name = tab item['href'] = url item['name'] = name item['current'] = name.lower()==whichon.lower() tabdicts.append(item) page = self.management_tabs return page(self, self.REQUEST, tabdicts=tabdicts) def manage_beforeDelete(self, item, container): """ we're about to be deleted! """ self._old_instance_physicalpath = self.getPhysicalPath() def _postCopy(self, container, op=0): """ Called after the copy is finished to accomodate special cases. The op var is 0 for a copy, 1 for a move. """ if hasattr(self, '_old_instance_physicalpath'): old_path = self._old_instance_physicalpath new_path = self.getPhysicalPath() self._renameOldPaths(old_path, new_path) self.UpdateCatalog() def _renameOldPaths(self, old_path, new_path): """ this issuetracker has changed path from 'old_path' to 'new_path'. Change all the references where this appears. For example, there might be assignments withing issues that point to users who are defined as acl users within this issue tracker. """ old_path_joined = '/'.join(old_path) new_path_joined = '/'.join(new_path) count = {} for issue in self.getIssueObjects(): acl_adder = issue.getACLAdder() if acl_adder.find(old_path_joined) > -1: new_acl_adder = acl_adder.replace(old_path_joined, new_path_joined) issue._setACLAdder(new_acl_adder) count['issues'] = count.get('issues',0) + 1 for thread in issue.getThreadObjects(): acl_adder = thread.getACLAdder() if acl_adder.find(old_path_joined) > -1: new_acl_adder = acl_adder.replace(old_path_joined, new_path_joined) thread._setACLAdder(new_acl_adder) count['threads'] = count.get('threads',0) + 1 for assignment in issue.getAssignments(sort=False): acl_adder = assignment.getACLAdder() if acl_adder.find(old_path_joined) > -1: new_acl_adder = acl_adder.replace(old_path_joined, new_path_joined) assignment._setACLAdder(new_acl_adder) count['assignments'] = count.get('assignments',0) + 1 acl_assignee = assignment.getACLAssignee() if acl_assignee.find(old_path_joined) > -1: new_acl_assignee = acl_assignee.replace(old_path_joined, new_path_joined) assignment._setACLAssignee(new_acl_assignee) count['assignees'] = count.get('assignees',0) + 1 msg = '' if count: for k, v in count.items(): msg += "postcopy fix %s %s\n" %(v, k) if msg: LOG(self.__class__.__name__, INFO, "Post copy fixup: %s" % msg) security.declareProtected(VMS, 'UpdateEverything') def UpdateEverything(self, DestinationURL=None): """ do a DeployStandards(), AssertAllProperties() and UpdateCatalog() """ msgs = [] msgs.append(self.DeployStandards()) msgs.append(self.AssertAllProperties()) msgs.append(self.UpdateCatalog()) msgs.append(self.PrerenderDescriptionsAndComments()) msgs.append(self._cleanTempFolder(implode_if_possible=True)) msgs.append(self.CleanOldSavedFilters(user_excess_clean=True, implode_if_possible=True, clean_keyed_only_filtervaluers=True)) if base_hasattr(self, FILTERVALUEFOLDER_ID): if self.getFilterValuerCatalog() is None: self._setupFilterValuerCatalog() msgs.append('Created ZCatalog for saved filters') msgs.append(self.UpdateFilterValuerCatalog()) msg = '\n'.join([x for x in msgs if x]) if DestinationURL: method = Utils.AddParam2URL params = {'manage_tabs_message':"Everything updated\n\n%s"%msg, } try: pingurl = "http://www.issuetrackerproduct.com/UserStories/ping" pingable = urlopen(pingurl) if pingable: if hasattr(self, 'userstory_plea'): no_previous_pleas = int(getattr(self, 'userstory_plea')) else: no_previous_pleas = 0 if no_previous_pleas < 3: params['userstory'] = 'plea' self.userstory_plea = no_previous_pleas + 1 except: pass url = method(DestinationURL, params) self.REQUEST.RESPONSE.redirect(url) else: return msg security.declarePrivate('_cleanTempFolder') def _cleanTempFolder(self, hours=CLEAN_TEMPFOLDER_INTERVAL_HOURS, implode_if_possible=False): """ remove all relativly old files in the temporary directory """ tempfolder = self._getTempFolder(clean_if_necessary=False) folders2del = [] now = DateTime() for folder in tempfolder.objectValues('Folder'): if now - folder.bobobase_modification_time() > hours/24.0: folders2del.append(folder.getId()) if folders2del: # need to use 'folders2del' here (before the action) # because manage_delObjects() # will reset the list after execution if len(folders2del) < 5: del_info = ', '.join(folders2del) else: del_info = "%s folders in total"%len(folders2del) tempfolder.manage_delObjects(folders2del) msg = "Deleted temp files: " + del_info else: msg = "" if implode_if_possible: # maybe the temp-folder is now totally empty, if so, # delete it if not len(tempfolder.objectValues()): parent = tempfolder.aq_parent folderid = tempfolder.getId() parent.manage_delObjects([folderid]) msg += "\nDeleted temp folder because it was empty" msg = msg.strip() return msg def _getTempFolder(self, clean_if_necessary=True): """ make sure there's a folder called `TEMPFOLDER_ID` in the root """ id = TEMPFOLDER_ID root = self.getRoot() if id not in root.objectIds(['Folder','BTreeFolder2']): title = 'Used for temporary file uploads' if self.manage_canUseBTreeFolder(): _adder = root.manage_addProduct['BTreeFolder2'].manage_addBTreeFolder else: _adder = root.manage_addFolder _adder(id, title) elif clean_if_necessary: # clean it up from old junk self._cleanTempFolder() return getattr(root, id) security.declareProtected(VMS, 'PrerenderDescriptionsAndComments') def PrerenderDescriptionsAndComments(self, REQUEST=None): """ invoke the _prerender_* function on all issues and threads """ count_issues = 0 count_threads = 0 root = self.getRoot() for issue in root.getIssueObjects(): # fix a few possible legacy issues with the issue if isinstance(issue.getTitle(), str): issue._unicode_title() if isinstance(issue.getDescription(), str): issue._unicode_description() if isinstance(issue.fromname, str): issue.fromname = unicodify(issue.fromname) d_before = issue._getFormattedDescription() issue._prerender_description() d_after = issue._getFormattedDescription() if d_before != d_after: count_issues += 1 for thread in issue.getThreadObjects(): # fix a few possible legacy issues with the issue if isinstance(thread.getComment(), str): thread._unicode_comment() if isinstance(thread.fromname, str): thread.fromname = unicodify(thread.fromname) c_before = thread._getFormattedComment() thread._prerender_comment() c_after = thread._getFormattedComment() if d_before != d_after: count_threads += 1 if count_issues and count_threads: if count_issues == 1: msg = "1 issue and " else: msg = "%s issues and " % count_issues if count_threads == 1: msg += "1 followup " else: msg += "%s followups " % count_threads msg += "prerendered" elif not count_threads: if count_issues == 1: msg = "1 issue " else: msg = "%s issues " % count_issues msg += "prerendered" elif not count_issues: if count_threads == 1: msg = "1 followup " else: msg = "%s followups " % count_threads msg += "prerendered" else: msg = "" if REQUEST is None: return msg else: root = self.getRoot() desturl = root.absolute_url() + "/manage_ManagementForm" url = Utils.AddParam2URL(desturl, {'manage_tabs_message':msg}) REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'CleanOldSavedFilters') def CleanOldSavedFilters(self, user_excess_clean=False, implode_if_possible=False, clean_keyed_only_filtervaluers=False, REQUEST=None): """ remove all saved filters that are X days old. If you pass user_excess_clean=True then it goes through how many saved filters each user has. If a user has more than X saved filters, all the >X oldest ones are deleted.""" del_ids = [] treshold = FILTERVALUER_EXPIRATION_DAYS today = DateTime() container = self._getFilterValueContainer() for filtervaluer in container.objectValues(FILTEROPTION_METATYPE): try: age = today - filtervaluer.getModificationDate() except AttributeError: # if the filter valuer doesn't have a mod_date it must be very old # ie. a legacy object that we still need to support age = today - filtervaluer.bobobase_modification_time() if filtervaluer.acl_adder: # If the filtervaluer is done by some posh person who has a Zope # acl user access account, then we give them more breathing space # by increasing the treshold limit quite a lot used_treshold = treshold * 3 elif clean_keyed_only_filtervaluers and filtervaluer.getKey(): # This is quite special, filtervaluers that have a "key" have # that because they don't have an acl_adder, # adder_fromname or adder_email. Ie. users who haven't bothered # to identify themselfs at all. This kind of people glog up the # saved-filters folder with stuff that they might not reuse # because either they don't use the issuetracker more than once # or they don't support cookies (eg. Googlebot). # If this is the case, take out the filtervaluers that are # half-expired (see elif statement above) thus being less # lenient against these kind of objects. treshold = treshold / 2 if age > treshold: del_ids.append(filtervaluer.getId()) filtervaluer.unindex_object() if del_ids: msg = "Deleted %s old saved filters" % len(del_ids) else: msg = "No old saved filters to delete" container.manage_delObjects(del_ids) if not user_excess_clean: if implode_if_possible: if self._implodeFilterValueContainerIfPossible(): msg += "\nDeleted saved filters folder because it was empty" catalog = self.getFilterValuerCatalog() if catalog is not None: catalog.manage_catalogClear() if REQUEST is None: return msg else: root = self.getRoot() desturl = root.absolute_url() + "/manage_ManagementForm" url = Utils.AddParam2URL(desturl, {'manage_tabs_message':msg}) REQUEST.RESPONSE.redirect(url) # Now for an even more anal cleaning. For every user, # we only want them to have a max of FILTERVALUEFOLDER_MAX_PER_USER # filtervaluers in their name. There is actually nothing # stopping a user having more but that's only because we # don't want to annoy them with this restriction when they're # using saved filters. It is only here in the cleanup function # that we care. max_per_user = FILTERVALUER_MAX_PER_USER user_valuers = {} filtervaluers = container.objectValues(FILTEROPTION_METATYPE) sorted_filtervaluers = self.sortSequence(filtervaluers, (('mod_date',),)) # reversing puts the youngest first in the list sorted_filtervaluers.reverse() del_ids = [] for filtervaluer in sorted_filtervaluers: k = [] if filtervaluer.acl_adder: k.append(filtervaluer.acl_adder) if filtervaluer.adder_fromname: k.append(filtervaluer.adder_fromname) if filtervaluer.adder_email: k.append(filtervaluer.adder_email) if filtervaluer.getKey(): k.append(filtervaluer.getKey()) k = ','.join(k) # k is now the user key. Notice that it doesn't matter # how we identified this as long as it's unique. # But these in buckets now if k: if not user_valuers.has_key(k): user_valuers[k] = [filtervaluer.getId()] elif len(user_valuers) > max_per_user: # this one goes into the bin del_ids.append(filtervaluer.getId()) else: user_valuers[k].append(filtervaluer.getId()) # and we're done, let's see what we caught if del_ids: msg += "\nDeleted %s user excessive saved filters" % len(del_ids) container.manage_delObjects(del_ids) if implode_if_possible: if self._implodeFilterValueContainerIfPossible(): msg += "\nDeleted saved filters folder because it was empty" if REQUEST is None: return msg else: root = self.getRoot() desturl = root.absolute_url() + "/manage_ManagementForm" url = Utils.AddParam2URL(desturl, {'manage_tabs_message':msg}) REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'AssertAllProperties') def AssertAllProperties(self, REQUEST=None): """ invoke the assertAllProperties() on all objects """ count = 0 count += self._assertAllProperties() root = self.getRoot() for issue in root.getIssueObjects(): count += issue.assertAllProperties() for thread in issue.objectValues(ISSUETHREAD_METATYPE): count += thread.assertAllProperties() if count: msg = "Made sure %s objects have all properties."%count else: msg = "No objects needed assurance on new properties." if REQUEST is None: return msg else: root = self.getRoot() method = Utils.AddParam2URL desturl = root.absolute_url()+"/manage_ManagementForm" url = method(desturl,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) security.declarePrivate('_assertAllProperties') def _assertAllProperties(self): # sorry about the ugly name """ Return how many properties we made sure we have. Make sure the the root has the correct properties. """ self = self.getRoot() # be certain that we're in the root object count = 0 checks = {'menu_items':DEFAULT_MENU_ITEMS, 'show_id_with_title':DEFAULT_SHOW_ID_WITH_TITLE, 'show_use_accesskeys_option':DEFAULT_SHOW_USE_ACCESSKEYS_OPTION, 'can_add_new_sections':DEFAULT_CAN_ADD_NEW_SECTIONS, 'images_in_menu':DEFAULT_IMAGES_IN_MENU, 'use_estimated_time':DEFAULT_USE_ESTIMATED_TIME, 'use_actual_time':DEFAULT_USE_ACTUAL_TIME, 'include_description_in_notifications':DEFAULT_INCLUDE_DESCRIPTION_IN_NOTIFICATIONS, 'use_tellafriend':DEFAULT_USE_TELLAFRIEND, 'brother_issuetracker_paths':[], 'plugin_paths':[], } for key, default in checks.items(): if not hasattr(self, key): self.__dict__[key] = default count += 1 return count security.declareProtected(VMS, 'DeployStandards') def DeployStandards(self, remove_oldstuff=0, DestinationURL=None, initzcatalog=1): """ copy images and other documents into the instance unless they are already there """ t={} if initzcatalog: t = self.InitZCatalog(t=t) # create folders root = self.getRoot() #for f in ['notifyables', 'www', 'tinymce']: for f in ['notifyables', 'www']: if not f in root.objectIds('Folder'): root.manage_addFolder(f) t[f]='Folder' osj = os.path.join standards_home = osj(package_home(globals()),'standards') self._deployImages(root, standards_home, t=t, remove_oldstuff=remove_oldstuff, skipfolders=('mainbuttons','actionbuttons','.svn','CVS')) www_home = osj(standards_home,'www') self._deployImages(root.www, www_home, t=t, remove_oldstuff=remove_oldstuff, skipfolders=('.svn','CVS')) ##home = osj(standards_home, 'tinymce') ##self._deployImages(root.tinymce, home, ## t=t, remove_oldstuff=remove_oldstuff, ## check_updates=True) ##self._deployDocuments(root.tinymce, home, ## t=t, remove_oldstuff=remove_oldstuff, ## check_updates=True) # perhaps TinyMCE is now installed but 'html' is not a recognized # display format option if self.hasWYSIWYGEditor() and 'html' not in self.display_formats: df = list(self.display_formats) df.append('html') self.display_formats = df msg = "Standard objects deployed\n" if t: for k,v in t.items(): msg += "(%s)\n%s" % (k, v) else: msg = "No standard objects deployed." if DestinationURL: method = Utils.AddParam2URL url = method(DestinationURL,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) else: return msg def _deployImages(self, destination, directory, extensions=['.gif','.ico','.jpg','.png'], t={}, remove_oldstuff=False, check_updates=False, skipfolders=[]): """ do the actual deployment of images in a dir """ # expect 'skipfolders' to be a list of tuple if skipfolders is None: skipfolders = [] elif not isinstance(skipfolders, (tuple, list)): skipfolders = [skipfolders] osj = os.path.join base= getattr(destination,'aq_base',destination) for filestr in os.listdir(directory): if os.path.isdir(osj(directory, filestr)): if filestr in skipfolders: continue if hasattr(base, filestr) and remove_oldstuff: destination.manage_delObjects([filestr]) if not hasattr(base, filestr): destination.manage_addFolder(filestr) t[filestr] = "Folder" new_destination = getattr(destination, filestr) self._deployImages(new_destination, osj(directory, filestr), extensions=extensions, t=t, remove_oldstuff=remove_oldstuff, check_updates=check_updates, skipfolders=skipfolders) elif self._file_has_extensions(filestr, extensions): # take the image id, title = Utils.cookIdAndTitle(filestr) if hasattr(base, id) and remove_oldstuff: destination.manage_delObjects([id]) if hasattr(base, id) and check_updates: # if the new file is different, delete the existing current one this_image = getattr(destination, id) this_length = len(this_image.data) that_image = open(osj(directory, filestr),'rb').read() that_length = len(that_image) if this_length != that_length: destination.manage_delObjects([id]) if not hasattr(base, id): destination.manage_addImage(id, title=title, \ file=open(osj(directory, filestr),'rb').read()) t[id]="Image" def _file_has_extensions(self, filestr, extensions): """ check if a filestr has any of the give extensions """ for extension in extensions: if filestr.find(extension) > -1: return True return False def _deployDocuments(self, destination, directory, extensions=('.js','.css','.html','.htm'), t={}, remove_oldstuff=False, check_updates=False): """ do the actual deployment of images in a dir """ osj = os.path.join base= getattr(destination,'aq_base',destination) for filestr in os.listdir(directory): if os.path.isdir(osj(directory, filestr)): if hasattr(base, filestr) and remove_oldstuff: destination.manage_delObjects([filestr]) if not hasattr(base, filestr): destination.manage_addFolder(filestr) t[filestr] = "Folder" new_destination = getattr(destination, filestr) self._deployDocuments(new_destination, osj(directory, filestr), extensions=extensions, t=t, remove_oldstuff=remove_oldstuff, check_updates=check_updates) elif self._file_has_extensions(filestr, extensions): # take the image id, title = Utils.cookIdAndTitle(filestr) if hasattr(base, id) and remove_oldstuff: destination.manage_delObjects([id]) if hasattr(base, id) and check_updates: this_content = open(osj(directory, filestr)).read() this_content = self._massageDTMLDocumentContent(filestr, this_content) that_content = getattr(destination, id).document_src() if this_content != that_content: destination.manage_delObjects([id]) if not hasattr(base, id): content = open(osj(directory, filestr)).read() content = self._massageDTMLDocumentContent(filestr, content) destination.manage_addDTMLDocument(id, title, file=content) #destination.manage_addImage(id, title=title, \ # file=open(osj(directory, filestr),'rb').read()) t[id]="Document" def _massageDTMLDocumentContent(self, filename, content): """ return the content slightly modified. The purpose of this method is to improve and prepare the document for the usage. If the filename ends in '.js' put some caching header and some DTML code that sets the correct Content-Type. """ if content.lower().find("setHeader('Content-Type')".lower()) == -1: if filename.endswith('.js'): add = '<dtml-call "RESPONSE.setHeader(\'Content-Type\',\'application/x-javascript\')">' elif filename.endswith('.css'): add = '<dtml-call "RESPONSE.setHeader(\'Content-Type\',\'text/css\')">' else: add = None if add: content = add + content.strip() if content.find('doCache(') == -1: content = '<dtml-call "doCache(hours=12)">' + content.strip() return content ## Properties wizard security.declareProtected(VMS, 'manage_PropertiesWizard') def manage_PropertiesWizard(self, REQUEST, *args, **kw): """ Overridden template """ try: firsttime = int(REQUEST.get('firsttime',0)) except: firsttime = 0 stage, msg, error = self._saveFromPropertiesWizard(REQUEST) if msg: kw['manage_tabs_message'] = msg.strip()+'\n' if error: kw['error'] = error kw['stage'] = stage kw['firsttime'] = firsttime file = 'dtml/PropertiesWizard' name = 'PropertiesWizard' return apply(DTMLFile(file, globals(), __name__=name ).__of__(self), (), kw) def _saveFromPropertiesWizard(self, request): """ return message a dict of submission error """ try: submit = int(request.get('submit',1)) except: submit = 1 try: stage = int(request.get('stage',0)) except: stage = 0 try: firsttime = int(request.get('firsttime',0)) except: firsttime = 0 msg = None error = {} if not submit: return stage, msg, error if stage == 1 and firsttime: msg = [] # attempt to save properties from stage 1 whatuse = ss(request.get('whatuse','softwaredevelopment')) if whatuse == 'helpdesk_external': sections = ['General','Front office','Back office','Other'] self.sections_options = sections msg.append("Set section options to: " + ', '.join(sections)) types = ['general', 'announcement', 'idea', 'content', 'feature request','question','other'] self.types = types msg.append("Set type options to: " +', '.join(types)) if not self.allow_subscription: self.allow_subscription = True msg.append("Allowed issue subscription") if not self.show_confidential_option: self.show_confidential_option = True msg.append("Allowed for confidential issues") if not self.show_hideme_option: self.show_hideme_option = True msg.append("Allowed for \"hide me\" option") elif whatuse == 'helpdesk_internal': sections = ['General','Back office','Other'] self.sections_options = sections msg.append("Set section options to: " + ', '.join(sections)) types = ['general', 'announcement', 'idea', 'content', 'feature request','question','other'] self.types = types msg.append("Set type options to: " +', '.join(types)) if self.isViewPermissionOn(): self.manage_ViewPermissionToggle() msg.append("Switched off Anonymous access") if not self.UseIssueAssignment(): self.manage_UseIssueAssignmentToggle() msg.append("Switched on Issue Assignment") if not self.private_statistics: self.private_statistics = True msg.append("Allow statistics") if self.encode_emaildisplay: self.encode_emaildisplay = False msg.append("Email addresses not encoded") if not self.show_always_notify_status: self.show_always_notify_status = True msg.append("Always show who was notified") if not self.CanAddNewSections(): self.can_add_new_sections = True msg.append("Can add new sections with each issue") else: # first time typical sections sections = ['General','Database','Interface','Support', 'Documentation','Other'] self.sections_options = sections msg.append("Set section options to: " + ', '.join(sections)) types = ['general','announcement','bug report', 'feature request','content request', 'usability','other'] self.types = types msg.append("Set type options to: " +', '.join(types)) if not self.UseIssueAssignment(): self.manage_UseIssueAssignmentToggle() msg.append("Switched on Issue Assignment") if not self.show_always_notify_status: self.show_always_notify_status = True msg.append("Always show who was notified") if self.no_followup_fileattachments == 0: self.no_followup_fileattachments = 1 _m = "Allowed for at least one file " _m += "attachment on follow up" msg.append(_m) msg = '\n'.join(msg) # can now move on to stage 2 stage += 1 elif stage == 2: msg = [] sections_options = request.get('sections_options',[]) # clean them a bit sections_options = [x.strip() for x in sections_options if x.strip()] sections_options = Utils.uniqify(sections_options) if not sections_options: error['sections_options'] = "No sections entered" else: self.sections_options = sections_options msg = "Set section options to: " + ', '.join(sections_options) stage += 1 elif stage == 3: defaultsections = request.get('defaultsections',[]) if not defaultsections: request.set('defaultsections', [self.sections_options[0]]) m = "None selected, try %s?"%self.sections_options[0] error['defaultsections'] = m else: # filter out unrecognized ones checked = [] for each in defaultsections: if each in self.sections_options: checked.append(each) if not checked: m = "None of selected was recognized" error['defaultsections'] = m else: self.defaultsections = checked if len(checked) > 1: msg = "Set default sections to: " else: msg = "Set default section to: " msg += ', '.join(checked) stage += 1 elif stage == 4: types = request.get('types',[]) urgencies = request.get('urgencies',[]) # clean them a bit types = [x.strip() for x in types] urgencies = [x.strip() for x in urgencies] while '' in types: types.remove('') while '' in urgencies: urgencies.remove('') types = Utils.uniqify(types) urgencies = Utils.uniqify(urgencies) if not types: error['types'] = "None entered" if not urgencies: error['urgencies'] = "None entered" if types and urgencies: self.types = types self.urgencies = urgencies msg = "Set types to: " + ', '.join(types) + '\n' msg += "Set urgencies to: " + ', '.join(urgencies) stage += 1 elif stage == 5: default_type = request.get('default_type','').strip() ok = True if default_type not in self.types: error['default_type'] = "Unrecognized" ok = False default_urgency = request.get('default_urgency','').strip() if default_urgency not in self.urgencies: error['default_urgency'] = "Unrecognized" ok = False if ok: self.default_type = default_type self.default_urgency = default_urgency msg = "Default type set to: " + default_type + '\n' msg += "Default urgency set to: " + default_urgency stage += 1 elif stage == 6: _default = self.getDefaultSortorder() default_sortorder = request.get('default_sortorder', _default) if default_sortorder not in self.getDefaultSortorderOptions(): error['default_sortorder'] = "Unrecognized option" ok = False else: self.default_sortorder = default_sortorder _translated = self.translateSortorderOption(default_sortorder) msg = "Default sort order set to %s"%_translated stage += 1 elif stage == 8: always_notify = request.get('always_notify',[]) always_notify = [x.strip() for x in always_notify] while '' in always_notify: always_notify.remove('') # Check that each is either a notifyable or a valid # email address. notifyables = self.getNotifyables() notifyables_names = [x.getName() for x in notifyables] email_checker = Utils.ValidEmailAddress checked = [] invalids = [] for each in always_notify: if each in notifyables_names: checked.append(each) elif Utils.ValidEmailAddress(each): checked.append(each) else: invalids.append(each) self.always_notify = checked if invalids: m = "Invalid entries: "+ ', '.join(invalids) error['always_notify'] = m else: msg = "Set to always be notified: " msg += ', '.join(checked) stage += 1 elif stage == 9: sitemaster_name = request.get('sitemaster_name','').strip() sitemaster_email = request.get('sitemaster_email','').strip() ok = True if not sitemaster_name: error['sitemaster_name'] = "Empty" ok = False if sitemaster_email != DEFAULT_SITEMASTER_EMAIL and \ not Utils.ValidEmailAddress(sitemaster_email): error['sitemaster_email'] = "Invalid" ok = False if ok: self.sitemaster_name = sitemaster_name self.sitemaster_email = sitemaster_email msg = "Site name set to: %s\n"%sitemaster_name msg +="Site email set to: %s"%sitemaster_email stage += 1 elif stage==10: no_fileattachments = request.get('no_fileattachments',1) no_followup_fileattachments = request.get('no_followup_fileattachments',1) display_date = request.get('display_date','').strip() show_dates_cleverly = bool(request.get('show_dates_cleverly',0)) ok = True try: no_fileattachments = int(no_fileattachments) except ValueError: error['no_fileattachments'] = "Not a number" ok = False try: no_followup_fileattachments = int(no_followup_fileattachments) except ValueError: error['no_followup_fileattachments'] = "Not a number" ok = False if not display_date: error['display_date'] = "No display date format" ok = False # nothing to test on the show_dates_cleverly if ok: self.no_fileattachments = no_fileattachments self.no_followup_fileattachments = no_followup_fileattachments self.display_date = display_date self.show_dates_cleverly = show_dates_cleverly msg = "" if no_fileattachments == 0: msg += "No file attachments to issues.\n" elif no_fileattachments == 1: msg += "One file attachment to issues.\n" else: msg += "%s file attachments to issues.\n"%no_fileattachments if no_followup_fileattachments == 0: msg += "No file attachments to follow ups.\n" elif no_followup_fileattachments == 1: msg += "One file attachment to follow ups.\n" else: msg += "%s file attachments to follow ups.\n"%no_followup_fileattachments msg += "Displays date in this format:" msg += DateTime().strftime(display_date) if show_dates_cleverly: msg += " (and dates are shown differently depending on how far from today)" msg = msg.strip() stage += 1 elif stage == 11: bool_keys = ('allow_issueattrchange', 'allow_subscription', 'use_tellafriend', 'private_statistics', 'encode_emaildisplay', 'show_always_notify_status', 'show_confidential_option', 'show_hideme_option', 'show_issueurl_option', 'can_add_new_sections', 'images_in_menu', ) for key in bool_keys: try: value = bool(int(request.get(key, getattr(self, key)))) except: continue self.__dict__[key] = value msg = "Yes/No questions set." stage = 12 else: stage += 1 #pass #raise "WhatNow", "What do we do now?" if stage == 1 and not firsttime: stage = 2 if msg == []: msg = None return stage, msg, error def ShowError(self, error, id, htmlwrap=1): """ show the error (used only by PropertiesWizard.dtml """ if error and error.has_key(id): s = error.get(id) if htmlwrap: s = '<span class="submiterror">%s</span><br />'%s return s else: return s else: return '' ## Users part of Management related def getAllIssueUserFolders(self): """ return all objects that are IssueUserFolders """ return self.superValues(ISSUEUSERFOLDER_METATYPE) def getAllIssueUsers(self, userfolders=None, filter=1, exclude_assignee=None): """ return all the acl users as identifiers """ if userfolders is None: userfolders = self.getAllIssueUserFolders() elif not isinstance(userfolders, list): userfolders = [userfolders] users = [] if filter: blacklist = self.getIssueAssignmentBlacklist() else: blacklist = [] for userfolder in userfolders: userfolderpath = userfolder.getIssueUserFolderPath() for username, user in userfolder.data.items(): username = userfolderpath+','+username if username not in blacklist: # skip if exclude_assignee and username == exclude_assignee: continue users.append({'userfolder':userfolder, 'user':user, 'identifier':username}) return users security.declareProtected(VMS, 'manage_UseIssueAssignmentToggle') def manage_UseIssueAssignmentToggle(self, DestinationURL=None): """ inverse the value of self.use_issue_assignment """ self.use_issue_assignment = not self.UseIssueAssignment() if self.UseIssueAssignment(): msg = "Issue Assignment switched on" else: msg = "Issue Assignment switched off" if DestinationURL: method = Utils.AddParam2URL url = method(DestinationURL,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected(VMS, 'manage_AddToBlacklist') def manage_AddToBlacklist(self, add_identifiers, DestinationURL=None): """ add some identifiers to the blacklist """ before = self.getIssueAssignmentBlacklist(check_each=True) blacklist = before + add_identifiers checked = [] for identifier in blacklist: if identifier not in checked: checked.append(identifier) self._assignment_blacklist = checked if len(add_identifiers) == 1: msg = "User blacklisted" else: msg = "Users blacklisted" if DestinationURL: method = Utils.AddParam2URL url = method(DestinationURL,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected(VMS, 'manage_RemoveFromBlacklist') def manage_RemoveFromBlacklist(self, remove_identifiers, DestinationURL=None): """ remove some identifiers from the blacklist """ before = self.getIssueAssignmentBlacklist() checked = [] for identifier in before: if identifier not in remove_identifiers: checked.append(identifier) self._assignment_blacklist = checked if len(remove_identifiers) == 1: msg = "User blacklisted" else: msg = "Users blacklisted" if DestinationURL: method = Utils.AddParam2URL url = method(DestinationURL,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) else: return msg def isAnonymous(self): """ return true if the user is not logged into zope in any way. """ username = getSecurityManager().getUser().getUserName() return username.lower().replace(' ','') == 'anonymoususer' security.declareProtected(VMS, 'isViewPermissionOn') def isViewPermissionOn(self): """ return True if View permission is on for Anonymous """ return not not self.acquiredRolesAreUsedBy('View') security.declareProtected(VMS, 'manage_ViewPermissionToggle') def manage_ViewPermissionToggle(self, DestinationURL=None): """ Change the Aquire attribute for the View permission """ viewpermission_on = self.isViewPermissionOn() roles_4_view = ['Manager', IssueTrackerManagerRole, IssueTrackerUserRole] self.manage_permission('View', roles=roles_4_view, acquire=not viewpermission_on) if viewpermission_on: msg = "View permission disabled for Anonymous" else: msg = "View permission enabled for Anonymous" if DestinationURL: method = Utils.AddParam2URL url = method(DestinationURL,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) else: return msg ## Useful root instance methods def getRoot(self): """ Get the root instance object """ mtype = ISSUETRACKER_METATYPE r = self while r.meta_type != mtype: r = aq_parent(aq_inner(r)) return r def titleTag(self): """ return suitable content for <title> tag """ root_title = self.getRoot().title_or_id() title = root_title if self.meta_type == ISSUE_METATYPE: prefix = "" if Utils.niceboolean(self.REQUEST.get('autorefresh')): prefix = _("(auto refreshed)") if self.ShowIdWithTitle(): title = "%s %s - #%s %s" title = title % (prefix, root_title, self.getIssueId(), self.getTitle()) else: title = "%s %s - %s" % (prefix, root_title, self.getTitle()) else: page = self.REQUEST.URL.split('/')[-1] _rtdict = {'root_title':root_title} if page == 'ListIssues': title = _('%(root_title)s - List Issues') % _rtdict elif page == 'CompleteList': title = _('%(root_title)s - Complete List') % _rtdict elif page == 'AddIssue': if self.REQUEST.form.has_key('previewissue'): title = _('Preview before adding issue - %(root_title)s') % _rtdict else: title = _('%(root_title)s - Add Issue') % _rtdict elif page == 'QuickAddIssue': title = _('%(root_title)s - Quick Add Issue') % _rtdict elif page == 'User': title = '%(root_title)s - User' % _rtdict elif page == 'About.html': title = _('About the IssueTrackerProduct version %s') title = title % self.getIssueTrackerVersion() elif page == 'SubmitIssue': if self.REQUEST.get('HTTP_REFERER').find('QuickAddIssue'): title = _('%(root_title)s - Quick Add Issue') % _rtdict else: title = _('%(root_title)s - Add Issue') % _rtdict elif page == 'What-is-WYSIWYG': title = "WYSIWYG = What You See Is What You Get" elif page == 'What-is-StructuredText': title = "About Structured Text" if isinstance(title, basestring): # legacy return Utils.html_entity_fixer(title) else: # new way return title def hasWYSIWYGEditor(self): """ return true if we have a WYSIWYG editor available """ return self.getWYSIWYGEditor() is not None def getWYSIWYGEditor(self): """ return the ztinymce configuration with the expected name """ ztinymce_conf_id = 'tinymce-issuetracker.conf' if hasattr(self.getRoot(), ztinymce_conf_id): return getattr(self.getRoot(), ztinymce_conf_id) return None def getCookiekey(self, which): """ return the cookiekey constants depending on key """ which_orig = which match_decorate = lambda x: x.lower().strip().replace('_','').replace('-','') which = match_decorate(which) keys = {'name': NAME_COOKIEKEY, 'fullname': NAME_COOKIEKEY, 'email': EMAIL_COOKIEKEY, 'displayformat': DISPLAY_FORMAT_COOKIEKEY, 'sortorder': SORTORDER_COOKIEKEY, 'sortorderreverse': SORTORDER_REVERSE_COOKIEKEY, 'draftissueids': DRAFT_ISSUE_IDS_COOKIEKEY, 'draftthreadids': DRAFT_THREAD_IDS_COOKIEKEY, 'autologin': AUTOLOGIN_COOKIEKEY, 'useaccesskeys': USE_ACCESSKEYS_COOKIEKEY, 'saved-filters': SAVED_FILTERS_COOKIEKEY, 'remember_savedfilter_persistently': REMEMBER_SAVEDFILTER_PERSISTENTLY_COOKIEKEY, 'draft_followup_ids': DRAFT_THREAD_IDS_COOKIEKEY, 'show_nextactions': SHOW_NEXTACTIONS_COOKIEKEY, } for k, v in keys.items(): if match_decorate(k) == which: return v if self.doDebug(): debug("Unable to find cookiekey for %s" % which_orig, steps=4) def __before_publishing_traverse__(self, object, request=None): """ sort things out before publising object """ self.get_environ() def get_environ(self): """ Populate REQUEST as appropriate """ request = self.REQUEST stack = request['TraversalRequestNameStack'] popped = [] _special = 'REQUEST' # things to pop out queryitems = ({'key':'start', 'mkey':'start', 'type':'int'}, {'key':'sortorder', 'mkey':'sortorder', 'type':'string'}, {'key':'reverse', 'mkey':'reverse', 'type':'boolean'}, {'key':'show', 'mkey':'show', 'type':'string'}, {'key':'report', 'mkey':'report', 'type':'string'}, ) splitter = '-' if stack: stack_copy = stack[:] found_item = 1 for each in range(len(stack_copy)): found_item = 0 stack_item = stack_copy[each] for each in queryitems: key, value = each['key'], each.get('mkey') if value is None and stack_item==key: # this is a valueless item found_item = 1 request.set(key, 1) elif stack_item.startswith("%s%s"%(key,splitter)) \ and value==_special: found_item = 1 first_key = stack_item.replace("%s%s"%(key,splitter),'') try: key, value = first_key.split(splitter,1) value = int(value) request.set(key, value) except ValueError: try: key, value = first_key.split(splitter,1) request.set(key, value) except: pass elif stack_item.startswith("%s%s"%(key,splitter)): found_item = 1 replace_what = "%s%s"%(key,splitter) if each['type']=='boolean': key = stack_item.replace(replace_what,'') key = Utils.niceboolean(key) elif each['type']=='int': key = int(stack_item.replace(replace_what,'')) else: key = stack_item.replace(replace_what,'') request.set(value, key) if found_item: stack.remove(stack_item) popped.append(stack_item) request.set('popped',popped) ## General for file attachments to issues def getFileattachmentInput(self, index, initsize=40): """ return either a file input field or a keep option """ request = self.REQUEST input_field = '<input size="%s" name="fileattachment:list" ' input_field += 'type="file" />' icon_html = '<img hspace="2" src="%s" alt="File" '\ 'title="File size %s" border="0" />' if request.has_key(TEMPFOLDER_REQUEST_KEY): upload_folder = request[TEMPFOLDER_REQUEST_KEY] # Maybe the actual folder doesn't exist any more tempfolder = self._getTempFolder() if upload_folder is None or not safe_hasattr(tempfolder, upload_folder): return input_field % initsize files = tempfolder[upload_folder].objectValues('File') try: file = files[index] file_src = self.getFileIconpath(file.getId()) file_size = self.ShowFilesize(file.getSize()) icon = icon_html%(file_src, file_size) confirm_title = _("Tick if you want to keep this file attachment") confirm = '<input type="checkbox" checked="checked" ' confirm += 'name="confirm_fileattachment:list" ' confirm += 'value="%s" title="%s" />'%(file.getId(), confirm_title) icon = '%s<a href="%s" title="File size %s">%s%s (%s)</a>'%\ (confirm, file.absolute_url(), file_size, icon, file.getId(), file_size) return icon except: return input_field % initsize else: return input_field % initsize def _uploadTempFiles(self): """ Attempt to upload fileattachments to temp-folder and stick some information in the REQUEST """ request = self.REQUEST temp_folder_id = None rkey = TEMPFOLDER_REQUEST_KEY # first, delete all unconfirmed files self._removeUnConfirmedFiles() if request.get(rkey, None) not in [None,'']: temp_folder_id = request.get(rkey) if request.has_key('fileattachment'): files = request.get('fileattachment') if not isinstance(files, (tuple, list)): files = [files] # fileattachment is a list, deal with each item for file in files: if self._isFile(file): if temp_folder_id is None: temp_folder_id = self._generateTempFolder() temp_folder = self._getTempFolder()[temp_folder_id] filename = getattr(file, 'filename') id=filename[max(filename.rfind('/'), filename.rfind('\\'), filename.rfind(':'), )+1:] if id.startswith('_'): id=id[1:] id = Utils.badIdFilter(id) temp_folder.manage_addFile(id, file=file) fileobject = getattr(temp_folder, id) if self._canCreateThumbnail(fileobject): try: self._createThumbnail(fileobject) except IOError: # we failed to create thumbnail not good. # A log message will already have been # sent. pass # This tests whether any files were uploaded if temp_folder_id is not None: request.set(rkey, temp_folder_id) return temp_folder_id security.declarePublic('_removeUnConfirmedFiles') def _removeUnConfirmedFiles(self): """ if we have a tempfolder with files that don't have a matching confirm, then delete them """ request = self.REQUEST rkey = TEMPFOLDER_REQUEST_KEY if request.get(rkey, None) not in [None,'']: temp_folder = self._getTempFolder()[request.get(rkey)] confirms = self._getConfirmFileattachments() un_upload_ids = [] for fileid in temp_folder.objectIds('File'): if not fileid in confirms: un_upload_ids.append(fileid) self._deleteTempFiles(temp_folder, un_upload_ids) # Anything left now? if len(temp_folder.objectIds('File'))==0: request.set(rkey, None) self._getTempFolder().manage_delObjects([temp_folder.getId()]) def _deleteTempFiles(self, source, ids): """ simply delete some files """ source.manage_delObjects(ids) def _isFile(self, file): """ Check if Publisher file is a real file """ if hasattr(file, 'filename'): if getattr(file, 'filename').strip() != '': # read 1 byte if file.read(1) == "": m = _(u"Filename provided (%s) but not file content") m = m % getattr(file, 'filename') raise NotAFileError, m else: file.seek(0) #rewind file return True else: return False else: return False security.declarePublic('_generateTempFolder') def _generateTempFolder(self): """ Create a folder in temp_folder with randomish id and return its id """ root = self._getTempFolder() timestamp = str(int(self.ZopeTime())) randstr = self.getRandomString(length=3, numbersonly=1) rand_id_start = "uploadtmp-it-%s"%timestamp rand_id = "%s-%s"%(rand_id_start, randstr) while hasattr(root, rand_id): new_rand_str = self.getRandomString(length=3, numbersonly=1) rand_id = "%s-%s"%(rand_id_start, new_rand_str) try: root.manage_addFolder(rand_id) tempfolder = getattr(root, rand_id) except "Unauthorized": LOG(self.__class__, PROBLEM, "Could not create temporary folder") return rand_id def getFileattachmentContainer(self, only_temporary=0): """ if TEMPFOLDER_REQUEST_KEY is set in REQUEST return folder object, otherwise return self. """ request = self.REQUEST rkey = TEMPFOLDER_REQUEST_KEY if request.has_key(rkey) and request.get(rkey) is not None: return getattr(self._getTempFolder(), request[rkey]) elif only_temporary: return None else: return self def showFileattachments(self, container=None, only_temporary=0): """ return HTML with the file attachments """ if container is None and only_temporary: container = self.getFileattachmentContainer(only_temporary=1) if not container: return '' elif container is None: # find then manually if self.meta_type == ISSUE_METATYPE: container = self if not container: return '' files = container.objectValues('File') if not files: return '' html = [] for file in files: url = file.absolute_url() url = self.relative_url(url) size = self.ShowFilesize(file.getSize()) alt = "File size: %s"%size href = '<a href="%s" rel="nofollow" title="%s">'%(url, alt) _html = '%s<img src="%s" alt="%s" title="%s" border="0" ' _html += 'class="fileatt" />' thumbid = 'thumbnail--%s'%file.getId() if hasattr(container, thumbid) and \ getattr(container, thumbid).meta_type == 'Image': src = getattr(container, thumbid).absolute_url_path() else: src = self.getFileIconpath(file.getId()) _html = _html%(href, src, alt, alt) _html += '</a>\n' file_id = file.getId() if len(file_id) > 50: file_id = file_id[:25]+'...'+file_id[-25:] _html += '%s%s</a>'%(href, self.HighlightQ(file_id, highlight_digits=True)) _html += ' <span class="shade"> (%s)</span>\n'%size html.append(_html) return '<br clear="left" />\n'.join(html)+'<br clear="left"/>' def nullifyTempfolderREQUEST(self): """ if request has tempfolder, make it None """ request = self.REQUEST rkey = TEMPFOLDER_REQUEST_KEY if request.has_key(rkey): request.set(rkey, None) ## Using ACL objects def getACLCookieNames(self): """ return acl_cookienames dict property """ return getattr(self, 'acl_cookienames', {}) def getACLCookieEmails(self): """ return acl_cookieemails dict property """ return getattr(self, 'acl_cookieemails', {}) def getACLCookieDisplayformats(self): """ return acl_cookiedisplayformats dict property """ return getattr(self, 'acl_cookiedisplayformats', {}) def setACLCookieName(self, fromname): """ append to acl_cookienames """ acluser = self._getACLUserName() if acluser: prev = self.getACLCookieNames() prev[acluser] = fromname self.acl_cookienames = prev def setACLCookieEmail(self, email): """ append to acl_cookieemails """ acluser = self._getACLUserName() if acluser: prev = self.getACLCookieEmails() prev[acluser] = email self.acl_cookieemails = prev def setACLCookieDisplayformat(self, displayformat): """ append to acl_cookiedisplayformats """ assert displayformat in self.display_formats, \ "Invalid displayformat value %r" % displayformat acluser = self._getACLUserName() if acluser: prev = self.getACLCookieDisplayformats() prev[acluser] = displayformat self.acl_cookiedisplayformats = prev def _getACLUserName(self): """ return ACL username or None """ usr = getSecurityManager().getUser().getUserName() if usr.lower().replace(' ','')=='anonymoususer': return None else: return usr ## Adding an Issue def fixSectionsSubmission(self): """ here's a special script that converts 'section' into ['section'] if present and 'sections' if not present. """ request = self.REQUEST if not request.has_key('sections') and request.get('section'): request.set('sections', [request.get('section')]) return True return False security.declareProtected(AddIssuesPermission, 'SubmitIssue') def SubmitIssue(self, REQUEST): """ This is the method to create an Issue Tracker Issue. It relies only on the REQUEST object. 1) Check data 2) Try to create issue 2a) If success, RESPONSE.redirect to issue plus Thank you message 2b) If failure, print failed data and urge to submit again """ request = self.REQUEST SubmitError = {} has_cookie = self.has_cookie get_cookie = self.get_cookie set_cookie = self.set_cookie # # Tune the data a bit # # strip whitespace for property in ['title','fromname','email', 'url2issue','display_format']: request[property] = request.get(property,'').strip() # Special treatment needed in case STX is used upon display request['description'] = request.get('description','').strip()+' ' email_cookiekey = self.getCookiekey('email') name_cookiekey = self.getCookiekey('name') display_format_cookiekey = self.getCookiekey('display_format') # use cookie if not else specified # assume that it is not a ACL user who adds the issue acl_adder = None issueuser = self.getIssueUser() cmfuser = self.getCMFUser() zopeuser = self.getZopeUser() if issueuser: acl_adder = issueuser.getIssueUserIdentifierString() if request.get('display_format'): if request.get('display_format') in self.display_formats: issueuser.setDisplayFormat(request.get('display_format')) elif zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_adder = ','.join([path, name]) _invalid_name_chars = re.compile('|'.join([re.escape(x) for x in list('<>;\\')])) if issueuser and issueuser.getEmail(): request['email'] = issueuser.getEmail() elif cmfuser and cmfuser.getProperty('email'): request['email'] = cmfuser.getProperty('email') elif not request.get('email','') and get_cookie(email_cookiekey): request['email'] = get_cookie(email_cookiekey) elif not request.get('email','') and self.getSavedUser('email'): request['email'] = self.getSavedUser('email') if issueuser and issueuser.getFullname(): request['fromname'] = issueuser.getFullname() elif cmfuser and cmfuser.getProperty('fullname'): request['fromname'] = cmfuser.getProperty('fullname') elif not request.get('fromname','') and get_cookie(name_cookiekey): request['fromname'] = get_cookie(name_cookiekey) elif not request.get('fromname','') and self.getSavedUser('fromname'): request['fromname'] = self.getSavedUser('fromname') # this prevents dodgy XSS attempts if _invalid_name_chars.findall(request['fromname']): SubmitError['fromname'] = u'Contains not allowed characters' if _invalid_name_chars.findall(request['email']): SubmitError['email'] = u'Contains not allowed characters' if not request.get('display_format','').strip(): request['display_format'] = self.getSavedTextFormat() newsection = None if request.get('newsection'): ns = request.get('newsection').strip() if ns and ns != 'New section...': if ns in self.sections_options: request.set('newsection','') else: newsection = ns # append the default sections if not specified if len(request.get('sections',[])) == 0 and not newsection: request['sections'] = self.defaultsections # # Check data # if not request.get('title','').strip(): SubmitError['title'] = _("Empty") elif self.DisallowDuplicateIssueSubjects(): this_subject = ss(request.get('title').strip()) for issue in self.getIssueObjects(): if ss(issue.getTitle()) == this_subject: link = '<a href="%s">#%s</a>' % (issue.absolute_url_path(), issue.getId()) SubmitError['title'] = _("Issue subject already used in %s" % link) break description_purified = Utils.SimpleTextPurifier(request.get('description','')) if not description_purified: SubmitError['description'] = _("Empty") elif self.containsSpamKeywords(request.get('description',''), verbose=True): SubmitError['description'] = _("Contains spam keywords") valid_emailaddress = 1 # to prevent problems with sending mail if not self.ValidEmailAddress(request.get('email','')): valid_emailaddress = 0 # Check issue assignment assignee = None if request.get('assignee'): ok_assignees = [x['identifier'] for x in self.getAllIssueUsers()] if not self.UseIssueAssignment(): SubmitError['assignee'] = _("Issue assignment disabled") elif request.get('assignee') in self.getIssueAssignmentBlacklist(): SubmitError['assignee'] = _("Invalid assignee") elif request.get('assignee') in ok_assignees: assignee = request.get('assignee') # Check that all attempts of file attachments really are files if request.get('fileattachment', []): fake_fileattachments = self._getFakeFileattachments(request.get('fileattachment')) if fake_fileattachments: m = _("Filename entered but no actual file content") SubmitError['fileattachment'] = m # Check the spambot prevention if self.useSpambotPrevention(): captcha_numbers = request.get('captcha_numbers','').strip() captchas_used = request.get('captchas') if isinstance(captchas_used, basestring): captchas_used = [captchas_used] if not captcha_numbers: m = _("Enter the numbers shown to that you are not a spambot") SubmitError['captcha_numbers'] = m else: errors = None for i, nr in enumerate(captcha_numbers): try: if int(nr) != int(self.captcha_numbers_map.get(captchas_used[i])): errors = True break except TypeError: logger.warn("Couldn't make %r or %r into ints" % (nr, self.captcha_numbers_map.get(captchas_used[i]))) errors = True break except ValueError: errors = True break if errors: # use this oppurtunity to clean up what they tried to enter captcha_numbers = request.get('captcha_numbers','').strip() captcha_numbers = re.sub('[^\d]','', captcha_numbers).strip() request.set('captcha_numbers', captcha_numbers) m = _("Incorrect numbers matching") SubmitError['captcha_numbers'] = m else: self._rememberProvenNotSpambot() # Look for a script or something that plugs in to the IssueTrackerProduct # if you in your customization want to validate your own things if safe_hasattr(self, 'pre_SubmitIssue'): script = getattr(self, 'pre_SubmitIssue') result = script() if isinstance(result, dict): SubmitError.update(result) if SubmitError: if request.get('previewissue'): request.set('previewissue', False) if request.get('addform','')=='quick': page = self.QuickAddIssue else: page = self.AddIssue return page(REQUEST, SubmitError=SubmitError) # # Let's submit the issue! # # if these are valid, save them if request.get('fromname') and not issueuser: set_cookie(self.getCookiekey('name'), request.get('fromname')) self.setACLCookieName(request.get('fromname')) if valid_emailaddress and not issueuser: set_cookie(self.getCookiekey('email'), request.get('email')) self.setACLCookieEmail(request.get('email')) if request.get('display_format') in self.display_formats \ and not issueuser: if request.get('display_format') in self.display_formats: set_cookie(self.getCookiekey('display_format'), request.get('display_format')) self.setACLCookieDisplayformat(request.get('display_format')) # filter out empty item from sections sections_newlist = self.cleanSectionsList(request.get('sections', [])) if not isinstance(sections_newlist, list): sections_newlist = [sections_newlist] sections_newlist = [x.strip() for x in sections_newlist if x.strip()] if newsection and self.CanAddNewSections(): sections_newlist.insert(0, newsection) _options = self.sections_options _options.append(newsection) self.sections_options = _options # add all the properties _rfg = request.form.get _rg = request.get title = unicodify(_rg('title')) fromname = unicodify(_rg('fromname')) email = _rg('email') url2issue = _rg('url2issue') type_ = _rg('type') urgency = _rg('urgency') description = unicodify(_rg('description')) display_format = _rg('display_format') confidential = int(_rg('confidential',0)) hide_me = int(_rg('hide_me',0)) status = _rfg('status', self.getStatuses()[0]) sections = sections_newlist # Let's massage up the description a bit description = description.strip() if display_format == 'html': while description.endswith('<p> </p>'): description = description[:-len('<p> </p>')].strip() while description.startswith('<p> </p>'): description = description[len('<p> </p>'):].strip() # # before we submit the issue, let's just check that it # hasn't been submitted before. This can happen if people # accidently press the Save Issue button twice. # _existing_issue = self._check4Duplicate(title, description, sections, type_, urgency) if _existing_issue: url = _existing_issue.absolute_url() url += '?NewIssue=Submitted' if _rfg('draft_issue_id'): self._dropDraftIssue(_rfg('draft_issue_id')) return self.REQUEST.RESPONSE.redirect(url) prefix = self.issueprefix genid = self.generateID(self.randomid_length, prefix, incontainer=self._getIssueContainer()) # Do the actual object adding cIO = self.createIssueObject issue = cIO(genid, request.title, status, type_, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, acl_adder=acl_adder) # remember it #self.RememberAddedIssue(genid) self.RememberRecentIssue(genid, 'added') if _rfg('draft_issue_id'): self._dropDraftIssue(_rfg('draft_issue_id')) if self.SaveDrafts(): # (see bug report on http://real.issuetrackerproduct.com/0126) self._dropMatchingDraftIssues(issue) # Also upload the fileattachments self._moveTempfiles(issue) # upload new file attachments if request.get('fileattachment', []): self._uploadFileattachments(issue, request.get('fileattachment')) # catalog it issue.index_object() # create assignment object if assignee is not None: _send_email = False if _rfg('notify-assignee'): _send_email = True issue.createAssignment(assignee, send_email=_send_email) # tune some exisiting data if not newsection: # when adding a new section, don't do this self._moveUpSections(sections) # Look for a script to call after the creation of the issue if safe_hasattr(self, 'post_SubmitIssue'): script = getattr(self, 'post_SubmitIssue') script(issue) # tell the people who wants to know if 1: #try: self.sendAlwaysNotify(issue, email=email, assignee=assignee) else: #except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass #typ, val, tb = sys.exc_info() #_classname = self.__class__.__name__ #_methodname = inspect.stack()[1][3] logger.error('Could not send always-notify emails', exc_info=True) # Where to next? #redirect_url = '%s?NewIssue=Submitted'%(issue.absolute_url()) redirect_url = issue.absolute_url() request.RESPONSE.redirect(redirect_url) def _check4Duplicate(self, title, description, sections, type, urgency, email_message_id=None ): """ check if there is an exact replica of this issue """ for issue in self.getIssueObjects(): # most basic test, the title if unicodify(issue.title) == title: # potentially match email 'Message-Id' if email_message_id and issue.getEmailMessageId(): if ss(email_message_id)==ss(issue.getEmailMessageId()): return issue # match description, sections, type and urgencies if unicodify(issue.description) == description and \ issue.sections == sections and \ issue.type == type and \ issue.urgency == urgency: return issue return None security.declareProtected(AddIssuesPermission, 'SubmitIssue') def createIssueObject(self, id, title, status, type_, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, issuedate=None, index=0, acl_adder=None, submission_type='', email_message_id=None): """ wrap the the self._createIssueObject() method """ if id is None or id=='': # create id prefix = self.issueprefix randlength = self.randomid_length id = self.generateID(randlength, prefix=prefix, incontainer=self._getIssueContainer()) if title.strip() == '': raise IssueInputError, "Issue has no subject line" if status.lower() not in [x.lower() for x in self.getStatuses()]: raise IssueInputError, "Unrecognized issue status %r" % status if type_ not in self.types: raise ValueError, "Unrecognized issue type" if urgency not in self.urgencies: raise ValueError, "Unrecognized issue urgency" if not isinstance(sections, list): raise ValueError, "Sections is not a list" if confidential not in [1,0]: raise ValueError, "Confidential value is not boolean (1 or 0)" if hide_me not in [1,0]: raise ValueError, "Hide_me value is not boolean (1 or 0)" if display_format not in self.display_formats: raise ValueError, "Invalid display format %r" % display_format if issuedate is None or issuedate =='': issuedate = DateTime() if fromname is None: fromname = "" if email is None: email = "" if acl_adder: userfolderpath, name = acl_adder.split(',') try: object = self.unrestrictedTraverse(userfolderpath) assert name in object.user_names() except: raise NoACLAdderError, "No ACL user object found" # Fine, submit it create_method = self._createIssueObject return create_method(id, title, status, type_, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, issuedate, index=index, acl_adder=acl_adder, submission_type=submission_type, email_message_id=email_message_id) def _createIssueObject(self, id, title, status, type, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, issuedate, index=0, acl_adder=None, submission_type='', email_message_id=None): """ crudely create issue object. No checking """ issueinst = IssueTrackerIssue(id, title, status, type, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, issuedate=issuedate, acl_adder=acl_adder, submission_type=submission_type) # not here where = self._getIssueContainer() where._setObject(id, issueinst) issue = getattr(where, id) if email_message_id: issue._setEmailMessageId(email_message_id) if index: # catalog it issue.index_object() return issue def _getFakeFileattachments(self, files): """ upload all new file attachments """ if not isinstance(files, (tuple, list)): files = [files] fakes = [] for file in files: try: ok = self._isFile(file) except NotAFileError: # if this exception is raised, it means that the user # didn't press the "Browse..." button but rather wrote # something for the file name. filename = getattr(file, 'filename') id=filename[max(filename.rfind('/'), filename.rfind('\\'), filename.rfind(':'), )+1:] fakes.append(id) return fakes ## ## Generating IDs for issues, threads and drafts ## def generateID(self, length, prefix='', meta_type=ISSUE_METATYPE, incontainer=None, use_stored_counter=True): """ see if there is an internal counter already, otherwise call up the old generateID() function that is now called _do_generateID(). """ if incontainer is None: incontainer = self counter_key = '_nextid_%s' % ss(incontainer.meta_type).replace(' ','') if use_stored_counter and incontainer.__dict__.has_key(counter_key): #logger.info("HAS (use_stored_counter=%s) %s in %s" % (use_stored_counter, counter_key, incontainer)) nextid_nr = incontainer.__dict__.get(counter_key) incontainer.__dict__[counter_key] = nextid_nr + 1 increment = nextid_nr #logger.info("START generate a new ID starting on increment %s" % increment) return self._do_generateID(incontainer, length, prefix=prefix, meta_type=meta_type, increment=increment) else: #logger.info("NO (use_stored_counter=%s) counter attribute"% use_stored_counter) nextid_str = self._do_generateID(incontainer, length, prefix=prefix, meta_type=meta_type) # in python2.1 you can't replace with an empty string. # thanks Thomas Kruger if prefix: nextid_nr_str = nextid_str.replace(prefix,'') else: nextid_nr_str = nextid_str nextid_nr = int(nextid_nr_str) incontainer.__dict__[counter_key] = nextid_nr return nextid_str def _do_generateID(self, incontainer, length, prefix='', meta_type=ISSUE_METATYPE, increment=None, ): """ generate IDs for different objects """ if increment is None: idnr = len(incontainer.objectIds(meta_type))+1 increment = idnr + 1 else: idnr = increment increment = increment +1 id='%s%s'%(prefix, string.zfill(str(idnr), length)) if base_hasattr(incontainer, id): # ah! Id already exists, try again return self._do_generateID(incontainer, length, prefix=prefix, meta_type=meta_type, increment=increment) else: return id ## ## Spambot ## def useSpambotPrevention(self): """ return true if spambot prevention should be used """ if self.ShowSpambotPrevention(): if self.getIssueUser() or self.getZopeUser() or self.getCMFUser(): return False ckey = ALREADY_NOT_SPAMBOT_COOKIE_KEY if self.get_cookie(ckey, False): return False return True return False def _rememberProvenNotSpambot(self): """ set a session variable on this user that proves that she's not a spambot. """ ckey = ALREADY_NOT_SPAMBOT_COOKIE_KEY self.set_cookie(ckey, True, expires=60, across_domain_cookie_=True) def _moveUpSections(self, sections): """ when an issue has been created, prioritize it's sections globally. """ if isinstance(self.sections_options, tuple): # fix for badly defined sections options. # this can go away in the future. self.sections_options = list(self.sections_options) sections_options = self.sections_options Utils.moveUpListelement(sections, sections_options) self.sections_options = sections_options def _canCreateThumbnail(self, fileobject): """ return True if recognized as a image that we can resize with PIL """ if not Image: return False try: if fileobject.getSize() < 100: return False except: return False ct = fileobject.content_type if ct in ('image/pjpeg','image/jpeg','image/gif','image/png', 'image/x-png'): return True return False def _createThumbnail(self, fileobject): """ create a thumbnail of the fileobject and name it 'thumbnail--'+fileobject.getId() """ oriFile = cStringIO.StringIO(str(fileobject.data)) try: image = Image.open(oriFile) except IOError: m = "PIL.Image could not read %s bytes imagefile" m = m%len(oriFile.getvalue()) LOG(self.__class__.__name__, WARNING, m, error=sys.exc_info()) raise except: # all other typ, val, tb = sys.exc_info() m = "Unable to create Image instance with open()" LOG(self.__class__.__name__, ERROR, m, error=sys.exc_info()) return image.thumbnail((45, 45)) image_type = image.format thumFile = cStringIO.StringIO() image.save(thumFile, image_type) thumFile.seek(0) container = fileobject.aq_parent thumbid = 'thumbnail--%s'%fileobject.getId() container.manage_addImage(thumbid, thumFile.getvalue()) # del!! def _uploadFileattachments(self, destination, files): """ upload all new file attachments """ if not isinstance(files, (tuple, list)): files = [files] for file in files: if self._isFile(file): filename = getattr(file, 'filename') id=filename[max(filename.rfind('/'), filename.rfind('\\'), filename.rfind(':'), )+1:] if id.startswith('_'): id=id[1:] id = Utils.badIdFilter(id) try: destination.manage_addFile(id, file) fileobject = getattr(destination, id) if self._canCreateThumbnail(fileobject): try: self._createThumbnail(fileobject) except IOError: # _createThumbnail() will already have logged # this IOError pass except: logger.warn("Could not upload file id=%s" % id, exc_info=True) security.declarePublic('_moveTempfiles') def _moveTempfiles(self, destination): """ move from temp folder to destination """ request = self.REQUEST rkey = TEMPFOLDER_REQUEST_KEY if request.has_key(rkey): files_copied = [] upload_folder_id = request.get(rkey) if not hasattr(self._getTempFolder(), upload_folder_id): return upload_folder = self._getTempFolder()[upload_folder_id] confirms = self._getConfirmFileattachments() cut_ids = [] for file in upload_folder.objectValues(['File','Image']): if file.getId().replace('thumbnail--','') in confirms: cut_ids.append(file.getId()) upload_id = file.getId() upload_id = Utils.badIdFilter(upload_id) if file.meta_type == 'Image': destination.manage_addImage(upload_id, file.data) else: destination.manage_addFile(upload_id, file.data) self._getTempFolder().manage_delObjects([upload_folder_id]) def _getConfirmFileattachments(self): """ return the 'confirm_fileattachments' request list """ confirms = self.REQUEST.get('confirm_fileattachment', []) if type(confirms) != type([]): confirms = [confirms] return confirms def sendAlwaysNotify(self, issue, email=None, assignee=None): """ send out emails to those who always notify """ ## Check that the sitemaster_email has been set #if self.sitemaster_email == DEFAULT_SITEMASTER_EMAIL: # m = "Sitemaster email not changed from default. Email not sent." # LOG(self.__class__.__name__, ERROR, m) # return assignee_email = None if assignee: if isinstance(assignee, basestring): acl_path, username = assignee.split(',') try: userfolder = self.unrestrictedTraverse(acl_path) if userfolder.data.has_key(username): assignee_user = userfolder.data.get(username) assignee_email = assignee_user.getEmail() except: pass always_emailstring = ', '.join(self.getAlwaysNotify()) tosend = self._alwaysNotifyMessage(issue, always_emailstring) __, to, __, __ = tosend issueid_header = issue.getGlobalIssueId() if to is not None: send_emails = [] email = ss(str(email)) for to_each in self.preParseEmailString(to, aslist=1): if ss(to_each) == email: continue elif assignee_email and ss(to_each) == assignee_email: continue #self.sendEmail(msg, to_each, fr, subject, swallowerrors=True, # headers={EMAIL_ISSUEID_HEADER: issueid_header}) send_emails.append(to_each) if send_emails: self.sendIssueNotifications(issue, send_emails) security.declarePrivate('sendIssueNotifications') def sendIssueNotifications(self, issue, emails): """ create a notification about about this issue notification and then send the notification. """ notifyid = self.generateID(5, self.issueprefix+"notification", meta_type=NOTIFICATION_META_TYPE, use_stored_counter=False, incontainer=issue) title = issue.getTitle() issueID = issue.getId() date = DateTime() notification = IssueTrackerNotification(notifyid, title, issue.getId(), emails, ) issue._setObject(notifyid, notification) notifyobject = getattr(issue, notifyid) # use the dispatcher to try to send # this notification right now. # there is no big deal if the dispatcher crashes here # because the notification is saved and the dispatcher # can be invoked some other time manually if self.doDispatchOnSubmit(): if 1: #try: self.dispatcher([notifyobject]) else: #except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, PROBLEM, 'Email could not be sent', error=sys.exc_info()) def _acceptEmailsToSiteMaster(self): """ return true if there is a POP3 account where one of the accepting emails is the same as that of sitemaster_email """ ss_sitemaster_email = ss(self.getSitemasterEmail()) for account in self.getPOP3Accounts(): for ae in account.getAcceptingEmails(): if ss(ae.getEmailAddress()) == ss_sitemaster_email: return True return False def _alwaysNotifyMessage(self, issue, emailstring): """ return the message, to, from and subject for a message to those who always get emails about new issues. """ br = "\r\n" root = self.getRoot() fromname = issue.getFromname() fromemail = issue.getEmail() _fromemail_valid = Utils.ValidEmailAddress(fromemail) if self._acceptEmailsToSiteMaster(): fr = self.getSitemasterFromField() else: if not fromname and fromemail and _fromemail_valid: fr = fromemail elif fromname and fromemail and _fromemail_valid: fr = "%s <%s>"%(fromname, fromemail) else: fr = self.getSitemasterFromField() if isinstance(issue, basestring): issue = getattr(self, issue) _issuetitle = issue.getTitle() to = self.preParseEmailString(emailstring) _r_dict = {'root_title':root.getTitle()} if self.ShowIdWithTitle(): _r_dict['issue_id'] = issue.getId() subject = _("%(root_title)s: new issue: #%(issue_id)s ") % _r_dict else: subject = _("%(root_title)s: new issue: ") % _r_dict subject += _issuetitle if fromname is None: msg = _('An issue has been added to your attention at '\ '%(root_title)s with the following title:') % _r_dict + br else: if fromemail: _from = "%s (%s)"%(fromname, fromemail) else: _from = fromname _r_dict['from_name'] = _from msg = _('%(from_name)s has entered an issue in %(root_title)s '\ 'with the following title:') % _r_dict + br msg += _issuetitle + br * 2 msg += _("The issue can be found at") + br msg += self.ActionURL(url=issue.absolute_url()) + br * 2 if self.IncludeDescriptionInNotifications(): # if this is true, enter the full text of the added issue # right here. if fromname: msg += _("%(fromname)s wrote:") % {'fromname':fromname} + br msg += Utils.LineIndent(issue.getDescriptionPure(), ' '*3, 67) msg += br * 2 msg += br # Footer signature = self.showSignature() if signature: msg += "--" + br +signature return msg, to, fr, subject ## Misc methods def getUrgencyCSSSelector(self, urgency=None): """ compare this with the parents option to return a CSS selector like 'ur-3' between [0-4] where 1 is default """ selector = 'ur-%s' if urgency is None: # self is an issue urgency = self.urgency if urgency in self.urgencies: index = self.urgencies.index(urgency) if index not in [0,1,2,3,4]: index = 1 else: index = 1 return selector%index def getIssueTrackerVersion(self): """ return global variable """ return __version__ security.declarePublic('About') def About(self): """ Show some info about the product """ osj = os.path.join f = open(osj(package_home(globals()), 'CHANGES.txt'), "r") changelog = f.read() f.close() changelog = self.ShowDescription(changelog.strip()+' ', 'structuredtext') version_number_re = re.compile(r'(<li>(\d.\d.\d\w))|(<li>(\d.\d.\d))') for version_number_html in version_number_re.findall(changelog): if version_number_html[2]: whole, number = version_number_html[2], version_number_html[3] else: whole, number = version_number_html[0], version_number_html[1] better = whole.replace(number, '<b>%s</b>'%number) changelog = changelog.replace(whole, better) version = self.getIssueTrackerVersion() f = 'zpt/About' name='About' return CTP(f, globals(), optimize=OPTIMIZE and 'xhtml', __name__=name).__of__(self)(changelog=changelog, version=version) security.declareProtected('View', 'ListIssues_CSV') def ListIssues_CSV(self, batchsize=None, withheaders=True, REQUEST=None): """ return a CSV file with the issues you're currently looking at. """ return self.CSVExport(batchsize=batchsize, withheaders=withheaders, issue_export=False, filename='ListIssues.csv', REQUEST=REQUEST) security.declareProtected('View', 'CSVExport') def CSVExport(self, batchsize=None, withheaders=True, issue_export=True, filename='export.csv', REQUEST=None): """ return a CSV file with all issue information """ outfile = cStringIO.StringIO() if csv is None: return "Sorry, CSV not supported" writer = csv.writer(outfile) if withheaders: self._write_csv_headers(writer) # if 'issue_export' is true we don't do any filtering # or any nonsense like that, we just dump all issues # there are and sort by 'issuedate' if issue_export: issues = self.getIssueObjects() issues = self._dosort(issues, 'issuedate') else: issues = self.ListIssuesFiltered() try: if batchsize: batchsize = abs(int(batchsize)) except: batchsize = None if batchsize: issues = issues[:batchsize] default_sortorder = self.getDefaultSortorder() for issue in issues: title = issue.getTitle() if self.isFromBrother(issue): title += "(%s)" % self.getBrotherFromIssue(issue).getTitle() row = ['#%s' % issue.getIssueId(), title.encode(UNICODE_ENCODING), issue.getStatus(), issue.getFromname().encode(UNICODE_ENCODING), issue.getEmail()] if default_sortorder == 'issuedate': row.append(issue.getIssueDate()) else: row.append(issue.getModifyDate()) row.append(', '.join(issue.getSections())) row.append(issue.getUrgency()) row.append(issue.getType()) writer.writerow(row) if REQUEST is not None: R = REQUEST.RESPONSE ct = 'application/msexcel-comma' R.setHeader('Content-Type', ct) cd = 'inline;filename="%s"' % filename R.setHeader('Content-Disposition', cd) return outfile.getvalue() def _write_csv_headers(self, writer): """ append the header for a csv file """ row = ['Issue ID','Subject', 'Status', 'Fromname','Email', self.translateSortorderOption(self.getDefaultSortorder()), 'Sections', 'Urgency', 'Type'] writer.writerow(row) security.declarePublic('CDATAText') def CDATAText(self, text): """ return text wrapped in CDATA tags """ return "<![CDATA[%s]]>" % text.strip() def RDF(self, batchsize=None, issues=None): """ return an RDF feed issues """ request = self.REQUEST template = self.rdf_template root = self.getRoot() about_url = root.absolute_url() + '/rdf.xml' if issues is None: request.set('keep_sortorder', False) request.set('sortorder', 'issuedate') request.set('reverse', False) issues = self.ListIssuesFiltered(skip_filter=True) else: for issue in issues: assert issue.meta_type == ISSUE_METATYPE, \ "Object meta_type not %r its %r" % (ISSUE_METATYPE, issue.meta_type) if batchsize is None: batchsize = self.getBatchSize() else: batchsize = int(batchsize) issues = issues[:batchsize] content_type = 'application/rdf+xml' request.RESPONSE.setHeader('Content-Type', content_type) return template(self, self.REQUEST, about_url=about_url, issues=issues) security.declareProtected('View', 'RSS10') def RSS10(self, batchsize=None, withheaders=True, show='normal'): """ return RSS XML 1.0 """ request = self.REQUEST root = self.getRoot() header = '<?xml version="1.0" encoding="ISO-8859-1"?>\n\n' header += '<rdf:RDF\n' header +=' xmlns="http://purl.org/rss/1.0/"\n' header +=' xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' header +=' xmlns:dc="http://purl.org/dc/elements/1.1/"\n' header +=' xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"' header +='\n>\n\n' rss_url = root.absolute_url()+'/rss.xml' header += '<channel rdf:about="%s">\n'%rss_url header += ' <title>%s\n'%root.getTitle() header += ' %s\n'%root.absolute_url() header += ' IssueTrackerProduct\n' header += ' English\n' header += ' %s\n'%self.getSitemasterEmail() xml = '' items = '\n \n' if batchsize is None: batchsize = self.default_batch_size else: batchsize = int(batchsize) if self.AllowShowAll(): assert batchsize <= 1000, "Too big batch size" else: assert batchsize <= self.default_batch_size, "Too big batch size" # manually set sortorder request.set('keep_sortorder',False) # request.set('sortorder','modifydate') request.set('reverse', True) comments_as_items = False if show.lower() in ['all','everything']: # then don't only show issues that are created new but # even those that are only follow ups request.set('sortorder', 'modifydate') comments_as_items = True else: request.set('sortorder', 'issuedate') self.REQUEST.set('keep_sortorder', 0) self.REQUEST.set('sortorder', self.getDefaultSortorder()) self.REQUEST.set('reverse', 0) allissues = self.ListIssuesFiltered(skip_filter=True) for issue in allissues[:batchsize]: sections = ", ".join(issue.sections) url = issue.absolute_url() if comments_as_items and issue.hasThreads(): _all_threads = issue.objectValues(ISSUETHREAD_METATYPE) lasthread = _all_threads[-1] issuetitle = Utils.html_quote(issue.getTitle()) threadtitle = Utils.html_quote(lasthread.getTitle()) if self.ShowIdWithTitle(): title = u"%s #%s (%s)"%(unicodify(issue.getTitle()), issue.getId(), lasthread.getTitle()) else: title = u"%s (%s)"%(unicodify(issue.getTitle()), lasthread.getTitle()) description = unicodify(lasthread.showComment()) fromname = unicodify(lasthread.getFromname()) fromemail = lasthread.getEmail() date = lasthread.getThreadDate() url += '#i%s'%len(_all_threads) else: #issuetitle = Utils.html_quote(issue.title) #issuestatus = Utils.html_quote(issue.status.capitalize()) if self.ShowIdWithTitle(): title = u"%s #%s (%s)"%(unicodify(issue.title), issue.getId(), issue.status.capitalize()) else: title = u"%s (%s)"%(unicodify(issue.title), issue.status.capitalize()) description = issue.showDescription() #issue.description.strip() fromname = issue.getFromname() fromemail = issue.getEmail() date = issue.getIssueDate() #if isinstance(title, unicode): # title = title.encode('ascii','xmlcharrefreplace') title = self._prepare_feed(title) #if isinstance(description, unicode): # description = description.encode('ascii','xmlcharrefreplace') description = self._prepare_feed(description) date = date.strftime("%Y-%m-%dT%H:%M:%S+00:00") item = '\n' % url item += ' %s\n' % title item += ' %s\n' % description item += ' %s\n' % url item += ' %s\n' % sections item += ' %s\n' % date item += '\n\n' xml += item items += ' \n'%url items += ' \n\n' footer = '\n' # Combine things header += items + '\n\n' rss = header + xml + footer request.RESPONSE.setHeader('Content-Type','application/rdf+xml') return rss security.declareProtected('View', 'RSS091') def RSS091(self, batchsize=None, withheaders=1, show='normal'): """ return RSS XML """ request = self.REQUEST root = self.getRoot() header=""" %s %s %s en-uk %s\n"""%\ (root.title, root.absolute_url(), root.title, self.sitemaster_email) logo = getattr(self, 'issuetracker_logo.gif') header=header+""" %s %s %s %s %s %s \n"""%(logo.title, logo.absolute_url().strip(), root.absolute_url(), logo.width, logo.height, root.title) # manually set sortorder request.set('sortorder','date') request.set('reverse',True) xml='' if batchsize is None: batchsize = self.default_batch_size comments_as_items = 0 if show.lower() in ['all','everything']: # then don't only show issues that are created new but # even those that are only follow ups request.set('sortorder', 'changedate') comments_as_items = 1 else: request.set('sortorder', 'creationdate') allissues = self.ListIssuesFiltered() for issue in allissues[:batchsize]: if comments_as_items and issue.hasThreads(): lasthread = issue.objectValues(ISSUETHREAD_METATYPE)[-1] title = "%s (%s)"%(issue.getTitle(), lasthread.getTitle()) description = lasthread.comment fromname = lasthread.fromname fromemail = lasthread.email else: title = "%s (%s)"%(issue.title, issue.status.capitalize()) description = issue.description fromname = issue.fromname fromemail = issue.email title = self._prepare_feed(title) description = self._prepare_feed(description) xml=xml+"""\n\t %s %s %s """%(title, description, issue.absolute_url()) if fromname != '': author = "%s (%s)"%(fromname, fromemail) xml="%s\n%s\n"%(xml, author) xml=xml+"\n\t" footer="""\n""" if withheaders: xml = header+xml+footer response = request.RESPONSE response.setHeader('Content-Type', 'text/xml') return xml def _prepare_feed(self, s): """ prepare the text for XML usage """ return "" % s def showURL2Issue(self, url=None, href=0, maxlength=60): """ display the url2issue for ShowIssueData """ if url is None: url = self.url2issue protocols = ('http','svn+ssh','svn','ftp') if href: if not [i for i in protocols if url.startswith(i)]: url = 'http://'+url return url else: if url.startswith('http://www.'): url = url.replace('http://','') return self.showBriefURL(url, maxlength) def showBriefURL(self, url, maxlength=70): """ show begining and end of a URL """ if len(url) > maxlength: half = int(maxlength/2) url = url[0:half]+'...'+url[-half:] return url def displayBriefTitle(self, title): """ return the title or truncate it a bit """ limit = BRIEF_TITLE_MAX_LENGTH if self.ShowIdWithTitle(): limit -= self.randomid_length if isinstance(title, str): # the old way return self.tag_quote( Utils.html_entity_fixer( self.lengthLimit(title, limit, '...') ) ) else: return self.tag_quote(self.lengthLimit(title, limit, '...')) def getOutlookDaylabels(self, issues): """ return a dictionary where the keys are the issue ids and the value is the string that expresses the day bucket. """ all={} def equal(date1, date2, fmt): return date1.strftime(fmt) == date2.strftime(fmt) today = DateTime() for issue in issues: all_values = all.values() modify_date = issue.getModifyDate() if equal(today, modify_date, '%Y%m%d'): if 'Today' not in all_values: all[issue.getId()] = 'Today' elif equal(today, modify_date+1, '%Y%m%d'): if 'Yesterday' not in all_values: all[issue.getId()] = 'Yesterday' elif equal(today, modify_date, '%Y%m%W'): if 'This week' not in all_values: all[issue.getId()] = 'This week' elif equal(today, modify_date+7, '%Y%m%W'): if 'Last week' not in all_values: all[issue.getId()] = 'Last week' elif equal(today, modify_date+14, '%Y%m%W'): if 'Two weeks ago' not in all_values: all[issue.getId()] = 'Two weeks ago' elif equal(today, modify_date, '%Y%m'): if 'This month' not in all_values: all[issue.getId()] = 'This month' elif equal(today, modify_date + 30, '%Y%m'): if 'Last month' not in all_values: all[issue.getId()] = 'Last month' return all #def _findIssueLinks(self, text): # """ return a compiled regular expression of where there are # links to other issues. The rules for making a link is: # # (eg. Real#0103) # # (eg. #0103) # #prefix (eg. #ibm0103) # Bare in mind that the text might contain hyperlinks to issues # from before, ignore them. # """ ## Cookies! def saveEmailstring(self, to): """ Save to string as a cookie """ raise DeprecatedError key = EMAILSTRING_COOKIEKEY key = self.defineInstanceCookieKey(key) self.set_cookie(key, to) def getSavedEmailstring(self): """ Return cookie translated or nothing """ key = EMAILSTRING_COOKIEKEY key = self.defineInstanceCookieKey(key) if self.REQUEST.cookies.has_key(key): to = self.REQUEST.cookies[key] for item in self.getNotifyables(): to = to.replace(item.getEmail(), item.getName()) return to else: return None def saveEmailfriends(self, friends): """ Save to string as a cookie with '|' between each """ raise DeprecatedError if not isinstance(friends, list): friends = [friends] key = EMAILFRIENDS_COOKIEKEY key = self.defineInstanceCookieKey(key) friends = '|'.join([str(x).strip() for x in friends]) self.set_cookie(key, friends) def getSavedEmailfriends(self): """ return cookie translated or nothing """ key = EMAILFRIENDS_COOKIEKEY key = self.defineInstanceCookieKey(key) if self.REQUEST.cookies.has_key(key): friends = self.REQUEST.cookies.get(key) return [x.strip() for x in friends.split('|')] else: return [] def getSavedTextFormat(self, no_default=False): """ This method returns what display_format value the user has. If none found, then the default one is returned. """ issueuser = self.getIssueUser() if issueuser: if issueuser.getDisplayFormat(): return issueuser.getDisplayFormat() if no_default: default = "" else: default = self.getDefaultDisplayFormat() s=None cookiekey = self.getCookiekey('display_format') if self.has_cookie(cookiekey): s = self.get_cookie(cookiekey) if s not in self.display_formats: s = None if s is None: return default else: return s def get_cookie(self, name, default=None): """ return RESPONSE cookie """ value = self.REQUEST.cookies.get(name,default) return value def set_cookie(self, key, value, expires=365, path='/', across_domain_cookie_=False, **kw): """ set a cookie in REQUEST 'across_domain_cookie_' sets the cookie across all subdomains eg. www.mobilexpenses.com and mobile.mobilexpenses.com etc. This rule will only apply if the current domain name plus sub domain contains at least two dots. """ if expires is None: then = DateTime()+365 then = then.rfc822() elif isinstance(expires, int): then = DateTime()+expires then = then.rfc822() elif type(expires)==DateTimeType: # convert it to RFC822() then = expires.rfc822() else: then = expires if across_domain_cookie_ and not kw.get('domain'): # set kw['domain'] = '.domainname.com' if possible cookie_domain = self._getCookieDomain() if cookie_domain: kw['domain'] = cookie_domain try: value = str(value) except UnicodeEncodeError: value = value.encode(UNICODE_ENCODING) self.REQUEST.RESPONSE.setCookie(key, value, expires=then, path=path, **kw) def has_cookie(self, name): """ return cookie presence """ return self.REQUEST.cookies.has_key(name) def expire_cookie(self, key, path='/', across_domain_cookie_=False): """ expire a cookie 'across_domain_cookie_' sets the cookie across all subdomains eg. www.mobilexpenses.com and mobile.mobilexpenses.com etc. This rule will only apply if the current domain name plus sub domain contains at least two dots. """ if across_domain_cookie_: cookie_domain = self._getCookieDomain() if cookie_domain: self.REQUEST.RESPONSE.expireCookie(key, path=path, domain=cookie_domain) return self.REQUEST.RESPONSE.expireCookie(key, path=path) def _getCookieDomain(self): """ from the REQUEST.URL work out what is the cookie domain. E.g. if REQUEST.URL is http://www.foo.com/path/page.html the correct result is '.foo.com' """ netloc = urlparse(self.REQUEST.URL)[1] threes = 'com', 'net', 'org', 'biz', 'gov' fours = 'name', 'info', 'firm', 'gov' if not re.findall('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', netloc): top = netloc.split('.')[-1] if top in threes or top in fours: if len(netloc.split('.')) > 2: return '.%s' % '.'.join(netloc.split('.')[1:]) else: if len(netloc.split('.')) > 3: return '.%s' % '.'.join(netloc.split('.')[1:]) return None def getSavedUser(self, name_email='email', d=0, use_request=True): """ Return the name or email from request, if not found, return from cookie, else return "" """ request = self.REQUEST if name_email =='email': s = 'email' cookie = self.getCookiekey('email') else: s = 'fromname' cookie = self.getCookiekey('name') issueuser = self.getIssueUser() if issueuser: if s == 'email': issueuser_email = issueuser.getEmail() if issueuser_email: return issueuser_email elif s == 'fromname': issueuser_name = issueuser.getFullname() if issueuser_name: return issueuser_name # now we know what we're looking for acl_username = getSecurityManager().getUser().getUserName() if acl_username.lower().replace(' ','') == 'anonymoususer': acl_username = None if use_request and request.get(s): return unicodify(request[s]) elif self.get_cookie(cookie): if s =='fromname': return unicodify(self.get_cookie(cookie)) else: return self.get_cookie(cookie) elif acl_username: r = self._getACLCookie(acl_username, s) if name_email == 'email': if r is None: return "" else: return r else: if r is None: return u"" else: return unicodify(r) else: if name_email == 'email': return "" else: return u"" def getSavedUserName(self): """ wrap getSavedUser() """ return self.getSavedUser('fromname') def getSavedUserEmail(self): """ wrap getSavedUser() """ return self.getSavedUser('email') def _getACLCookie(self, name, action='email'): if action == 'fromname': return self.getACLCookieNames().get(name) elif action == 'email': return self.getACLCookieEmails().get(name) elif action == 'displayformat': return self.getACLCookieDisplayformats().get(name) ## ## Sessions! ## def getFilterValue(self, key, filterlogic=None, request_only=False, default=None): """ return what the value should be. """ if filterlogic is None: filterlogic = self.getFilterlogic() filterkey = 'f-%s-%s'%(key, filterlogic) filterkey_simple = 'f-%s'%(key) request = self.REQUEST value = default if request.has_key(filterkey_simple) and request.get(filterkey_simple) is not None: value = request.get(filterkey_simple) value = unicodify(value) if key in ('statuses', 'sections', 'urgencies', 'types'): if isinstance(value, basestring): value = [value] else: # make sure each is a unicode string if isinstance(value, (tuple, list)): value = [unicodify(item) for item in value] else: logger.warn("Not sure what to do with %r (%s)" % (value, type(value))) elif not request_only and self.has_session(filterkey): value = self.get_session(filterkey) if default is not None and value is None: return default else: return value def _getDefaultFilterValueBlock(self, key): """ return default values """ if key == 'fromname' or key == 'email': return "" else: return [] def _getDefaultFilterValueShow(self, key): """ return default values """ if key == 'sections': return [] return self.sections_options elif key == 'fromname' or key == 'email': return "" else: return [] return self.__dict__[key] def ShowFilter(self, filtername, sequence=[]): """ Check whether to show filter or not """ request = self.REQUEST key = FILTEROPTIONS_KEY if request.has_key(filtername): return request[filtername] elif self.has_session(key): filteroptions = self.get_session(key) if filteroptions.has_key(filtername): return filteroptions[filtername] return [] def getListPageTitle(self, default='List Issues'): """ return a suitable page title for this list (ListIssues or CompleteList) """ request = self.REQUEST if request.get('q'): return _(u"Search Results") elif request.get('report'): try: # try to find the actual title of the report itself container = self.getReportsContainer() if hasattr(container, request.get('report')): report = getattr(container, request.get('report')) return "Report: %s" % report.title_or_id() except: return _(u"Report") elif request.get('i'): i = ss(request.get('i')) if i == 'assigned': return _(u"Issues assigned to you") elif i == 'added': return _(u"Issues you have added") elif i == 'followedup': return _(u"Issues you have followed up on") elif i == 'subscribed': return _(u"Issues you have subscribed to") else: return _(u"Your Issues") else: return _(u"List Issues") def setWhichList(self, what): """ set a SESSION with which list """ key = WHICHLIST_COOKIEKEY what = ss(what) if what in ['completelist','listissues']: issueuser = self.getIssueUser() if issueuser: # set the which list issueuser.setMiscProperty('whichlist', what) else: # put it in a cookie self.set_cookie(key, what) return None def whichList(self): """ inspect the SESSION object if there's information about either "ListIssues" or "CompleteList" """ key = WHICHLIST_COOKIEKEY issueuser = self.getIssueUser() default = 'ListIssues' if issueuser and issueuser.hasMiscProperty('whichlist'): # get it from the acl user value = issueuser.getMiscProperty('whichlist') else: # get it from cookie value = self.get_cookie(key) if value and ss(value) == 'completelist': return 'CompleteList' else: return default def setWhichSubList(self, what): """ determines 'compact' or 'rich' """ key = WHICHSUBLIST_COOKIEKEY what = ss(what) if what in ('rich','compact'): issueuser = self.getIssueUser() if issueuser: # set the which list issueuser.setMiscProperty('whichsublist', what) else: # set it in a cookie self.set_cookie(key, what) return None def whichSubList(self): """ return either 'rich' (default) or 'compact' If it's defined in REQUEST, remember that forever """ c_key = WHICHSUBLIST_COOKIEKEY default = 'rich' ok_values = ('rich', 'compact') issueuser = self.getIssueUser() if ss(self.REQUEST.get('list-type','')) in ok_values: # remember it! value = ss(self.REQUEST.get('list-type','')) if issueuser: issueuser.setMiscProperty('whichsublist', value) else: self.set_cookie(c_key, value) return value else: # look for an old one if issueuser and issueuser.hasMiscProperty('whichsublist'): value = issueuser.getMiscProperty('whichsublist') if value in ok_values: return value else: return default else: # use cookies instead cookie_value = self.get_cookie(c_key, None) if cookie_value in ok_values: return cookie_value else: return default def getListIssuesList(self, sublist): """ return the template for a particular sublist """ if self.doDebug(): assert sublist in ('rich','compact'), "Unrecognized sublist %r" % sublist # Read the comment inside getHeader() regard CheckoutableTemplates to understand # why we do what we do here. if sublist == 'rich': zodb_id = 'richList.zpt' base_tmpl = self.richList else: zodb_id = 'compactList.zpt' base_tmpl = self.compactList return getattr(self, zodb_id, base_tmpl) def changeWhichSubListURL(self, newtype): """ return the URL for the interface which is links that lets you change the sublist behaviour to Compact or Rich. """ assert newtype in ('Compact','Rich') request = self.REQUEST key = "list-type" params = {key:newtype} for e in ('q','i','f-statuses','f-fromname','f-email','f-sections', 'f-urgencies','f-types','report'): if request.get(e): params[e] = request.get(e) url = self.relative_url()+'/ListIssues' return Utils.AddParam2URL(url, params) def CSVExportURL(self): """ return the URL for the interface which is links that lets you export to csv with the ListIssues.csv function. """ request = self.REQUEST params = {} for e in ('q','i','f-statuses','f-fromname','f-email','f-sections', 'f-urgencies','f-types','report'): if request.get(e): params[e] = request.get(e) url = self.relative_url()+'/ListIssues.csv' return Utils.AddParam2URL(url, params, plus_quote=True) def ResetFilter(self, page='ListIssues', redirectafter=True): """ reset the filter then show the ListIssues or eq. again """ for key in ('statuses','sections','urgencies','types', 'fromname','email'): subkey1 = 'f-%s-show'%key subkey2 = 'f-%s-block'%key if self.has_session(subkey1): self.delete_session(subkey1) if self.has_session(subkey2): self.delete_session(subkey2) if self.has_session('last_savedfilter_id'): self.delete_session('last_savedfilter_id') key = LAST_SAVEDFILTER_ID_COOKIEKEY key = self.defineInstanceCookieKey(key) if self.has_cookie(key): debug("Expire cookie %s" % key, steps=1) self.expire_cookie(key) if redirectafter: page = page.lower().strip() if page == 'listissues': page = '/ListIssues' elif page == 'completelist': page = '/CompleteList' else: raise NotFound self.REQUEST.RESPONSE.redirect(self.getRootURL()+page) def HideFilter(self, page='ListIssues', REQUEST=None): """ hide the filter then show the ListIssues or eq. again """ key = SHOW_FILTEROPTIONS_KEY self.set_session(key, False) page = page.lower().strip() if page == 'listissues': page = '/ListIssues' elif page == 'completelist': page = '/CompleteList' else: raise NotFound url = self.getRootURL()+page if REQUEST is not None: REQUEST.RESPONSE.redirect(url) else: return url def get_session(self, name, default=None, globally=0): """ Override the session.get method a little bit """ if not globally: name = self.defineInstanceCookieKey(name) try: value = self.REQUEST.SESSION.get(name, default) return value except KeyError: # something's gone wrong with the SESSION object return default def set_session(self, name, value, globally=0): """ Overrode the session.set method a little bit """ if not globally: name = self.defineInstanceCookieKey(name) self.REQUEST.SESSION.set(name, value) def has_session(self, name, globally=0): """ Override the session.has_key method a little big """ if not globally: name = self.defineInstanceCookieKey(name) return self.REQUEST.SESSION.has_key(name) def delete_session(self, name, globally=0): """ override the session.delete method """ if not globally: name = self.defineInstanceCookieKey(name) self.REQUEST.SESSION.delete(name) ## URL related def aurl(self, url, params={}, ignore=[]): """ modify the URL to include url-request-variables """ request = self.REQUEST splitted = url.split('/') # # internal name # what it's called in REQUEST queryitems = ({'key':'start', 'mkey':'start'}, {'key':'sortorder', 'mkey':'sortorder'}, {'key':'reverse', 'mkey':'reverse'}, {'key':'show', 'mkey':'show'}, {'key':'report', 'mkey':'report'} ) splitter = '-' # Use old things if not isinstance(ignore, list): ignore = [ignore] keys_applied = [] for key, value in params.items(): keys_applied.append(key) if value is not None and key not in ignore: splitted.append("%s%s%s"%(key, splitter, value)) # Add new things for each in queryitems: key, mkey = each['key'], each.get('mkey') if mkey is not None: if key not in keys_applied and key not in ignore and\ request.has_key(mkey) and request[mkey] is not None: splitted.append("%s%s%s"%(key, splitter, request[mkey])) return '/'.join(splitted) def getRootURL(self, relative=None): """ quick wrapper around getRoot() """ return self.getRoot().absolute_url() def getRootRelativeURL(self): """ quick wrapper around getRoot() """ return self.getRoot().relative_url() def issueURLbyID(self, issueID): """ Return absolute_url of an issue from its id """ return getattr(self.getRoot(),issueID).absolute_url() def thisInURL(self, page, homepage=0): """ To find if a certain objectid is in the URL """ URL = self.ActionURL(self.REQUEST.URL) rootURL = self.getRootURL() if homepage and URL==rootURL: return True else: URL = URL.lower() if isinstance(page, (list, tuple)): # 'page' is iterable, think of an OR between each for each in page: expected = rootURL +'/'+ each if URL == expected.lower(): return True return False else: expected = rootURL +'/'+ page if URL == expected.lower(): return True elif not URL.startswith(rootURL): # most likely because we're inspecting a brother issue expected = '/'.join(rootURL.split('/')[:-1]+[URL.split('/')[-2]]+[page]) return URL == expected.lower() else: return False def ActionURL(self, url=None): """ If URL is http://host/index_html I prefer to display it http://host Just a little Look&Feel thing """ if url is None: url = self.REQUEST.URL URLsplitted = url.split('/') if URLsplitted[-1] == 'index_html': return '/'.join(URLsplitted[:-1]) return url ## ZCatalog related def getCatalog(self): """ return the installed ICatalog object """ if hasattr(self, 'ICatalog'): return self.ICatalog else: # backward compatability return self.Catalog def getFilterValuerCatalog(self): """ return the saved-filters-catalog or None if it does not exist. """ return getattr(self, FILTERVALUECATALOG_ID, None) def InitZCatalog(self, t={}): """ create a ZCatalog called 'ICatalog' and change its properties accordingly """ if not 'ICatalog' in self.objectIds('ZCatalog'): self.manage_addProduct['ZCatalog'].manage_addZCatalog('ICatalog','') t['ICatalog'] = "ZCatalog" zcatalog = self.getCatalog() indexes = zcatalog._catalog.indexes if not hasattr(zcatalog, 'Lexicon'): # This default lexicon sucks because it doesn't support unicode. # Consider creating a http://www.zope.org/Members/shh/UnicodeLexicon # instead. script = zcatalog.manage_addProduct['ZCTextIndex'].manage_addLexicon wordsplitter = Empty() wordsplitter.group = 'Word Splitter' wordsplitter.name = 'Whitespace splitter' casenormalizer = Empty() casenormalizer.group = 'Case Normalizer' casenormalizer.name = 'Case Normalizer' stopwords = Empty() stopwords.group = 'Stop Words' stopwords.name = 'Remove listed stop words only' script('Lexicon', 'Default Lexicon', [wordsplitter, casenormalizer, stopwords]) t['Lexicon'] = "Lexicon for ZCTextIndex created" # if not hasattr(zcatalog, 'Vocabulary'): # script = zcatalog.manage_addProduct['ZCatalog'].manage_addVocabulary # script(id='Vocabulary', title='', globbing=1) # t['Vocabulary'] = "It is recommended that you now run Update Catalog" for fieldindex in ('id','meta_type'): if not indexes.has_key(fieldindex): zcatalog.addIndex(fieldindex, 'FieldIndex') for keywordindex in ('filenames',): if not indexes.has_key(keywordindex): zcatalog.addIndex(keywordindex, 'KeywordIndex') textindexes = ('email','url2issue') for idx in textindexes: if not indexes.has_key(idx): zcatalog.addIndex(idx, 'TextIndex') #wrapped_textindexes = [('fromname','getTitle_idx')] #for idx, indexed_attrs in wrapped_textindexes: # if indexes.has_key(idx) and not indexes[idx].call_methods: # # the old way! # indexes.pop(idx) # if not indexes.has_key(idx): # extra = record() # extra.indexed_attrs = indexed_attrs # extra.vocabulary = 'Vocabulary' # zcatalog.addIndex(idx, 'TextIndex', extra) zctextindexes = ( ('title', 'getTitle_idx'), ('description', 'getDescription_idx'), ('comment', 'getComment_idx'), ('fromname', 'getFromname_idx'), ) for idx, indexed_attrs in zctextindexes: extras = Empty() extras.doc_attr = indexed_attrs # 'Okapi BM25 Rank' is good if you match small search terms # against big texts. # 'Cosine Rule' is useful to match similarity between two texts extras.index_type = 'Okapi BM25 Rank' extras.lexicon_id = 'Lexicon' if indexes.has_key(idx) and indexes.get(idx).meta_type \ not in ('ZCTextIndex', 'TextIndexNG2'): zcatalog.delIndex(idx) if indexes.has_key(idx):# and indexes.get(idx) if indexed_attrs not in indexes.get(idx).getIndexSourceNames(): # The old way zcatalog.delIndex(idx) if not indexes.has_key(idx): zcatalog.addIndex(idx, 'ZCTextIndex', extras) t['ZCTextIndex'] = idx return t def _setupFilterValuerCatalog(self): """ create a ZCatalog for the saved filters """ oid = FILTERVALUECATALOG_ID if not oid in self.objectIds('ZCatalog'): self.manage_addProduct['ZCatalog'].manage_addZCatalog(oid, 'ZCatalog for saved filters') zcatalog = self.getFilterValuerCatalog() # asserts that it works assert zcatalog is not None, "saved filters catalog not created" indexes = zcatalog._catalog.indexes #if 'meta_type' not in zcatalog.schema(): # zcatalog.addColumn('meta_type') idxs = ('meta_type','acl_adder','key', 'title', 'adder_fromname', 'adder_email') for fieldindex in idxs: if not indexes.has_key(fieldindex): zcatalog.addIndex(fieldindex, 'FieldIndex') pathindexes = [('path','getPhysicalPath'),] for idx, indexed_attrs in pathindexes: if not indexes.has_key(idx): extra = record() extra.indexed_attrs = indexed_attrs zcatalog.addIndex(idx, 'PathIndex', extra) dateindexes = [('mod_date','getModificationDate'),] for idx, indexed_attrs in dateindexes: if not indexes.has_key(idx): extra = record() extra.indexed_attrs = indexed_attrs zcatalog.addIndex(idx, 'DateIndex', extra) return zcatalog security.declareProtected(VMS, 'UpdateCatalog') def UpdateCatalog(self, REQUEST=None): """ Re-find items in the Catalog """ request = self.REQUEST catalog = self.getCatalog() # Zope 2.8.0 migration hell if not hasattr(catalog._catalog, '_length'): if hasattr(catalog._catalog, 'migrate__len__'): # perform the zope 2.8.0 migration script catalog._catalog.migrate__len__() else: # That's ok. This means that the _catalog object didn't # have the zope 2.8.0 migration method which effectively means that # we don't need to do the migration :) pass catalog.manage_catalogClear() for issue in self.getIssueObjects(): issue.index_object() for thread in issue.objectValues(ISSUETHREAD_METATYPE): thread.index_object() msg = "%s updated."%catalog.getId() if REQUEST is None: return msg else: method = Utils.AddParam2URL desturl = self.getRootURL()+"/manage_ManagementForm" url = method(desturl,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'UpdateFilterValuerCatalog') def UpdateFilterValuerCatalog(self, REQUEST=None): """ Re-find items in the saved filters catalog """ request = self.REQUEST catalog = self.getFilterValuerCatalog() # Zope 2.8.0 migration hell if not hasattr(catalog._catalog, '_length'): if hasattr(catalog._catalog, 'migrate__len__'): # perform the zope 2.8.0 migration script catalog._catalog.migrate__len__() else: # That's ok. This means that the _catalog object didn't # have the zope 2.8.0 migration method which effectively means that # we don't need to do the migration :) pass catalog.manage_catalogClear() container = self._getFilterValueContainer() for filter_valuer in container.objectValues(FILTEROPTION_METATYPE): filter_valuer.index_object() msg = "%s updated." % catalog.getId() if REQUEST is None: return msg else: method = Utils.AddParam2URL desturl = self.getRootURL()+"/manage_ManagementForm" url = method(desturl,{'manage_tabs_message':msg}) self.REQUEST.RESPONSE.redirect(url) ## Notification related def dispatcher(self, notificationobjects=None, min_age_minutes=0, REQUEST=None): """ Sends out all the emails or at least returns the string to use """ if notificationobjects is None: notificationobjects = self.getAllNotifications() if not isinstance(notificationobjects, (list, tuple)): notificationobjects = [notificationobjects] sendworthy = [x for x in notificationobjects if not x.isDispatched()] # if the @min_age_minutes is set to something other than 0, # a check is made that the notifications aren't too young. # With the new feature (Real#0686) of delayed sending, if some switches # of "Dispatch on submit" and allows a cron job call dispatcher() every # 15 minutes, there's a risk that it hits seconds after a notification # is created and then the notification goes out since the notifyee might # not have had time to respond even if he responds quickly. min_age_minutes = int(min_age_minutes) if min_age_minutes: now = DateTime() min_age_days = float(min_age_minutes)/(24*60) sendworthy = [x for x in sendworthy if (now-x.date) >= min_age_days] roottitle = self.getRoot().getTitle() sitemaster_name = self.getSitemasterName() sitemaster_email = self.getSitemasterEmail() if not sitemaster_name: m = "(%s) Sitemaster name not set" logger.info(m % self.getRoot().getTitle()) if not Utils.ValidEmailAddress(sitemaster_email): m = "(%s) Sitemaster email not valid. Email might not work" logger.warn(m % self.getRoot().getTitle()) From = u"%s <%s>" % (sitemaster_name, sitemaster_email) senttos = {} for notification in sendworthy: # The notification is either about a followup or a new issue. # The way to distinguish that is by the attribute notification.change issueID = notification.issueID #issue_url = self.issueURLbyID(issueID) issue = self.getIssueObject(issueID) issueid_header = issue.getGlobalIssueId() issue_url = issue.absolute_url() emails = [x.strip() for x in notification.emails if x.strip()] emails = [x for x in emails if Utils.ValidEmailAddress(x)] emails = Utils.uniqify(emails) if notification.assignment: # the notification is about an assingment assignment = notification.getAssignmentObject() if assignment is None: raise AttributeError, "Assignment object %r not found" % notification.assignment assignee_identifier = assignment.getACLAssignee() roottitle = self.getRoot().getTitle() issuetitle = issuetitle_short = self.getTitle() if len(issuetitle_short) > 45: issuetitle_short = issuetitle_short[:45].strip()+'...' if self.ShowIdWithTitle(): Subject = u"%s: (assignment) #%s %s" Subject = Subject % (roottitle, self.getId(), issuetitle_short) else: Subject = u"%s: (assignment) %s" Subject = Subject % (roottitle, issuetitle_short) try: userfolderpath, name = assignee_identifier.split(',') except ValueError: m = "Invalid assignee identifier (%s)" raise AssigneeNotFoundError, m % assignee_identifier userfolder = self.unrestrictedTraverse(userfolderpath) if name in userfolder.user_names(): user = self.getIssueUserObject(assignee_identifier) else: m = "Invalid assignee identifier (%s)" raise AssigneeNotFoundError, m % assignee_identifier to_name = user.getFullname() to_email = user.getEmail() if to_name: To = u'%s <%s>'%(to_name, to_email) else: To = to_email # who made the assignment can be found from the assignment object # itself. by_who = assignment.getFromname() if not by_who: by_who = assignment.getEmail() msg = u"" #%DateTime().strftime(self.display_date) msg += u"You have been assigned to an issue by %s" % by_who msg += u' with title: "%s"\n' % issue.getTitle() msg += u"The issue is currently %s.\n\n" % issue.status.capitalize() msg += u"The issue can be found at\n%s\n\n" % issue.absolute_url() signature = self.showSignature() if signature: msg += '--\n'+signature elif notification.change: if self.ShowIdWithTitle(): Subject = "%s: #%s %s"%(roottitle, issueID, notification.title) else: Subject = "%s: %s"%(roottitle, notification.title) fromname = notification.fromname if not fromname: fromname = '(No name)' br = '\r\n' msg = notification.date.strftime(self.display_date) + br msg += '%s has responded to "%s"'%(fromname, notification.title) + br msg += issue_url + br*2 msg += 'Change:' + br + ' '*4 + notification.change + br * 2 msg += 'Comment:' + br if notification.comment.strip(): msg += Utils.LineIndent(notification.comment, ' ' * 3, 67) else: msg += "(no comment)" msg += br*2 msg += issue_url +\ '#i%s'%notification.anchorname msg += br*2 signature = self.showSignature() if signature: msg += '--' + br + signature else: # the notification is about an issue and _alwaysNotifyMessage() # will generate the appropriate message and from address tosend = self._alwaysNotifyMessage(issue, ','.join(emails)) msg, __, From, Subject = tosend for email in emails: if senttos.has_key(issueID): senttos[issueID].append(email) else: senttos[issueID] = [email] To = email # send it! success = self.sendEmail(msg, To, From, Subject, swallowerrors=not(DEBUG and True or False), headers={EMAIL_ISSUEID_HEADER: issueid_header}) if success: notification.setSuccessEmail(To) notification.MarkNotificationDispatch() # show some output now if senttos: out = "Notifications sent.\n\n" for issueID, emails in senttos.items(): out += '*%s*\n'%issueID for email in emails: out += ' %s\n'%email out += '\n' else: out = "No notifications sent" if REQUEST is not None: self.StopCache() REQUEST.RESPONSE.setHeader('Content-Type','text/plain') return out def getAlwaysNotify(self, except_email=None): """ return always_notify or default """ always = getattr(self, 'always_notify', DEFAULT_ALWAYS_NOTIFY) if except_email is not None: except_email = except_email.lower().strip() always_checked = [] for each in always: emails = self.preParseEmailString(each, aslist=1) if emails: if emails[0].lower().strip() != except_email: always_checked.append(each) always=always_checked return always def Always2Notify(self, format='email', emailtoskip=None, requireemail=False, include_assignee=False): """ return a list of strings of people who will be notified when this issue gets submitted. 'format' can take three forms: email, name, both or merged. both returns 'Peter ' merged returns whatever self.ShowNameEmail() does """ if format not in ('email','name','both', 'merged'): format = 'email' if emailtoskip is None: issueuser = self.getIssueUser() if issueuser: emailtoskip = issueuser.getEmail() elif self.REQUEST.get('email'): emailtoskip = self.REQUEST.get('email') elif self.has_cookie(self.getCookiekey('email')): emailtoskip = self.get_cookie(self.getCookiekey('email')) all = [] appended_email_addresses = [] always = self.getAlwaysNotify() checked = [self._checkAlwaysNotify(x, format='list') for x in always] if include_assignee and self.REQUEST.get('notify-assignee'): assignment_acl_user = self.REQUEST.get('assignee') acl_path, username = assignment_acl_user.split(',') try: userfolder = self.unrestrictedTraverse(acl_path) if userfolder.data.has_key(username): u = userfolder.data.get(username) checked.append((True, [u.getFullname(), u.getEmail()])) except: pass elif include_assignee and self.objectValues(ISSUEASSIGNMENT_METATYPE): first_assignment = self.objectValues(ISSUEASSIGNMENT_METATYPE)[0] assignee_name = first_assignment.getAssigneeFullname() assignee_email = first_assignment.getAssigneeEmail() if requireemail: if assignee_email: checked.append((True, [assignee_name, assignee_email])) else: checked.append((True, [assignee_name, assignee_email])) for valid, name_and_email in checked: add = '' if valid: _name = name_and_email[0] _email = name_and_email[1] if emailtoskip is not None and ss(_email) == ss(emailtoskip): continue # skip! if requireemail and not self.ValidEmailAddress(_email): continue # skip! if format == 'email': add = _email or _name if add in all: continue # skip! elif format == 'name': add = _name or _email if add in all: continue # skip! else: if _name and _email: if format == 'both': if _email.lower() in appended_email_addresses: continue # skip! else: add = "%s <%s>"%(_name, _email) appended_email_addresses.append(_email.lower()) else: if _email.lower() in appended_email_addresses: continue # skip! else: add = self.ShowNameEmail(_name, _email, highlight=0) appended_email_addresses.append(_email.lower()) elif _name: if format == 'both': add = _name else: add = _name elif _email: if _email.lower() in appended_email_addresses: continue # skip! else: if format == 'both': add = _email else: add = self.ShowNameEmail(_name, _email, highlight=0) appended_email_addresses.append(_email.lower()) if add and add not in all: all.append(add) return all def getAllNotifications(self): """ Go through all issues and find all notification objects """ all = [] for issue in self.getIssueObjects(): all = all+issue.objectValues(NOTIFICATION_META_TYPE) return all def preParseEmailString(self, email_string, aslist=0, allnotifyables=1): """ wrapper around utils """ if isinstance(email_string, list): email_string = ', '.join(email_string) parsemethod = Utils.preParseEmailString all_notifyables = self.getNotifyables() if not allnotifyables: all_notifyables = [] names2emails = {} for item in all_notifyables: email = item.getEmail() name = item.getName() names2emails[name] = email names2emails["%s, %s"%(name, email)] = email # add acl_users for iuf in self.superValues(ISSUEUSERFOLDER_METATYPE): for username, userdata in iuf.data.items(): email = userdata.getEmail() names2emails[username] = email showname = "%s, %s"%(userdata.getFullname(), username) names2emails[showname] = email showname = "%s (%s)"%(userdata.getFullname(), username) names2emails[showname] = email all_groups = self.getNotifyableGroups() for group in all_groups: notifyables = self.getNotifyablesByGroup(group) their_email_addresses = [x.getEmail() for x in notifyables] names2emails['group: %s'%group.getTitle()] = their_email_addresses result = parsemethod(email_string, names2emails=names2emails, aslist=aslist) return result ## Manager related def getManagerRoles(self): """ Return the roles that makes an IssueTracker Manager """ return getattr(self, 'manager_roles', DEFAULT_MANAGER_ROLES) def hasManagerRole(self): """ This method determines if the current user is allowed to do stuff that only the Zope manager is supposed to be able to do. Feel free to edit appropriatly to what suits you. """ #user_roles = self.REQUEST.AUTHENTICATED_USER.getRoles() #user_roles = self.REQUEST.AUTHENTICATED_USER.getRolesInContext(self) user_roles = getSecurityManager().getUser().getRolesInContext(self) for role in self.getManagerRoles(): if role in user_roles: return True # still here! return False ## Helpers to templates def getHeader(self): """ Return which METAL header&footer to use """ # Since we might be using CheckoutableTemplates and macro # templates are very special we are forced to do the following # magic to get the macro 'standard' from a potentially checked # out StandardHeader zodb_id = 'StandardHeader.zpt' template = getattr(self, zodb_id, self.StandardHeader) return template.macros['standard'] # backwards compatability # StandardLook = StandardHeader def ManagerLink(self, shortlink=False, absolute_url=False): """ For the little hyperlink where you can login with """ if shortlink: link = '/redirectlogin' else: root = self.getRoot() if absolute_url: link = root.absolute_url()+'/redirectlogin' else: link = root.relative_url()+'/redirectlogin' if absolute_url: came_from = self.absolute_url()+'/' else: came_from = self.relative_url()+'/' if self.meta_type == ISSUETRACKER_METATYPE: page = self.REQUEST.URL.split('/')[-1] if page in ('AddIssue','QuickAddIssue', 'ListIssues','CompleteList', 'User'): came_from += page rurl=random.randrange(100, 200) return "%s?came_from=%s&r=%s"%(link, came_from, rurl) def standard_html_header(self): """ to make it possible to use DTML objects here """ breakword = '' page = self.StandardHeader() return page[:page.find(breakword)] def standard_html_footer(self): """ to make it possible to use DTML objects here """ breakword = '' page = self.StandardHeader() return page[page.find(breakword)+len(breakword)+1:] def BatchedQueryString(self, batchdict={}, encode=False): """ return QUERY_STRING but make sure stuff in the batchdict isn't duplicated. """ request = self.REQUEST actionurl = self.ActionURL() if isinstance(batchdict, basestring) and batchdict=='all': #request.set('start', None) url = self.aurl(actionurl, {'show':'all'}, ignore='start') elif isinstance(batchdict, basestring) and batchdict.lower()=='none': url = self.aurl(actionurl, ignore=['start','show']) else: batchdict = self._Zero2None(batchdict) url = self.aurl(actionurl, batchdict) url = self._addQuerystring(url, encode=encode) return url def _Zero2None(self, dict): """ Replace all occurances of 0 (as tested int) to None """ n_dict={} for key, value in dict.items(): try: if int(value)==0: n_dict[key]=None else: n_dict[key]=value except: n_dict[key]=value return n_dict def rememberSavedfilterPersistently(self): """ return if the last saved filter should be saved persistently. (this means, in a cookie for `FILTERVALUER_EXPIRATION_DAYS` days) """ issueuser = self.getIssueUser() default = False if issueuser: return issueuser.rememberSavedfilterPersistently(default=default) else: # look in cookies ckey = self.getCookiekey('remember_savedfilter_persistently') return Utils.niceboolean(self.get_cookie(ckey, default)) def useAccessKeys(self): """ return if the interface should use Accesskeys """ issueuser = self.getIssueUser() default = False if issueuser: return issueuser.useAccessKeys(default=default) else: # look in cookies ckey = self.getCookiekey('use_accesskeys') return Utils.niceboolean(self.get_cookie(ckey, default)) def showNextActionIssues(self): """ return if the interface should show the 'Your next action issues' on the home page. """ issueuser = self.getIssueUser() default = False if issueuser: return issueuser.showNextActionIssues(default=default) else: # look in cookies ckey = self.getCookiekey('show_nextactions') return Utils.niceboolean(self.get_cookie(ckey, default)) def ShowNameEmail(self, fromname, email=None, hideme=None, highlight=1, nolink=0, encode=True, angle_brackets=1): """ Show name and email depending on certain criterias """ out = '' if not isinstance(fromname, basestring) and hasattr(fromname, 'meta_type'): # This is a very special case. The fromname isn't a name but instead # an issue user object. Enabling for this strange parameter is why # the 'email' parameter has a default None. if fromname.meta_type == ISSUEUSERFOLDER_METATYPE: email = fromname.getEmail() fromname = fromname.getFullname() if isinstance(fromname, str): # old way fromname = Utils.html_entity_fixer(self.safe_html_quote(fromname)) fromname = self.safe_html_quote(fromname) else: # new way fromname = self.safe_html_quote(fromname.encode('ascii', 'xmlcharrefreplace')) email = Utils.html_quote(email) show_email = email if highlight: fromname = self.HighlightQ(fromname) #email = self.HighlightQ(email) show_email = self.HighlightQ(email) if not fromname and not email: name_email = NONAME_NOEMAIL elif not fromname: # Show only the email address if encode and self.EncodeEmailDisplay(): email = self.encodeEmailString(email) else: email = '%s'%(email, email) if angle_brackets: name_email ='<%s>'%email else: name_email = email elif not email: # only name was specified name_email = fromname else: # both were specified if encode and self.EncodeEmailDisplay(): name_email = self.encodeEmailString(email, fromname) else: name_email = '%s'%(email, fromname) if angle_brackets: name_email = '<%s>'%(name_email) if hideme is not None and hideme: out += NAME_EMAIL_HIDDEN if self.hasManagerRole(): out += "
" + name_email else: out += name_email return out def showFilterOptions(self, checkrequest=True): """ Determine if we want to display the filter options """ request = self.REQUEST showkey = SHOW_FILTEROPTIONS_KEY rkey = 'ShowFilterOptions' if checkrequest and request.get(rkey) and int(request[rkey]): # Someone has chosen to show filter options return True for key in ('statuses','sections','urgencies','types', 'fromname','email'): if checkrequest and request.get('f-%s'%key): return True elif self.get_session('f-%s-show'%key) or self.get_session('f-%s-block'%key): return True return False def hasStoredFilter(self): """ Check if filter is stored in session """ return self.showFilterOptions(checkrequest=False) def hasFilter(self): """ check if filter is being used at all """ return self.showFilterOptions(checkrequest=True) def guessNewFiltername(self): """ pass """ default = u"" if self.hasFilter(): # get filter setup filterlogic = self.getFilterlogic() def getFVal(key, zope=self, filterlogic=filterlogic): return zope.getFilterValue(key, filterlogic, request_only=True) f_statuses = getFVal('statuses') f_sections = getFVal('sections') f_urgencies = getFVal('urgencies') f_types = getFVal('types') f_fromname = getFVal('fromname') f_email = getFVal('email') main_option = self.getFilterlogic() if main_option == 'show': start = _(u"Only") + " " else: start = _(u"Hide") + " " name = u"" if f_statuses: name += ", ".join(f_statuses) + " " + _("issues") + " " if f_sections: name += _("in") + " " + ", ".join(f_sections) + " " if f_urgencies: name += _("that are") + " " + ", ".join(f_urgencies) + " " if f_types: name += _("of type") + " " + ", ".join(f_types) + " " if f_fromname and f_email: L = [f_fromname.strip(), f_email.strip()] name += _("by") + " " + ', '.join(L) + " " elif f_fromname: name += _("by") + " " + f_fromname.strip() + " " elif f_email: name += _("by") + " " + f_email.strip() + " " if name: return start + name.strip() else: return default else: return default def useFilterName(self, saved_filter=None): """ help return to the list page again but with the 'saved-filter' variable applied on the REQUEST. This method basically supports those people who use the Go button on the filter_options. The Go button is hidden by stylesheets plus that the accompanying select input redirects on change.""" if saved_filter is None: saved_filter = self.REQUEST.get('saved-filter','') page = self.whichList() url = "%s/%s" % (self.getRootURL(), page) url = Utils.AddParam2URL(url, {'saved-filter':saved_filter}) self.REQUEST.RESPONSE.redirect(url) def saveFilterOption(self, fname=None, REQUEST=None): """ here we store the current filter options into the instance and save the reference to it into the user. If the user is not an Issue User we'll have to store it as a cookie. """ # 1. get all the values of the filter. when we do this # it will automatically pick up all the new values and store # them in a session. filterlogic = self.getFilterlogic() def getFVal(key, zope=self, filterlogic=filterlogic): return zope.getFilterValue(key, filterlogic, request_only=True) f_statuses = getFVal('statuses') f_sections = getFVal('sections') f_urgencies = getFVal('urgencies') f_types = getFVal('types') f_fromname = getFVal('fromname') f_email = getFVal('email') _c_key = LAST_SAVEDFILTER_ID_COOKIEKEY _c_key = self.defineInstanceCookieKey(_c_key) # 2. Get a nice filter name if fname is None: fname = "" elif fname == 'null': # might come from javascript fname = "" fname = fname.strip() if not fname: fname = self.guessNewFiltername() if fname == '': # no filter settings to save from. # Perhaps the user manually reset each and every filter if self.has_session('last_savedfilter_id'): self.delete_session('last_savedfilter_id') if self.has_cookie(_c_key): debug("Expire cookie %s" % _c_key, steps=1) self.expire_cookie(_c_key) return # 2.1. (optimisation) # if the last saved filter is the same as this one, # then don't bother saving it again last_savedfilter_id = self.get_session('last_savedfilter_id') if not last_savedfilter_id and self.rememberSavedfilterPersistently(): # try fetching it via a cookie and transfer it to a session last_savedfilter_id = self.get_cookie(_c_key, None) if last_savedfilter_id: self.set_session('last_savedfilter_id', last_savedfilter_id) if last_savedfilter_id and self.hasSavedFilterObject(last_savedfilter_id): last_saved_filter = self.getSavedFilterObject(last_savedfilter_id) if last_saved_filter.getTitle() == fname: return # 3.5. Load the basic properties issueuser = self.getIssueUser() zopeuser = self.getZopeUser() acl_adder = fromname = email = cookie_key = None if issueuser: acl_adder = issueuser.getIssueUserIdentifierString() elif zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_adder = ','.join([path, name]) fromname = self.get_cookie(self.getCookiekey('name')) email = self.get_cookie(self.getCookiekey('email')) if not (acl_adder or fromname or email): # the user hasn't identified herself, then create a cookie key # and use that instead # save this in a cookie ckey = self.getCookiekey('saved-filters') ckey = self.defineInstanceCookieKey(ckey) if self.has_cookie(ckey): cookie_key = self.get_cookie(ckey) else: cookie_key = Utils.getRandomString() # attach this to the user self.set_cookie(ckey, cookie_key, days=FILTERVALUER_EXPIRATION_DAYS) valuer = self._getOrCreateFilterValuer(fname, acl_adder, fromname=fromname, email=email, cookie_key=cookie_key) # 3.4. # to save time the next time, save that id that was created here self.set_session('last_savedfilter_id', valuer.getId()) if self.rememberSavedfilterPersistently(): key = LAST_SAVEDFILTER_ID_COOKIEKEY key = self.defineInstanceCookieKey(key) self.set_cookie(key, valuer.getId(), days=FILTERVALUER_EXPIRATION_DAYS) # 3.5. Load all the values in for the filter valuer.set('filterlogic', filterlogic) valuer.set('statuses', f_statuses) valuer.set('sections', f_sections) valuer.set('urgencies', f_urgencies) valuer.set('types', f_types) valuer.set('fromname', f_fromname) valuer.set('email', f_email) if REQUEST is not None: # return the listing issues but now with this filter as # the chosen one page = REQUEST.get('page', self.whichList()) page = ss(page) if page == 'listissues': page = '/ListIssues' elif page == 'completelist': page = '/CompleteList' else: raise NotFound url = self.getRootURL()+page url = Utils.AddParam2URL(url, {'saved-filter':id}) REQUEST.RESPONSE.redirect(url) else: return id def _getOrCreateFilterValuer(self, filtername, acl_adder, fromname, email, cookie_key): """ if we can't find a matching filtername already, create a new one """ container = self._getFilterValueContainer() found_filters = self._findOldMatchingFilters(filtername, acl_adder, adder_fromname=fromname, adder_email=email, cookie_key=cookie_key) if found_filters: found_filters = self.sortSequence(found_filters, (('mod_date',),)) valuer = found_filters[0] # default sort is newest first # update the mod_date on the most recent one and... valuer.updateModDate() # ...delete the rest. The reason we do this is that there's no point # in keeping filters (if there are any) that have this filtername. rest = found_filters[1:] if rest: ids = [x.getId() for x in rest] try: container.manage_delObjects(ids) except: for restid in ids: try: container.manage_delObjects([restid]) except: logger.error("Could not delete valuerid %r" % restid, exc_info=True) return valuer # 2. generate a suitable id if hasattr(container, 'id_counter'): id = getattr(container, 'id_counter') # this is an int container.manage_changeProperties({'id_counter':id + 1}) id = str(id + 1) else: id = str(len(container.objectValues())+1) if safe_hasattr(container, id): id = str(int(id) + 1) while safe_hasattr(container, id): id = str(int(id) + 1) container.manage_addProperty('id_counter', int(id)+1, 'int') # 3.3. create instance and register as object instance = FilterValuer(id, filtername) container._setObject(id, instance) valuer = container._getOb(id) if acl_adder: valuer.set('acl_adder', acl_adder) if fromname: valuer.set('adder_fromname', fromname) if email: valuer.set('adder_email', email) if cookie_key: valuer.set('key', cookie_key) valuer.index_object() try: if len(container.objectIds()) > FILTERVALUEFOLDER_THRESHOLD_CLEANING: msg = self.CleanOldSavedFilters(user_excess_clean=1) logger.info("Cleaned old saved filters %s" % str(msg)) except: logger.error("Failed to check for filtervaluer excess", exc_info=True) try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass return valuer def _findOldMatchingFilters(self, filtername, acl_adder=None, adder_fromname=None, adder_email=None, cookie_key=None): """ delete filtervaluers that have this exact filtername, and match also either the acl_adder or adder_fromname and adder_email together. """ if not (acl_adder or adder_fromname or adder_email or cookie_key): raise UnmatchableError, "must provide either acl_adder or "\ "adder_fromname and adder_email or cookie_key" search = {'title':filtername} search['meta_type'] = FILTEROPTION_METATYPE search['sort_on'] = 'mod_date' search['sort_order'] = 'reverse' if acl_adder: search['acl_adder'] = acl_adder elif cookie_key: search['key'] = cookie_key else: assert adder_fromname or adder_email, "one must exist" if adder_fromname: search['adder_fromname'] = adder_fromname if adder_email: search['adder_email'] = adder_email catalog = self.getFilterValuerCatalog() if catalog is None: catalog = self._setupFilterValuerCatalog() objects = [] for brain in catalog.searchResults(**search): try: object = brain.getObject() assert object.getTitle().lower() == filtername.lower(), \ "%r != %r" % (object.getTitle().lower(), filtername.lower()) except KeyError: logger.warn("Saved filters catalog out of sync. Press Update Everything") continue objects.append(object) return objects def _getFilterValueContainer(self): """ return a BTreeFolder2 or a folder object where we can save all the filter value objects """ folderid = FILTERVALUEFOLDER_ID root = self.getRoot() if safe_hasattr(root, folderid): return getattr(root, folderid) else: if self.manage_canUseBTreeFolder(): _adder = root.manage_addProduct['BTreeFolder2'].manage_addBTreeFolder else: _adder = root.manage_addFolder _adder(folderid) self._setupFilterValuerCatalog() return getattr(root, folderid) def _implodeFilterValueContainerIfPossible(self): """ delete the save-filters container if it's empty """ container = self._getFilterValueContainer() if len(container.objectIds()) == 0: objid = container.getId() assert objid == FILTERVALUEFOLDER_ID parent = aq_parent(aq_inner(container)) parent.manage_delObjects([objid]) return True return False def hasSavedFilterObject(self, objectid): """ return if there is an object like this """ # do we have a container? if hasattr(self.getRoot(), FILTERVALUEFOLDER_ID): try: return hasattr(self._getFilterValueContainer(), objectid) except: return False else: return False def getSavedFilterObject(self, objectid): """ return the filtervaluer object """ return getattr(self._getFilterValueContainer(), objectid) def getMySavedFilters(self, howmany=10): # New, cataloged saved filter """ return an list of filtervaluer objects that belongs to the current user """ folderid = FILTERVALUEFOLDER_ID root = self.getRoot() if not safe_hasattr(root, folderid): return [] issueuser = self.getIssueUser() zopeuser = self.getZopeUser() search = {} if issueuser: search['acl_adder'] = issueuser.getIssueUserIdentifierString() elif zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() search['acl_adder'] = ','.join([path, name]) else: email_cookiekey = self.getCookiekey('email') name_cookiekey = self.getCookiekey('name') key = self.getCookiekey('saved-filters') key = self.defineInstanceCookieKey(key) key = self.get_cookie(key) fromname = self.get_cookie(name_cookiekey) email = self.get_cookie(email_cookiekey) if fromname or email: if fromname: search['adder_fromname'] = fromname if email: search['adder_email'] = email else: search['key'] = key if not search: # then there's nothing to identify this user by # so we can't fish out her saved filters return [] else: search['meta_type'] = FILTEROPTION_METATYPE search['sort_on'] = 'mod_date' search['sort_order'] = 'reverse' if howmany: search['sort_limit'] = int(howmany) # now use this to make a catalog search catalog = self.getFilterValuerCatalog() if catalog is None: catalog = self._setupFilterValuerCatalog() objects = [] for brain in catalog.searchResults(**search): objects.append(brain.getObject()) return objects def getCurrentlyUsedSavedFilter(self, request_only=True): """ look for saved-filter key in request or in session """ rkey = 'saved-filter' request = self.REQUEST if request_only: return request.get(rkey) else: return request.get(rkey, self.get_session('last_savedfilter_id')) def HighlightQ(self, text, q=None, highlight_html=None, highlight_digits=False): """ Highlight a piece of a text from q """ _checker = lambda p: p.find('ListIssues') + p.find('CompleteList') > -2 if highlight_html is None: highlight_html = '%s' if q is None: # then look for it in REQUEST q = self.REQUEST.get('q',None) current_page = self.REQUEST.URL list_or_complete = _checker(current_page) if q is None and not list_or_complete: # look at the HTTP_REFERER referer = self.REQUEST.get('HTTP_REFERER','') if referer and _checker(referer): try: querystring = referer.split('?')[1] qs = cgi.parse_qs(querystring) if qs.has_key('q'): q = qs.get('q')[0] if q: # so that consecutive calls to HighlightQ() # doesn't need to dig it out again self.REQUEST.set('q',q) except IndexError: pass if q is None: return text else: transtab = string.maketrans('/ ','_ ') q=string.translate(q, transtab, '?&!;<=>*#[]{}') highlightfunction = lambda x: highlight_html % x for q in self.QasList(q): if highlight_digits and q.isdigit(): #text = re.sub('(%s)'% re.escape(q), highlightfunction(r'\1'), text) text = Utils.highlightCarefully(q, text, highlightfunction, word_boundary=False) #r=re.compile(r'\b(%s)\b' % re.escape(q), re.I) #text = r.sub(highlightfunction(r'\1'), text) text = Utils.highlightCarefully(q, text, highlightfunction) return text def _text_replace(self, text, old, new): """ A custom string replace that doesn't have choke on tags. Don't do string replace on tags basically.""" t=[] for part in text.split('<'): if part.find('>')>-1: t.append('<%s>'%part[0:part.find('>')]) t.append(part[part.find('>')+1:].replace(old, new)) else: t.append(part.replace(old,new)) return ''.join(t) def _getrandstr(self,l=5): """ """ pool="0123456789" s='' for i in range(l): s='%s%s'%(s,random.choice(list(pool))) return s def colorizeThreadChange(self, title): """ Make "Changed status from Open to... to "Changed status from Open to... """ highlight_html = '\1' statuses = self.getStatuses() assignment_statuses = ['Rejected','Accepted','Reassigned'] combined = statuses + assignment_statuses regex = regex = '|'.join([r'\b%s\b'%x for x in combined]) regex = '(%s)'%regex status_reg = re.compile(regex, re.I) title = re.sub(status_reg, highlight_html, title) return title def QasList(self, q): """ q is a string that might contain 'and' and/or 'or'. Remove that and make it a list. """ r=re.compile(r"\band\b|\bor\b", re.IGNORECASE) return r.sub("", q).split() def HeadingLinks(self, display, sortname, default=0, inverted=0, sortinfo=None): """Returns a hyperlink that can be used for resorting the listing. 'inverted' means that it's default behaviour is not ASC, it's DESC. """ request = self.REQUEST querystring = request.QUERY_STRING if sortinfo is None: sortorder, reverse = self.getSortOrder(self.REQUEST) else: sortorder, reverse = sortinfo if sortorder == sortname: # have sorted by this, just let them reverse if reverse: descending = self.www['descarrow.gif'].tag(hspace=2, alt="Descending order") ps = {'sortorder':sortname, 'reverse':None} url = self.aurl(request.URL, ps) url = self._addQuerystring(url) url = self.relative_url(url) return '%s%s'\ % (url, SORT_BY, display, display, descending) else: ascending = self.www['ascarrow.gif'].tag(hspace=2, alt="Ascending order") ps = {'sortorder':sortname, 'reverse':'true'} url = self.aurl(request.URL, ps) url = self._addQuerystring(url) url = self.relative_url(url) return '%s%s'\ % (url, SORT_REVERSE, display, ascending) else: if 0:#startreversed: ps = {'sortorder':sortname, 'reverse':True} url = self.aurl(request.URL, ps) url = self._addQuerystring(url) url = self.relative_url(url) return '%s'\ % (url, SORT_BY, display, display ) else: ps = {'sortorder':sortname, 'reverse':None} url = self.aurl(request.URL, ps) url = self._addQuerystring(url) url = self.relative_url(url) return '%s' \ % (url, SORT_BY, display, display ) def _addQuerystring(self, url, encode=True): """ Add REQUEST querystring """ querystring = self.REQUEST.get('QUERY_STRING','') if querystring is not None and querystring.strip()!='': if encode: url = "%s?%s"%(url, querystring.replace('&','&')) else: url = "%s?%s"%(url, querystring) return url ## Form submission helpers def has_key_special(self, name, shorten=0): """ Normally you would do REQUEST.has_key('IssueAction') but if an imagebutton is used you'll find that you have REQUEST['IssueAction.y'] and REQUEST['IssueAction.x'] But the result should be the same. """ request = self.REQUEST if request.has_key(name): return True elif request.has_key('%s.y'%name) and request.has_key('%s.x'%name): return True elif shorten: for key in request.keys(): if key[:len(name)]==name: return True return False else: return False def get_special_key(self, name): """ Normally you would do REQUEST.has_key('IssueAction') but if an imagebutton is used you'll find that you have REQUEST['IssueAction.y'] and REQUEST['IssueAction.x'] But the result should be the same. """ try: return self.REQUEST[name] except KeyError: try: return self.REQUEST['%s.x'%name] except: raise KeyError, name ## Error related def ShowSubmitError(self, options, id, linebreak=0): """ errordict is a dictionary of errors """ s = '' errordict = options.get('SubmitError',{}) if errordict and errordict.has_key(id): s = errordict.get(id) if s and linebreak: s += '
' return s ## Deleting an issue security.declareProtected(DeleteIssues, 'DeleteIssue') def DeleteIssue(self): """ Delete an Issue from the IssueTracker instance """ request = self.REQUEST if request.has_key('issueID') and self.hasManagerRole(): container = self._getIssueContainer() issue = getattr(container, request['issueID']) container.manage_delObjects(request['issueID']) # delete all notifications about this Issue del_notify_ids = [] for notifyobject in self.objectValues('Issue Notification'): if notifyobject.issueID == request['issueID']: del_notify_ids.append(notifyobject.id) self.manage_delObjects(del_notify_ids) listpage = '/%s'%self.whichList() request.RESPONSE.redirect(request.URL1+listpage) else: msg = "The issueID could not be found in the REQUEST" raise ValueError, msg ## Sys admin security.declareProtected('Access IssueTracker', 'redirectlogin') def redirectlogin(self, came_from=None): """ this method is protected so that when viewed the user will have been logged in. """ if not came_from: came_from = self.getRootURL() + '/' elif came_from.startswith('/'): came_from = self.REQUEST.BASE0 + came_from issueuser = self.getIssueUser() if issueuser and issueuser.mustChangePassword(): url = self.getRootURL()+'/User_must_change_password' params = {'cf':came_from} came_from = Utils.AddParam2URL(url, params) self.REQUEST.RESPONSE.redirect(came_from) def StopCache(self): """ Maybe we should set some cachepreventing headers """ if self.doStopCache(): response = self.REQUEST.RESPONSE now = DateTime().toZone('GMT').rfc822() response.setHeader('Expires', now) response.setHeader('Cache-Control','public,max-age=0') response.setHeader('Pragma','no-cache') # for HTTP 1.0 def doCache(self, hours=10): """ set cache headers on this request if not in debug mode """ if not self.doDebug() and hours > 0: response = self.REQUEST.RESPONSE now = DateTime() then = now+int(hours/24.0) response.setHeader('Expires',then.rfc822()) response.setHeader('Cache-Control', 'public,max-age=%d' % int(3600*hours)) def sendEmail(self, msg, to, fr, subject, swallowerrors=False, headers={}): """ this is the new sendEmail that works much better but with Unicode instead """ if DEBUG: # print the email instead of sending it out = sys.stdout print >>out, "To: %s\n" % to print >>out, "From: %s\n" % fr print >>out, "Subject: %s\n" % subject print >>out, "\n" if isinstance(msg, unicode): print >>out, msg.encode('ascii','replace') else: print >>out, msg return True try: header_charset = 'ISO-8859-1' #header_charset = UNICODE_ENCODING # We must choose the body charset manually for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8', 'LATIN-1': try: msg.encode(body_charset) except UnicodeError: pass else: break #body_charset = UNICODE_ENCODING # Split real name (which is optional) and email address parts fr_name, fr_addr = parseaddr(fr) to_name, to_addr = parseaddr(to) # Make sure email addresses do not contain non-ASCII characters fr_addr = fr_addr.encode('ascii') to_addr = to_addr.encode('ascii') # We must always pass Unicode strings to Header, otherwise it will # use RFC 2047 encoding even on plain ASCII strings. fr_name = str(Header(unicode(fr_name), header_charset)) to_name = str(Header(unicode(to_name), header_charset)) headers_clean={} for key, value in headers.items(): if isinstance(key, str) and key.strip(): key = key.strip() if key.endswith(':'): key = key[:-1] value = str(value).strip() headers_clean[key] = value # Create the message ('plain' stands for Content-Type: text/plain) try: msg_encoded = msg.encode(body_charset) except UnicodeDecodeError: if isinstance(msg, str): try: msg_encoded = unicode(msg, body_charset).encode(body_charset) except UnicodeDecodeError: logger.warn("Unable to encode msg (type=%r, body_charset=%s)" %\ (type(msg), body_charset), exc_info=True) msg_encoded = Utils.internationalizeID(msg) else: logger.warn("Unable to encode msg (type=%r, body_charset=%s)" %\ (type(msg), body_charset), exc_info=True) msg_encoded = Utils.internationalizeID(msg) message = MIMEText(msg_encoded, 'plain', body_charset) message['From'] = formataddr((fr_name, fr_addr)) message['To'] = formataddr((to_name, to_addr)) message['Subject'] = Header(unicode(subject), header_charset) for k, v in headers_clean.items(): message[k] = Header(unicode(v), header_charset) mailhost = self._findMailHost() # We like to do our own (more unicode sensitive) munging of headers and # stuff but like to use the mailhost to do the actual network sending. mailhost._send(fr, to, message.as_string()) return True except: debug("Failed to send email") debug(msg, steps=4) typ, val, tb = sys.exc_info() if swallowerrors: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass _classname = self.__class__.__name__ _methodname = inspect.stack()[1][3] LOG("%s.%s"%(_classname, _methodname), ERROR, 'Could not send email to %s'%to, error=sys.exc_info()) return False else: raise typ, val def _findMailHost(self): """ find a suitable MailHost object and return it. """ # root instance object of issuetracker root = self.getRoot() # root instance object but without deeper acquisition rootbase = getattr(root, 'aq_base', root) ## Notice the order of this if-statement. # 1. 'MailHost' explicitly in the issuetrackerroot # (would fail if the MailHost is defined "deeper") if hasattr(rootbase, 'MailHost'): mailhost = self.MailHost # 2. 'SecureMailHost' explicitly in the issuetrackerroot # (would fail if the SecureMailHost is defined "deeper") elif hasattr(rootbase, 'SecureMailHost'): mailhost = self.SecureMailHost # 3. Any 'MailHost' in acquisition elif hasattr(self, 'MailHost'): mailhost = self.MailHost # 4. Any 'SecureMailHost' in acquisition elif hasattr(self, 'SecureMailHost'): mailhost = self.SecureMailHost else: # desperate search all_mailhosts = self.superValues(['Secure Mail Host', 'Mail Host']) if all_mailhosts: mailhost = all_mailhosts[0] # first one else: raise AttributeError, "MailHost object not found" return mailhost ## ## Listing issues ## def searchWithOR(self, q=None): """ return true if there is a search and if that search isn't already "orified" :) """ if q is None: request = self.REQUEST q = request.get('q') if q: if str(q).lower().find(' or ') == -1: terms_list = Utils.splitTerms(q) if len(terms_list) > 1: return " or ".join(terms_list) return False def useFilterInSearch(self): """ default is to use filter in search, but first check if there's something in session. """ key = USE_FILTER_IN_SEARCH_SESSION_KEY default = False if self.REQUEST.has_key('filter_in_search'): filter_in_search = self.REQUEST.get('filter_in_search') try: return not not int(filter_in_search) except ValueError: return not not filter_in_search else: return self.get_session(key, default) def ListIssuesFiltered(self, q=None, **kw): """ wrapper around _ListIssuesFiltered() that prepares a search if REQUEST holds 'q' """ request = self.REQUEST q_orig = q if q is None and request.get('q','').strip(): q = q_orig = request.get('q').strip() transtab = string.maketrans('/ ','_ ') q = string.translate(q, transtab, '?&!;<=>*#[]{}') ##q=q.replace('%','*') # allow both wildcards # needs thought i = None if request.has_key('i'): # user filtering welcomed_i = ('Added','FollowedUp','Assigned','Subscribed') welcomed_i = [ss(x) for x in welcomed_i] if ss(request.get('i')) in welcomed_i: i = request.get('i') report = None if request.has_key('report'): # check that the report script exists container = self.getReportsContainer() if hasattr(container, request.get('report')): report = getattr(container, request.get('report')) else: # try case insensitivity lowercase_key = str(request.get('report')).lower().strip() for scriptid, scriptobject in container._getAllScriptItems(): if scriptid.lower() == lowercase_key: report = scriptobject request.set('report', scriptid) break if request.has_key('filter_in_search'): filter_in_search = request.get('filter_in_search') elif request.has_key('q'): filter_in_search = False else: filter_in_search = True try: filter_in_search = not not int(filter_in_search) except ValueError: filter_in_search = not not filter_in_search # remember this self.set_session(USE_FILTER_IN_SEARCH_SESSION_KEY, filter_in_search) if q is not None and q_orig.startswith('#') and q in self.getIssueIds(): # q was like '#00123', just go to the issue response = request.RESPONSE url = self.getIssueObject(q).absolute_url() response.redirect(url, lock=1) return [] elif q is not None and len(q.split(',')) > 1 and self._validIssueIDList(q): issue_ids = self._splitIssueIDList(q) seq = [] for issue_id in issue_ids: seq.append(self.getIssueObject(issue_id)) elif q is not None: # Use catalog to search try: seq = self._searchCatalog(q, search_only_on=request.get('search_only_on')) except ParseError, msg: request.set('SearchError', msg) seq = [] except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass seq = [] # searched and found one? if len(seq) == 1 and not filter_in_search: # then redirect response = request.RESPONSE url = seq[0].absolute_url() params = {} # So, only one issue has been found. We'll redirect there. # Now it's just a question of whether we'll include the searchterm # they used or if we're just going to go there. # We'll just go there if the searchterm was a issuenumber but if it # wasn't then include the search term in the redirect if not str(q).replace('#','').replace(self.issueprefix,'').isdigit(): params = {'q':q} url = Utils.AddParam2URL(url, params) response.redirect(url, lock=1) return [] elif i is not None: # The source is by this user seq = self.getMyIssues(i) elif report is not None: seq = self._generateReport(report) self.RememberReportRun(report.getId(), len(seq)) else: # We won't need the ZCatalog, we can use objectValues() which # is many times faster if the amount of issues is small seq = self.getIssueObjects() if q_orig is not None: # Remember this searchterm self.RememberSearchTerm(q_orig, len(seq)) skip_filter = kw.get('skip_filter', not filter_in_search) skip_sort = kw.get('skip_sort', False) # transfer some parameters over to request, # because that's how they are being fetched inside # _ListIssuesFiltered() if kw.has_key('sortorder'): request.set('sortorder', kw.get('sortorder')) if kw.has_key('keep_sortorder'): request.set('keep_sortorder', kw.get('keep_sortorder')) if kw.has_key('reverse'): request.set('reverse', kw.get('reverse')) return self._ListIssuesFiltered(seq, skip_filter=skip_filter, skip_sort=skip_sort) def _validIssueIDList(self, comma_delimited_string): """ return true or false, a wrapper around _splitIssueIDList() """ return bool(self._splitIssueIDList(comma_delimited_string)) def _splitIssueIDList(self, comma_delimited_string): """ return true if the 'comma_delimited_string' is a comma separated list of valid issue ids that can be found. The format of the string might be like '#0234, #0456' or '#234, #456' or '234, 456' or '0234, 0456' or any combination of each but nothing else. The issue formatting might correct for this issue tracker instance but the issue must still exist in the database. """ parts = [x.strip() for x in comma_delimited_string.split(',')] assert len(parts) > 1, "String %r not comma separated" % comma_delimited_string zfill_length = self.randomid_length if self.issueprefix: _regex = '^(\d{1,%s}|\#\d{1,%s}|%s\d{1,%s)$' ok_issue_id = re.compile(_regex % (zfill_length, zfill_length, self.issueprefix, zfill_length)) else: _regex = '^(\d{1,%s}|\#\d{1,%s})$' ok_issue_id = re.compile(_regex % (zfill_length, zfill_length)) all_issue_ids = self.getIssueIds() ok = [] for part in parts: # this is an inversion of the regular expression test. # If there's nothing but the OK issue id pattern, then # it's ok. if not ok_issue_id.sub('', part) and bool(ok_issue_id.findall(part)): part = part.replace('#','') part = string.zfill(part, zfill_length) if part in all_issue_ids: ok.append(part) return ok def getReportIssues(self, report_id): """ wrapper around _generateReport(report object) that returns a list of issue objects. This method is useful if you for example want to figure something out about the issues that a report returns. """ container = self.getReportsContainer() report = getattr(container, report_id, None) assert report.meta_type == REPORTSCRIPT_METATYPE, \ "Not a Report script object" return self._generateReport(report) def _generateReport(self, report): """ return a sequence of issues where each issues yields a true result when applied on the report script. """ checked = [] for issue in self.getIssueObjects(): if report(issue): checked.append(issue) report.setYieldCount(len(checked)) return checked def _searchCatalog(self, q, search_only_on=None): """ return a sequence of issue objects by searching and possibly searching inside the threads. """ request = self.REQUEST catalog = self.getCatalog() seq = [] titleq = '*'+q+'*' # prepare the search result variables _exact_title_search = [] _title_search = [] _description_search = [] _fromname_search = [] _email_search = [] if search_only_on: if isinstance(search_only_on, basestring): search_only_on = [search_only_on] search_only_on = [ss(s) for s in search_only_on] else: search_only_on = None # all the different searches catalogs = [] if not search_only_on or 'title' in search_only_on: _exact_title_search = catalog.searchResults(title=q) catalogs += _exact_title_search if not _exact_title_search: _title_search = catalog.searchResults(title=titleq) catalogs += _title_search ss_q = ss(q) if ss_q in [ss(x) for x in self.statuses]: # find the correct case for each in self.statuses: if ss(each) == ss_q: self._setSearchFilterWarning(status=each) break elif ss_q in [ss(x) for x in self.sections_options]: # find the correct case for each in self.sections_options: if ss(each) == ss_q: self._setSearchFilterWarning(section=each) break elif ss_q in [ss(x) for x in self.urgencies]: # find the correct case for each in self.urgencies: if ss(each) == ss_q: self._setSearchFilterWarning(urgency=each) break elif ss_q in [ss(x) for x in self.types]: # find the correct case for each in self.types: if ss(each) == ss_q: self._setSearchFilterWarning(type_=each) break if len(catalogs) < self.default_batch_size: _description_search = catalog.searchResults(description=q) catalogs += _description_search # there now? if len(catalogs) < self.default_batch_size: # dig deeper _author_search = [] if not search_only_on or 'fromname' in search_only_on: _author_search.extend(catalog.searchResults(fromname=q)) if not search_only_on or 'email' in search_only_on: _author_search.extend(catalog.searchResults(email=q)) catalogs += _author_search if len(_author_search) > 0: # advise people to use the filter msg = self._setSearchFilterWarning(author=q) # Now, also search on comment catalogs_threads = [] if not search_only_on or 'comment' in search_only_on: catalogs_threads = catalog.searchResults(comment=q) if len(catalogs)+len(catalogs_threads)==0: # nothing found, maybe user typed in an id _issue_objectids = self.getIssueIds() if q in _issue_objectids: object = getattr(self, q) return [object] elif string.zfill(q, self.randomid_length) in _issue_objectids: object = getattr(self, string.zfill(q, self.randomid_length)) return [object] # these variables are used in the loop to avoid calling LOG() # for every bloody object that goes wrong _has_logged_about_NoneType = 0; _has_logged_about_metatype = 0 _has_logged_about_Issue_metatype = 0 # Convert our search result to a list of unique issue objects for brain in catalogs: object = brain.getObject() if getattr(object, 'meta_type','') != ISSUE_METATYPE: if not _has_logged_about_Issue_metatype: _has_logged_about_Issue_metatype = 1 m = "%s has cataloged thread objects with titles. " m = m % catalog.getId() m += "Have you done a manual update on the catalog? " m += "Please press the Update Everything button under the "\ "Management tab in the Zope management interface." LOG(self.__class__.__name__, WARNING, m) continue if object not in seq: seq.append(object) # Also, search the file attachments if len(q) >= 2: indexes = catalog._catalog.indexes if 'filenames' in indexes: _finder = self._searchByFilename else: import warnings warnings.warn("It appears you don't have the 'filenames' index in your ZCatalog. "\ "To enable much quicker searches, press the Update Everything "\ "button in the Zope management interface.", DeprecationWarning) _finder = self._findby_filename for issue in _finder(q): if issue not in seq: seq.append(issue) first_thread_id = None for threadbrain in catalogs_threads: threadobject = threadbrain.getObject() if threadobject is None: if not _has_logged_about_NoneType: _has_logged_about_NoneType = 1 m = "%s has references to Zope objects that do not exist. " m = m%self.getCatalog().getId() m += "Please press the Update Everything button under the "\ "Management tab in the Zope management interface." LOG(self.__class__.__name__, WARNING, m) continue elif getattr(threadobject, 'meta_type', '') != ISSUETHREAD_METATYPE: if not _has_logged_about_metatype: _has_logged_about_metatype = 1 m = "%s has references to Zope objects that are not of type %s. " m = m%(self.getCatalog().getId(), ISSUETHREAD_METATYPE) m += "Please press the Update Everything button under the "\ "Management tab in the Zope management interface." LOG(self.__class__.__name__, WARNING, m) continue object = threadobject.aq_parent if object not in seq: if first_thread_id is None: first_thread_id = object.getId() request.set('FirstThreadResultId', first_thread_id) seq.append(object) if self.searchWithOR(q) and search_only_on is None: for issue in self._searchCatalog(self.searchWithOR(q)): if issue not in seq: seq.append(issue) return seq def _searchByFilename(self, q): """ Search all file attachments """ sR = self.getCatalog().searchResults qparts = [ss(x) for x in q.split() if ss(x) not in ('and','or','not')] brains = sR(filenames=qparts) issues = [] for brain in brains: obj = brain.getObject() if obj: if obj.meta_type == ISSUETHREAD_METATYPE: issue = aq_parent(aq_inner(obj)) else: issue = obj if issue not in issues: issues.append(issue) return issues def _findby_filename(self, q): """ Search all file attachments """ q = q.lower() issues = [] r = self.ZopeFind(self, obj_metatypes=['File'], search_sub=1) valid_meta_types = [ISSUE_METATYPE, ISSUETHREAD_METATYPE] for file in r: path, fileobject = file parent = fileobject.aq_parent if parent.meta_type in valid_meta_types and path.lower().find(q)>-1: if parent.meta_type == ISSUETHREAD_METATYPE: issues.append(aq_parent(aq_inner(parent))) else: issues.append(parent) return issues def _setSearchFilterWarning(self, author=None, status=None, section=None, urgency=None, type_=None): """ put a HTML chunk in REQUEST about how the user can user the filter feature instead of search based on what they searched for. """ msg = None url = self.getRootURL()+'/'+self.whichList() params = {'ShowFilterOptions':'1'} if author: msg = 'You can use the filter options to filter on people' if Utils.ValidEmailAddress(author): params['f-email'] = author else: params['f-fromname'] = author url = Utils.AddParam2URL(url, params) msg = msg%url elif section: msg = 'You can use the filter options to filter on sections' params['f-sections'] = section url = Utils.AddParam2URL(url, params) msg = msg%url elif status: msg = 'You can use the filter options to filter on status' params['f-statuses'] = status url = Utils.AddParam2URL(url, params) msg = msg%url elif urgency: msg = 'You can use the filter options to filter on different urgencies' params['f-urgencies'] = urgency url = Utils.AddParam2URL(url, params) msg = msg%url elif type_: msg = 'You can use the filter options to filter on different types' params['f-types'] = type_ url = Utils.AddParam2URL(url, params) msg = msg%url if msg: self.REQUEST.set('SearchFilterWarning', msg) def _ListIssuesFiltered(self, issues, skip_filter=False, skip_sort=False): """ Filter and sort """ request = self.REQUEST # 1. Remember how many issues there are before filtering request.set('TotalNoIssues', len(issues)) # 2. Filter issues if not skip_filter: issues = self._filterIssues(issues) # 3. Mandatory filter if not self.hasManagerRole(): issues = [issue for issue in issues if not issue.isConfidential() or issue.isYourIssue()] # 4. Sort them if not skip_sort: issues = self._sortIssues(issues, request) # 5. and we're done! return issues def _filterIssues(self, issues): """ look for things that shouldn't appear or should only appear """ # assume that we always save the current filter options _do_save_filter = True request = self.REQUEST _c_key = LAST_SAVEDFILTER_ID_COOKIEKEY _c_key = self.defineInstanceCookieKey(_c_key) # # o 'filteroptions' gets set if people press the # "Apply filter options" button on filter_options.zpt # o 'f-statuses' is from the Home page where you can clicl # all the various statuses # o 'f-sections' is from the More statistics page # if request.get('filteroptions') or request.get('f-statuses') or \ request.get('f-sections'): # they have applied some filter options # by default we want to save the filter for later _do_save_filter = True # Has this been overridden if request.has_key('remember-filterlogic'): _do_save_filter = Utils.niceboolean(request.get('remember-filterlogic')) elif request.get('saved-filter'): if self.hasSavedFilterObject(request.get('saved-filter')): filtervaluer = self.getSavedFilterObject(request.get('saved-filter')) filtervaluer.populateRequest(request) filtervaluer.incrementUsageCount() filtervaluer.updateModDate() elif self.has_session('last_savedfilter_id') or \ self.has_cookie(_c_key) and self.rememberSavedfilterPersistently(): if not self.has_session('last_savedfilter_id'): # transfer from cooke to session last_savedfilter_id = self.get_cookie(_c_key, None) if last_savedfilter_id: self.set_session('last_savedfilter_id', last_savedfilter_id) saved_filter_id = request.get('saved-filter', self.get_session('last_savedfilter_id')) if self.hasSavedFilterObject(saved_filter_id): filtervaluer = self.getSavedFilterObject(saved_filter_id) filtervaluer.populateRequest(request) # since we're using a selected saved-filter, there's # no need to save again _do_save_filter = False # get filter setup filterlogic = self.getFilterlogic() def getFVal(key, zope=self, filterlogic=filterlogic): return zope.getFilterValue(key, filterlogic, request_only=True, ) f_statuses = getFVal('statuses') f_sections = getFVal('sections') f_urgencies = getFVal('urgencies') f_types = getFVal('types') f_fromname = getFVal('fromname') f_email = getFVal('email') if _do_save_filter: self.saveFilterOption() has_managerrole = self.hasManagerRole() checked = [] if filterlogic == 'show' and \ f_statuses is None and f_sections is None and \ f_urgencies is None and f_types is None and \ f_fromname is None and f_email is None: # Filter logic is to show only selected items but # nothing has been set so just return everything for issue in issues: if not issue.isConfidential() or has_managerrole or issue.isYourIssue(): checked.append(issue) return checked if f_fromname: _maker = Utils.createStandaloneWordRegex f_fromname_regex = _maker(f_fromname) for issue in issues: if issue.isConfidential() and not (has_managerrole or issue.isYourIssue()): continue if filterlogic == 'show': #do_add = true if f_statuses is not None: if issue.status not in f_statuses: # do_add = False continue if f_sections is not None: do_continue = 0 for subsection in f_sections: if subsection in issue.sections: # good! do_continue = 1 break if not do_continue: continue if f_urgencies is not None: if issue.urgency not in f_urgencies: # checked.append(issue) continue if f_types is not None: if issue.type not in f_types: # checked.append(issue) continue if f_fromname is not None: ##if f_fromname and ss(f_fromname) != ss(issue.getFromname()): if f_fromname and not f_fromname_regex.findall(issue.getFromname()): # checked.append(issue) continue if f_email is not None: if f_email and ss(f_email) != ss(issue.getEmail()): # checked.append(issue) continue checked.append(issue) else: # block things out then if f_statuses is not None: if issue.status in f_statuses: continue if f_sections is not None: do_continue = 0 for subsection in issue.sections: if subsection in f_sections: do_continue = 1 break if do_continue: continue if f_urgencies is not None: if issue.urgency in f_urgencies: continue if f_types is not None: if issue.type in f_types: continue if f_fromname: # conditional covers both None and "" if f_fromname_regex.findall(issue.getFromname()): continue if f_email: # conditional covers both None and "" if ss(f_email) == ss(issue.getEmail()): continue # if none of the above skipped the loop, do this checked.append(issue) return checked security.declarePublic('forceFilterValuerUpdate') def forceFilterValuerUpdate(self): """ checks if there is a filtervaluer used in the session and if so, do what _filterIssues() does, ie. to populate the REQUEST. (see larger comment in filter_options.zpt) """ request = self.REQUEST if self.has_session('last_savedfilter_id'): saved_filter_id = request.get('saved-filter', self.get_session('last_savedfilter_id')) if self.hasSavedFilterObject(saved_filter_id): filtervaluer = self.getSavedFilterObject(saved_filter_id) filtervaluer.populateRequest(request) def _sortIssues(self, issues, request): """ inspect request for how we should sort and remember the sort order """ session_key = 'sortorder' session_key_reverse = 'sortorder_reverse' if request.get('sortorder','').lower()=='search' and \ request.get('q','').strip(): return issues # If this is True, we remember the sortorder found this time # so that it can be used in the future. keep_sortorder = request.get('keep_sortorder', True) sortorder, sortorder_reverse = self.getSortOrder(request) # use special methods for some sorting if sortorder == 'urgency': issues = self._sortByUrgency(issues, not sortorder_reverse) elif sortorder == 'status': issues = self._sortByStatus(issues, sortorder_reverse) elif sortorder == 'type': issues = self._sortByType(issues, sortorder_reverse) else: do_reverse = sortorder_reverse # dates are naturally sorted in reverse if sortorder in ('modifydate', 'issuedate'): do_reverse = not do_reverse # define a dictionary of the renaming of sortorder keys. # For example, in REQUEST you can find 'sortorder=from' # but the actual attribute is called 'fromname' so it # should have been called 'sortorder=fromname' _translations = {'from':'fromname', 'changedate':'modifydate', # legacy } issues = self._dosort(issues, _translations.get(sortorder, sortorder)) if do_reverse: issues.reverse() if keep_sortorder: self.set_session(session_key, sortorder) self.set_session(session_key_reverse, sortorder_reverse) return issues def getSortOrder(self, request=None): """ return (sortorder, sortorder_reverse) based on request and SESSION """ if request is None: request = self.REQUEST session_key = 'sortorder' session_key_reverse = 'sortorder_reverse' #default_sortorder = 'modifydate' default_sortorder = self.getDefaultSortorder() default_sortorder_reverse = 0 sortorder = request.get('sortorder', self.get_session(session_key, default_sortorder)) if request.has_key('reverse'): sortorder_reverse = request.get('reverse', self.get_session(session_key_reverse, default_sortorder_reverse)) else: # then it might be deliberatly left out if request.get('sortorder'): # if so, and there is no reverse set, assume it to be # False sortorder_reverse = False else: sortorder_reverse = self.get_session(session_key_reverse, default_sortorder_reverse) request.set('reverse', sortorder_reverse) return sortorder, sortorder_reverse def _sortByStatus(self, issues, reverse=0): """ Use self.getStatuses() which is a humanly ordered list. """ statuses = {} for issue in issues: if statuses.has_key(issue.status): statuses[issue.status].append(issue) else: statuses[issue.status] = [issue] # recreate the list issues = [] default = 'modifydate' all_statuses = self.getStatuses()[:] if reverse: all_statuses.reverse() for status in all_statuses: if statuses.has_key(status): these = self._dosort(statuses[status], default) these.reverse() issues += these return issues def _sortByType(self, issues, reverse=0): """ Use self.types to sort the issues """ types = {} for issue in issues: if types.has_key(issue.type): types[issue.type].append(issue) else: types[issue.type] = [issue] # recreate the list issues = [] default = 'modifydate' all_types = self.types[:] all_types.sort() if reverse: all_types.reverse() for type in all_types: if types.has_key(type): these = self._dosort(types[type], default) these.reverse() issues += these return issues def _sortByUrgency(self, issues, reverse=0): """ Use self.urgencies to sort the issues """ urgencies = {} for issue in issues: if urgencies.has_key(issue.urgency): urgencies[issue.urgency].append(issue) else: urgencies[issue.urgency] = [issue] # recreate the list issues = [] default = 'modifydate' all_urgencies = self.urgencies[:] if reverse: all_urgencies.reverse() for urgency in all_urgencies: if urgencies.has_key(urgency): these = self._dosort(urgencies[urgency], default) these.reverse() issues += these return issues ## def _getFilter(self, request): ## """ Inspect 'request' for appropriate filter data and ## return a dictionary of filters. """ ## ## key = FILTEROPTIONS_KEY ## ## # Convert data from session into request ## filteroptions = self.get_session(key, {}) ## for each in ['ignorestatuses','ignoresections', 'ignoreurgencies', ## 'ignoretypes','showfromname','showemail']: ## if filteroptions.has_key(each): ## request.set(each, filteroptions[each]) ## ## filter = {} ## ## # statuses ## if request.has_key('ignorestatuses'): ## filter['status'] = request.ignorestatuses ## elif request.has_key('status'): ## filter['status'] = self._pop_from_list(request['status'], ## self.getStatuses()) ## else: ## filter['status'] = [] ## ## # sections ## if request.has_key('ignoresections'): ## filter['sections'] = request.ignoresections ## elif request.has_key('section'): ## filter['sections'] = [request.section.strip()] ## else: ## filter['sections'] = [] ## ## # urgencies ## if request.has_key('ignoreurgencies'): ## filter['urgency'] = request.ignoreurgencies ## elif request.has_key('urgency'): ## filter['urgency'] = [request.urgency.strip()] ## else: ## filter['urgency'] = [] ## ## # types ## if request.has_key('ignoretypes'): ## filter['type'] = request.ignoretypes ## elif request.has_key('type'): ## filter['type'] = [request.type.strip()] ## else: ## filter['type'] = [] ## ## ## fromname ## # Either a name to ignore or show only. ## # It does not make sense to have both showfromname and ignorefromname ## if request.has_key('showfromname') and \ ## request.showfromname <> self.when_ignore_word: ## filter['showfromname'] = request.showfromname.strip() ## elif request.has_key('ignorefromname') and \ ## request.ignorefromname <> self.when_ignore_word: ## filter['ignorefromname'] = request.fromname.strip() ## ## # email ## if request.has_key('showemail') and \ ## request.showemail <> self.when_ignore_word: ## filter['showemail'] = string.strip(request.showemail) ## elif request.has_key('ignoreemail') and \ ## request.ignoreemail <> self.when_ignore_word: ## filter['ignoreemail'] = request.email ## ## ## # prepare for caseindependence ## filter = Utils.fixDictofLists(filter) ## ## return filter ## def _pop_from_list(self, item, olist): ## """ remove one item from a list """ ## nlist = [] ## for oitem in olist: ## if item!=oitem: ## nlist.append(oitem) ## return nlist ## def _filter_fromnameemail(self, issue, filter): ## """ do the advanced filtering. ## If filter has ignorefromname then reject this issue. ## If filter has showfromname, then ignore this issue ## if not == showfromname ## """ ## ss = lambda x: x.strip().lower() ## # if asked to be ingored return False ## if filter.get('ignorefromname','') and \ ## ss(issue.fromname) == filter['ignorefromname']: ## return False ## elif filter.get('ignoreemail','') !='' and \ ## ss(issue.email) == filter['ignoreemail']: ## return False ## elif filter.get('showfromname','') !='' and \ ## ss(issue.fromname) != filter['showfromname']: ## return False ## elif filter.get('showemail','') !='' and \ ## ss(issue.email) != filter['showemail']: ## return False ## else: ## # not trapped in anything ## return true def _dosort(self, seq, key): """ do the actual sort """ if not isinstance(key, (tuple, list)): key = (key,) return sequence.sort(seq, (key,)) def getBatchStart(self): """ return the batchstart value """ try: return int(self.REQUEST.get('start',0)) except: return False def getBatchSize(self, default=None, factor=None): """ return the batchsize value """ request = self.REQUEST if request.get('show','')=='all' and self.AllowShowAll(): if factor: return int(1000*factor) else: return 1000 if default is None: default = self.default_batch_size try: s = int(request.get('size', default)) if factor: return int(s * factor) else: return s except: return 0 ## Recent history related # Recent reports usage # def RememberReportRun(self, reportid, result): """ remember that we've run this report """ request = self.REQUEST key = RECENTHISTORY_REPORTSKEY reports = self.get_session(key, []) as_dict = {'reportid': reportid, 'yield':result} #request.set('NotYetRecent' if as_dict not in reports: reports.insert(0, as_dict) if len(reports) > 25: # we don't want to store too much in the session # manager so limit it. reports = reports[:25] self.set_session(key, reports) def hasRecentReportRuns(self): """ return if any exist """ key = RECENTHISTORY_REPORTSKEY return self.get_session(key, {}) != {} def getRecentReportRuns(self, length=None): """ return all the recently run reports if any """ key = RECENTHISTORY_REPORTSKEY reports = self.get_session(key, {}) if length: reports = reports[:length] return reports def getNiceRecentReportRuns(self, reports): """ return a hyperlink and bracket for each yield """ reportscontainer = self.getReportsContainer() rooturl = self.getRootRelativeURL() items = [] for reportrun in reports: reportid = reportrun['reportid'] reportobject = getattr(reportscontainer, reportid, None) if not reportobject: continue href = "/%s/report-%s" % (self.whichList(), reportid) href = rooturl + href htmlchunk = '%s (%s found)' items.append(htmlchunk % (href, reportobject.title_or_id(), reportrun['yield'])) return items # Recent history SearchTerm # def RememberSearchTerm(self, q, result): """ Stick this in a session variable """ request = self.REQUEST key = RECENTHISTORY_SEARCHKEY searches = self.get_session(key, []) as_dict = {'q':unicodify(q), 'yield':result} request.set('NotYetRecent', as_dict) if as_dict not in searches: searches.insert(0, as_dict) #searches.append(as_dict) if len(searches)>25: # we don't want to store too much in the session # manager so limit it. searches = searches[:25] self.set_session(key, searches) def hasRecentSearchTerms(self): """ check if any exists """ key = RECENTHISTORY_SEARCHKEY return self.get_session(key, {})!={} def getRecentSearchTerms(self, length=None): """ Return if any exists """ key = RECENTHISTORY_SEARCHKEY searches = self.get_session(key, {}) if length: searches = searches[:length] return searches def getNiceRecentSearchTerms(self, searches): """ return a hyperlink and a bracket with the yield """ if self.thisInURL('CompleteList'): page = '/CompleteList' else: page = '/ListIssues' actionurl = self.getRootRelativeURL()+page actionurl = self.aurl(actionurl, {'sortorder':'search'}) items = [] for term in searches: q = term['q'] if isinstance(q, str): q_quoted = Utils.url_quote_plus(q) else: try: q_quoted = Utils.url_quote_plus(q.encode(UNICODE_ENCODING)) except UnicodeEncodeError: q_quoted = Utils.url_quote_plus(q.encode('ascii','xmlcharrefreplace')) href = actionurl + '?q=%s' % q_quoted if isinstance(q, str): # old way q_nice = Utils.html_entity_fixer(q) else: q_nice = q htmlchunk = '%s '%(href, q_nice) htmlchunk += '(%s found)'%term['yield'] items.append(htmlchunk) return items # Recent history IssueVisit # def RememberIssueVisit(self, issueid): """ Remember that this issue has been visited """ request = self.REQUEST key = RECENTHISTORY_ISSUEIDVISITKEY if not isinstance(issueid, basestring): # we only want objects id issueid = issueid.getId() visits = self.get_session(key, []) added_issueids = self.getRecentAddedIssues(ids=1) if issueid not in visits and issueid not in added_issueids: visits.append(issueid) if len(visits)>20: # we don't want to store too much in the session # manager so limit it. visits.reverse() visits = visits[:20] visits.reverse() self.set_session(key, visits) request.set('NotYetRecent', issueid) def hasRecentIssueVisits(self): """ check if any exists """ if self.getRecentIssueVisits(): return True else: return False def getRecentIssueVisits(self, length=None): """ Return if any exists """ request = self.REQUEST key = RECENTHISTORY_ISSUEIDVISITKEY try: issueids = self.get_session(key, []) except: issueids = [] # make them objects issues=[] issuecontainer = self._getIssueContainer() for issueid in self.filterTooRecent(issueids): try: issues.append(getattr(issuecontainer, issueid)) except: # Could have been deleted pass issues.reverse() if length: issues = issues[:length] return issues # Recent history AddedIssue # def RememberAddedIssue(self, issueid): """ Stick this in a session variable """ request = self.REQUEST key = RECENTHISTORY_ADDISSUEIDKEY if not isinstance(issueid, basestring): # we only want objects id issueid = issueid.getId() added = self.get_session(key, []) if issueid not in added: added.append(issueid) self.set_session(key, added) request.set('NotYetRecent', issueid) def hasRecentAddedIssues(self): """ check if any exists """ return bool(self.getRecentAddedIssues()) def getRecentAddedIssues(self, ids=0, length=None): """ Return if any exists """ request = self.REQUEST key = RECENTHISTORY_ADDISSUEIDKEY issueids = self.get_session(key, []) # make them objects if ids: return issueids issues=[] issuecontainer = self._getIssueContainer() for issueid in self.filterTooRecent(issueids): try: issues.append(getattr(issuecontainer, issueid)) except: # Could have been deleted pass issues.reverse() if length: return issues[:length] return issues # Combination of recent additions and recent views # def RememberRecentIssue(self, issueid, action): """ return that we've touched this issue """ assert action in ('viewed','added') key = RECENTHISTORY_ISSUESKEY issues = self.get_session(key, []) as_dict = {'issueid': issueid, 'action':action} if issueid not in [each['issueid'] for each in issues]: issues.insert(0, as_dict) if len(issues) > 25: # keep the numbers small issues = issues[:25] self.set_session(key, issues) def hasRecentIssues(self, check_each=False): """ return true if have either recent issue visits or recent issue adds """ return bool(self.getRecentIssues(check_each=check_each)) def getRecentIssues(self, length=None, check_each=True): """ return a combination of added issues and visited issues """ key = RECENTHISTORY_ISSUESKEY issues = self.get_session(key, []) if length: issues = issues[:length] if check_each: issuecontainer = self._getIssueContainer() checked = [] for recentissue in issues: if hasattr(issuecontainer, recentissue['issueid']): checked.append(recentissue) return checked else: return issues def getNiceRecentIssues(self, length=None): """ return a list of nicely formatted links to recent issues """ issues = self.getRecentIssues(length=length) issuecontainer = self._getIssueContainer() show_with_ids = self.ShowIdWithTitle() items = [] for recentissue in issues: chunks = [] issueobject = getattr(issuecontainer, recentissue['issueid'], None) if not issueobject: continue if show_with_ids: chunks.append('#%s ' % issueobject.getId()) chunks.append('' % issueobject.absolute_url_path()) chunks.append(self.displayBriefTitle(issueobject.getTitle())) if recentissue['action'] == 'added': chunks.append(' (added)') else: #chunks.append(' (viewed)') chunks.append('') items.append(''.join(chunks)) return items def hasRecentHistory(self): """ check if anything is stored """ test1 = self.hasRecentIssues(check_each=True) test2 = self.hasRecentSearchTerms() test3 = self.hasRecentReportRuns() return test1 or test2 or test3 def filterTooRecent(self, recenthistory): """ Go through list and take out something too new """ request = self.REQUEST too_recent_element = None if request.get('NewIssue') == 'Submitted' and self.meta_type == ISSUE_METATYPE: too_recent_element = self.getId() n_recenthistory = [] for each in recenthistory: if each != too_recent_element: n_recenthistory.append(each) return n_recenthistory ## Misc. methods def defineInstanceSessionKey(self, key): """ We use the default session key, but add to it for this issuetracker only. """ id = self.getRoot().getId() return '%s-%s'%(key, id) def defineInstanceCookieKey(self, key): """ We use the default cookie key, but add to it for this issuetracker only. """ # since that method is the same return self.defineInstanceSessionKey(key) ## POP3 def getPOP3Accounts(self): """ return the POP3 Account objects """ root = self.getPOP3Root(create_if_necessary=0) if root: return root.objectValues(POP3ACCOUNT_METATYPE) else: return [] def SupportPOP3SSL(self): """ return true if we're able to support POP3_SSL """ return _has_pop3_ssl security.declareProtected(VMS, 'createPOP3Account') def createPOP3Account(self, hostname, username, password, portnr=110, ssl=False, delete_after=False, REQUEST=None): """ create POP3Account object """ genid = "%s-%s"%(hostname, username) genid = genid.lower().strip() genid = Utils.safeId(genid, nospaces=1) try: portnr = int(portnr) except ValueError: raise ValueError, "Port number must be a number" root = self.getPOP3Root() if hasattr(root, genid): raise ValueError, "POP3Account already exists" pop3account = POP3Account(genid, hostname, username, password, portnr, ssl=ssl, delete_after=delete_after) root._setObject(genid, pop3account) pop3account = getattr(root, genid) if REQUEST is not None: url = self.getRootURL()+'/manage_POP3ManagementForm' url = '%s?manage_tabs_message=%s'%(url, 'POP3 Account created') response = self.REQUEST.RESPONSE response.redirect(url) else: return pop3account security.declareProtected(VMS, 'editPOP3Account') def editPOP3Account(self, id, hostname=None, portnr=None, username=None, password=None, password_dummy=None, delete_after=False, REQUEST=None): """ old method name """ import warnings m = "editPOP3Account() is an old name. Use manage_editPOP3Account() instead", warnings.warn(m, DeprecationWarning, 2) return self.manage_editPOP3Account(id, hostname, portnr, username, password, password_dummy, delete_after, REQUEST) def manage_hasFormatFlowedInstalled(self): """ return if formatflowed_decode is installed """ return _has_formatflowed_ security.declareProtected(VMS, 'manage_enableEmailRepliesSetting') def manage_enableEmailRepliesSetting(self, email, include_description_in_notifications=False, pop3accountid=None, REQUEST=None): """ set this email address to be the sitemaster_email """ assert Utils.ValidEmailAddress(email), "Not a valid email address" found_email = None for pop3account in self.getPOP3Accounts(): for ae in self.getAcceptingEmails(pop3account.getId()): if ss(ae.getEmailAddress()) == ss(email): found_email = ae.getEmailAddress() break if not found_email: if pop3accountid: pop3account = self.getPOP3Account(pop3accountid) else: pop3account = self.getPOP3Accounts()[0] self.createAcceptingEmail(pop3account.getId(), email, self.getDefaultSections(), self.getDefaultType(), self.getDefaultUrgency(), True) found_email = email self.sitemaster_email = found_email self.include_description_in_notifications = bool(include_description_in_notifications) if REQUEST is not None: # redirect back to POP3 management form url = self.getRootURL()+'/manage_POP3ManagementForm' params = {'manage_tabs_message':'Email replies made possible'} if pop3accountid: params['pop3accountid'] = pop3accountid url = Utils.AddParam2URL(url, params) response = self.REQUEST.RESPONSE response.redirect(url) security.declareProtected(VMS, 'manage_hasEmailRepliesPossible') def manage_hasEmailRepliesPossible(self): """ true if the email address of one of the accepting emails is the same as the sitemaster_email. """ sitemaster_email = ss(self.getSitemasterEmail()) for pop3account in self.getPOP3Accounts(): for acceptingemail in self.getAcceptingEmails(pop3account.getId()): if ss(acceptingemail.getEmailAddress()) == sitemaster_email: return True return False security.declareProtected(VMS, 'manage_saveBlackWhitelist') def manage_saveBlackWhitelist(self, id, acceptingemail_id, whitelist_emails, blacklist_emails, REQUEST=None): """ save blacklist and whitelists for an accepting email """ account = self.getPOP3Account(id) acceptingemail = getattr(account, acceptingemail_id) if isinstance(whitelist_emails, basestring): whitelist_emails = [whitelist_emails] if isinstance(blacklist_emails, basestring): blacklist_emails = [blacklist_emails] # clean up the lists whitelist_emails = [x.strip() for x in whitelist_emails if x.strip()] blacklist_emails = [x.strip() for x in blacklist_emails if x.strip()] acceptingemail.editDetails(whitelist_emails=whitelist_emails, blacklist_emails=blacklist_emails) if REQUEST is not None: # redirect back to POP3 management form url = self.getRootURL()+'/manage_POP3ManagementForm' params = {'pop3accountid':id, 'manage_tabs_message':"White-, blacklist saved"} url = Utils.AddParam2URL(url, params) REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'manage_editPOP3Account') def manage_editPOP3Account(self, id, hostname=None, portnr=None, username=None, password=None, password_dummy=None, delete_after=False, ssl=False, REQUEST=None): """ edit POP3 account details """ account = self.getPOP3Account(id) if hostname is not None and hostname.strip() != '': account.manage_editAccount(hostname=hostname.strip()) if portnr is not None: try: portnr = int(portnr) account.manage_editAccount(portnr=portnr) except ValueError: raise ValueError, "Port number must be a number" if username is not None and username.strip() != '': account.manage_editAccount(username=username.strip()) if password is not None and password.strip() != password_dummy: account.manage_editAccount(password=password.strip()) account.manage_editAccount(delete_after=bool(delete_after), ssl=bool(ssl)) if REQUEST is not None: # redirect back to POP3 management form url = self.getRootURL()+'/manage_POP3ManagementForm' url = '%s?pop3accountid=%s'%(url, id) url = '%s&manage_tabs_message=%s'%(url, 'POP3 Account saved') response = self.REQUEST.RESPONSE response.redirect(url) def manage_testPOP3Account(self, accountid, REQUEST=None): """ do a welcome test on this POP3 account """ account = self.getPOP3Account(accountid) if account.doSSL(): connect_class = POP3_SSL else: connect_class = POP3 try: M = connect_class(account.getHostname(), port=account.getPort()) M.user(account.getUsername()) M.pass_(account._password) result = M.welcome try: if result.find('OK') > -1: result = result.strip() + '\n(# messages: %s)' % len(M.list()[1]) except: pass M.quit() except poplib.error_proto, msg: result = msg except Exception, msg: result = str(msg) if REQUEST is not None: url = self.getRootURL() + '/manage_POP3ManagementForm' params = {'pop3accountid':accountid} params.update({'connectiontest_result':result}) url = Utils.AddParam2URL(url, params) REQUEST.RESPONSE.redirect(url) else: return result security.declareProtected('View management screens','createAcceptingEmail') def createAcceptingEmail(self, id, email_address, defaultsections=None, default_type=None, default_urgency=None, send_confirm=False, reveal_issue_url=False, REQUEST=None): """ create accepting email objet in this account """ account = self.getPOP3Account(id) if defaultsections is None: defaultsections = self.defaultsections if default_type is None: default_type = self.default_type if default_urgency is None: default_urgency = self.default_urgency email_address = email_address.strip() if not self.ValidEmailAddress(email_address): raise ValueError, "Email address is invalid %r" % email_address always_notify = ",".join(self.always_notify) always_notify = self.preParseEmailString(always_notify, aslist=1) if email_address.lower() in [x.lower() for x in always_notify]: raise ValueError, "Email %s is already used as always-notify"%\ email_address genid = email_address.replace('@','-at-').lower() a_email = account.createAcceptingEmail(genid, email_address, defaultsections, default_type, default_urgency, send_confirm, reveal_issue_url=reveal_issue_url) if REQUEST is not None: url = self.getRootURL()+'/manage_POP3ManagementForm' url = '%s?pop3accountid=%s'%(url, id) url = '%s&manage_tabs_message=%s'%(url, 'Accepting email created') response = self.REQUEST.RESPONSE response.redirect(url) else: return a_email def hasAcceptingEmails(self, id): """ return if any accepting emails """ return len(self.getAcceptingEmails(id))>0 def getAcceptingEmails(self, id): """ return accepting email objects """ if getattr(id, 'meta_type','') == POP3ACCOUNT_METATYPE: root = id else: root = self.getPOP3Account(id) return root.objectValues(ACCEPTINGEMAIL_METATYPE) def getPOP3Account(self, id): """ get an object by id """ return getattr(self.getPOP3Root(), id) security.declareProtected('View management screens','saveAcceptingEmails') def saveAcceptingEmails(self, id, allids): """ save all accepting emails. Find info via REQUEST object """ request = self.REQUEST account = self.getPOP3Account(id) for each_id in allids: acceptingemail = getattr(account, each_id) rkey_email_address = 'email_address-%s'%each_id rkey_defaultsections = 'defaultsections-%s'%each_id rkey_default_type = 'default_type-%s'%each_id rkey_defaul_urgency = 'default_urgency-%s'%each_id rkey_send_confirm = 'send_confirm-%s'%each_id rkey_reveal_issue_url = 'reveal_issue_url-%s'%each_id email_address = request.get(rkey_email_address) if not self.ValidEmailAddress(email_address): raise ValueError, "Invalid email address %s"%email_address defaultsections = request.get(rkey_defaultsections) default_type = request.get(rkey_default_type) default_urgency = request.get(rkey_defaul_urgency) send_confirm = request.get(rkey_send_confirm, False) reveal_issue_url = bool(request.get(rkey_reveal_issue_url, False)) acceptingemail.editDetails(email_address, defaultsections, default_type, default_urgency, send_confirm, reveal_issue_url=reveal_issue_url) url = self.getRootURL()+'/manage_POP3ManagementForm' url = '%s?pop3accountid=%s'%(url, id) url = '%s&manage_tabs_message=Accepting emails saved'%url response = request.RESPONSE response.redirect(url) security.declareProtected(VMS, 'manage_delPOP3Accounts') def manage_delPOP3Accounts(self, ids=[], REQUEST=None): """ delete some POP3 Accounts """ if isinstance(ids, basestring): ids = [ids] root = self.getPOP3Root() root.manage_delObjects(ids) if REQUEST is not None: if len(ids)==0: mtm = "Nothing to delete" else: mtm = "Deleted %s POP3 Accounts"%len(ids) page = self.manage_POP3ManagementForm return page(self.REQUEST, manage_tabs_message=mtm) def getPOP3Root(self, create_if_necessary=True): """ return root/pop3 folder object. Create if necessary """ root = self.getRoot() folderid = 'pop3' if create_if_necessary: if not folderid in root.objectIds('Folder'): root.manage_addFolder(folderid) return getattr(root, folderid) else: return getattr(root, folderid, None) def manage_delAcceptingEmails(self, id, ids=[], REQUEST=None): """ delete some accepting email objects """ account = self.getPOP3Account(id) if isinstance(ids, basestring): ids = [ids] account.manage_delObjects(ids) if REQUEST is not None: url = self.getRootURL()+'/manage_POP3ManagementForm' url = '%s?pop3accountid=%s'%(url, id) url = '%s&manage_tabs_message=%s accepting emails deleted'%\ (url, len(ids)) response = self.REQUEST.RESPONSE response.redirect(url) def check4MailIssues(self, verbose=False, connect_class=None): """ connect to a pop3 account and possibly create some issues. The parameter @connect_class is if you want to override what class should be instanciated to open the POP3 connection. """ if email_Parser is None: raise NotImplementedError, "The email package is not installed" # a variable where we will collect all the messages if the verbose # parameter is True v = [] # Optimization: The combos variable is created here so that it only # gets filled with useful stuff if there are any interesting # emails to deal with. As soon as there is such an email, the # fill this variable. combos = None count = 0 # total count for account in self.getPOP3Accounts(): v.append('Opening account host %s:%s' % \ (account.getHostname(),account.getPort())) if connect_class is None: if account.doSSL(): connect_class = POP3_SSL else: connect_class = POP3 try: M = connect_class(account.getHostname(), port=account.getPort()) except poplib.error_proto, msg: return "poplib.error_proto: " + str(msg) except socket_error, msg: return "socket.error: " + str(msg) M.user(account.getUsername()) v.append('Using username %r' % account.getUsername()) M.pass_(account._password) # Get messages... # sub_v = [] emails = self.getPOP3Messages(M, account, log=sub_v) v.extend(['\t%s' % x for x in sub_v]) v.append('Downloaded %s emails' % len(emails)) # ...parsed. emails = self._appendEmailIssueData(emails, account) v.append('Keep and process %s of them' % len(emails)) # Now, create the issues # for email in emails: if combos is None: # In case we only have an email address this can possibly help combos = self.getEmailFromnameCombos() if email.get('is_spam', False): v.append('\tDownloaded email is spam') elif email.get('is_blacklisted', False): v.append('\tEmail originator is blacklisted (%s)' % email['email']) elif self._processInboundEmail(email, combos=combos, log=v): v.append('\tSaved email %r' % email.get('subject', email.get('title','')[:50])) count += 1 elif verbose: v.append('\tDid not keep the email and it was not spam') if account.doDeleteAfter(): v.append('\tDelete the email') M.dele(email.get('message_number')) v.append('') M.quit() if count == 1: msg = "Created 1 issue" else: msg = "Created %s issues" % count if verbose: br = '\r\n' msg += br + br.join(v) return msg def _processInboundEmail(self, email, combos, log=[]): """ take this accepting email and upload it as an issue Write all verbose messages as strings into the list @log. """ if email.get('fromname','') == '': email['fromname'] = combos.get(email.get('email','').lower(),'') email['fromname'] = email['fromname'].replace('<','').replace('>','') email['fromname'] = email['fromname'].replace('"','').strip() else: email['fromname'] = email['fromname'].replace('"','').strip() try: # DateTime paranoia ok = DateTime(str(email['date'])) except: email['date'] = DateTime() if email.has_key('display_format'): display_format = email['display_format'] else: display_format = self.getDefaultDisplayFormat() _root_title = self.getRoot().getTitle() _root_id = self.getRoot().getId() _issueid_pattern = r'\d' * self.randomid_length def matchUrlInBody(body, url): if url.find('http://localhost') > -1: if body.find(url.replace('http://localhost', 'http://127.0.0.1')) > -1: return True elif url.find('http://127.0.0.1') > -1: if body.find(url.replace('http://127.0.0.1', 'http://localhost')) > -1: return True return body.find(url) > -1 reply_issue_id_found = None # special header for the emails _key = EMAIL_ISSUEID_HEADER if email.get(_key, email.get(_key.lower(), None)): log.append('\t%r header in email' % _key) reply_issue_id_found = email.get(_key, email.get(_key.lower())) reply_issue_id_found = reply_issue_id_found.replace('%s#' % _root_id, '') reply_issue_id_found = reply_issue_id_found.strip() if reply_issue_id_found: log.append('\t\treply issue ID found: %r' % reply_issue_id_found) try: obj = self.getIssueObject(reply_issue_id_found) log.append('\t\t\t...as object URL %s' % obj.absolute_url_path()) except AttributeError: LOG(self.__class__.__name__, ERROR, "Reply to issue %s doesn't exit" % reply_issue_id_found) reply_issue_id_found = None log.append("\t\t\t...but doesn't exist as object") if reply_issue_id_found: sub_log = [] reply_result = self._processInboundEmailReply(email, reply_issue_id_found, log=sub_log) log.extend(['\t\t%s' % x for x in sub_log]) return reply_result # is the root of the issuetracker to be found in the email body elif matchUrlInBody(email['body'], self.getRoot().absolute_url()): log.append('\tfound the url %r the body of the email' % self.getRoot().absolute_url()) if self.issueprefix: issue_url_regex = r'(http|https)://\S+/%s/(%s|%s%s)' % \ (_root_id, _issueid_pattern, self.issueprefix, _issueid_pattern) else: issue_url_regex = r'(http|https)://\S+/%s/(%s)' issue_url_regex = issue_url_regex % \ (_root_id, _issueid_pattern) # check if the email contains a URL to an issue that follows # pattern we've defined above. if re.findall(issue_url_regex, email['body']): __, reply_issue_id_found = re.findall(issue_url_regex, email['body'])[0] reply_issue_id_found = reply_issue_id_found.strip() log.append('\t\tan issue URL is found in the body of the email') else: # it could very well be that the issuetracker is pointed to by # a top domain (eg. real.issuetrackerproduct.com) so the root # issuetracker instance id won't be in the URL. If this is the # case, look for any URL that might match and check the domain # name with that used right now. issue_url_regex = issue_url_regex.replace('/%s' % _root_id, '') whole_url_regex = '(%s)' % issue_url_regex if re.findall(whole_url_regex, email['body']): whole_url, __, reply_issue_id_found = re.findall(whole_url_regex, email['body'])[0] log.append('\t\tan issue URL is found in the body of the email') if reply_issue_id_found: try: self.getIssueObject(reply_issue_id_found) except: reply_issue_id_found = None log.append('\ta reply issue ID is found %r' % reply_issue_id_found) # Is this email a reply to something this issuetracker has already # sent out. The first test checks if the body contains a URL to # an issue on this issuetracker if reply_issue_id_found: # we passed the first test, now let's dig deeper! # Perhaps the email is a reply on an email sent out from this # issuetracker before. It would then have the same signature and # at least a reference to an issue by URL. rendered_signature = self.showSignature() if email['body'].find(rendered_signature) > -1: # if we find an exact match on the signature, this email is a reply # of some sort on an email sent from this issuetracker log.append("\t\tcertain it's a reply because it has the same signature") return self._processInboundEmailReply(email, reply_issue_id_found) elif 0 < email['title'].find('%s: new issue:' % _root_title) < 6: # the subject line of the email has the "new issue:" thing # in the subject line near the begning. log.append("\t\tcertain it's a reply because the expected title") return self._processInboundEmailReply(email, reply_issue_id_found) elif 0 < email['title'].find('%s: ' % _root_title) < 6: # if the subject line starts like 'Re: : bla blab ...' # (where is self.getRoot().getTitle()) then we should # be able to find a title of an issue in the email title. if self.ShowIdWithTitle(): title_finder_regex = re.compile('%s: #(%s) (.*?)$' % (_root_title, _issueid_pattern)) _found = title_finder_regex.findall(email['title']) if _found: issueid, found_title = _found[0] # if we now can find a title in this issuetracker that # matches we know we're safe found_seq = self._searchCatalog(found_title, search_only_on=['title']) if list(found_seq): return self._processInboundEmailReply(email, reply_issue_id_found) else: title_finder_regex = re.compile('%s: (.*?)$' % _root_title) _found = title_finder_regex.findall(email['title']) if _found: # if we now can find a title in this issuetracker that # matches we know we're safe found_seq = self._searchCatalog(_found[0], search_only_on=['title']) if list(found_seq): return self._processInboundEmailReply(email, reply_issue_id_found) if email['body'].find(_("Thank you for submitting this issue via email.")) > -1: # it was one of those Thank you messages that the issue has been # added. Not overly happy about this test check. return self._processInboundEmailReply(email, reply_issue_id_found) body = unicodify(email['body'].strip()) # Before we can create this issue, we need to make a duplication # check to prevent duplicate issues with the exact same # input. title = unicodify(email['title']) if self._check4Duplicate(title, body, sections=email['sections'], type=email['type'], urgency=email['urgency'], email_message_id=email.get('message_id', None)): log.append('\tfound that the email is a duplicate of an already existing issue') return False create = self.createIssueObject issue = create(None, unicodify(email['title']), self.getStatuses()[0], email['type'], email['urgency'], email['sections'], unicodify(email['fromname']), email.get('email',''), '', 0, 0, body, display_format, email['date'], index=True, submission_type='email', email_message_id=email.get('message_id', None)) for name, file in email.get('fileattachments', {}).items(): name_id = Utils.badIdFilter(name) if name: issue.manage_addFile(name_id, file) else: m = "File attachment didn't have a name %r" % (name) LOG(self.__class__.__name__, ERROR, m) try: issue._setEmailOriginal(email['originalfile'].read()) except: logger.error("Failed to upload the original as a file", exc_info=True) # Possibly send a return email if email['acceptingemail'].doSendConfirm(): if email['fromname'] is not None and email['fromname'].strip() !='': fromname = email['fromname'] else: fromname = None # In the old days around the 0.5 version, # there used to be a standards script called # SendInboundEmailConfirm_script which would be used for # the email confirmations. Now it's not used anymore but # for the few people who are still using it, we'll stick # to it. if hasattr(self, 'SendInboundEmailConfirm_script'): script = self.SendInboundEmailConfirm_script m = "Your deployed 'SendInboundEmailConfirm_script' " m += "object is no longer necessary unless you have " m += "customized it beyond default now. Consider " m += "deleting it from the instance." parent_url = aq_parent(aq_inner(script)).absolute_url() m += "\n%s" % parent_url import warnings warnings.warn(m, DeprecationWarning) #LOG(self.__class__.__name__, WARNING, m) else: script = self.SendInboundEmailConfirm kwargs = {} if email.has_key('reveal_issue_url'): kwargs['reveal_issue_url'] = email['reveal_issue_url'] try: result = script(issue, email['email'], fromname, **kwargs) except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass typ, val, tb = sys.exc_info() _classname = self.__class__.__name__ _methodname = inspect.stack()[1][3] LOG("IssueTrackerProduct.check4MailIssues()", ERROR, 'Could not send autoreply', error=sys.exc_info()) # Notify always notifyables try: self.sendAlwaysNotify(issue, email=email.get('email', None)) except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass typ, val, tb = sys.exc_info() _classname = self.__class__.__name__ _methodname = inspect.stack()[1][3] LOG("IssueTrackerProduct.check4MailIssues()", ERROR, 'Could not send always-notify emails', error=sys.exc_info()) # if we made it all the way down to here, then the email # was added as an issue. return True def _processInboundEmailReply(self, email, issueid): """ the emaildict is a parsed email with all it's content that we can now create as a followup to the issue """ issueobject = self.getIssueObject(issueid) text = email['body'] _character_set = email.get('_character_set','us-ascii') if _has_formatflowed_: CRLF = '\r\n' text = text.replace('\n', CRLF) try: textflow = formatflowed_decode(text, character_set=_character_set) try: text, old = Utils.parseFlowFormattedResult(textflow) except AttributeError, msg: raise AttributeError, "%s (_character_set=%r)" %(msg, _character_set) except LookupError: # _character_set is quite likly 'iso-8859-1;format=flowed' _character_set = _character_set.split(';')[0].strip() textflow = formatflowed_decode(text, character_set=_character_set) try: text, old = Utils.parseFlowFormattedResult(textflow) except AttributeError, msg: raise AttributeError, "%s (_character_set=%r)" %(msg, _character_set) except UnicodeDecodeError, err: try: text = formatflowed_decode(text, character_set='latin-1') text, old = Utils.parseFlowFormattedResult(text) except UnicodeDecodeError, err: try: text = formatflowed_decode(text, character_set='utf-8') text, old = Utils.parseFlowFormattedResult(text) except UnicodeDecodeError: pass raise UnicodeDecodeError, err # raise the original error _originalmessage_regex = re.compile('''(-----\s*Original Message\s*-----\s+[From\:|Sent\:|To\:])''') # Some email clients don't use > on the replied-to lines but instead # splits the whole message with a "-----Original Message-----" # If the message can't be splitted correctly with formatflowed, do # the following: if not old.strip() and _originalmessage_regex.split(text) > 1: text = _originalmessage_regex.split(text)[0] # if the reply was parsed it's quite likely that it will contain # something like: # "On 10/19/05, Peter Bengtsson wrote:" # remove that if possible. original_email = self.sitemaster_email wrote_line_regex = r'^.*?<%s> .*?:$' % original_email for line in re.compile(wrote_line_regex, re.M).findall(text): text = text.replace(line, CRLF) # Until the IssueTracker supports storing all attributes in unicode, # do the following which is self-explanatory: if isinstance(text, unicode): text = text.encode(_character_set) else: # crude! Kill all lines that start with '> ' m = "Formatflowed is not installed. Email replies can't be parsed properly." LOG(self.__class__.__name__, WARNING, m) keeplines = [] for line in text.splitlines(): if not line.startswith('> '): keeplines.append(line) text = '\n'.join(keeplines) gentitle = _("Added Issue followup") # Before we can create this thread, we need to make a duplication- # check to prevent duplicate threads with the exact same # input. if issueobject._check4Duplicate(gentitle, text, email['fromname'], email['email'], email_message_id=email.get('message_id', None)): return False create = issueobject._createThreadObject randomid_length = self.randomid_length #if action == 'addfollowup': # gentitle = "Added Issue followup" #else: # gentitle = 'Changed status from %s to %s'%\ # (oldstatus.capitalize(), past_tense.capitalize()) prefix = self.issueprefix genid = issueobject.generateID(randomid_length, prefix+'thread', meta_type=ISSUETHREAD_METATYPE, use_stored_counter=0) if not email.has_key('display_format'): email['display_format'] = self.getDefaultDisplayFormat() thread = create(genid, gentitle, text, DateTime(), email['fromname'], email['email'], email['display_format'], submission_type='email', email_message_id=email.get('message_id', None) ) # make sure the issue object is updated now that this change has # been made issueobject._updateModifyDate() try: thread._setEmailOriginal(email['originalfile'].read()) except: LOG(self.__class__.__name__, ERROR, "Failed to upload the original as a file to a followup", error=sys.exc_info()) for name, file in email.get('fileattachments', {}).items(): name = Utils.badIdFilter(name) thread.manage_addFile(name, file) email_addresses = issueobject.Others2Notify(do='email', emailtoskip=email['email']) if email_addresses: issueobject.sendFollowupNotifications(thread, email_addresses, gentitle) # nothing else to complain about return True def SendInboundEmailConfirm(self, issueobject, emailaddress, fromname=None, reveal_issue_url=True): """ script for sending out a confirmation message back to the person who added an issue via email. Return true if the email was sent or False otherwise. """ br = "\r\n" if self.sitemaster_name: mfrom = "%s <%s>"%(self.sitemaster_name, self.sitemaster_email) else: mfrom = self.sitemaster_email subject = "%s: Your issue has been added" % self.getRoot().getTitle() msg = "Thank you for submitting this issue via email.%s%s" % (br, br) if reveal_issue_url: issueurl = issueobject.absolute_url() msg += "Your issue can be found here:%s%s" % (br, issueurl) else: issueid = issueobject.getId() msg += "Your issue id for this is: #%s" % issueid msg += br + br # Footer signature = self.showSignature() if signature: msg += "--" + br +signature if fromname is not None: mTo = "%s <%s>"%(fromname, emailaddress) else: mTo = emailaddress issueid_header = issueobject.getGlobalIssueId() self.sendEmail(msg, mTo, mfrom, subject, swallowerrors=True, headers={EMAIL_ISSUEID_HEADER: issueid_header}) def getEmailFromnameCombos(self): """ look through all issues and followups for combinations of fromname and email """ combos = {} for issue in self.getIssueObjects(): issue_email = issue.getEmail() if issue_email is None: continue if not combos.has_key(issue_email.lower()): combos[issue_email.lower()] = issue.getFromname() for thread in issue.objectValues(ISSUETHREAD_METATYPE): thread_email = thread.getEmail() if not combos.has_key(thread_email.lower()): combos[thread_email.lower()] = thread.getFromname() return combos def _appendEmailIssueData(self, emails, account): """ inspect message for certain issue data. """ allissueids = self.getIssueIds() allsections = self.getSectionOptions() allsections_ss = [ss(x) for x in allsections] alltypes = self.types allurgencies = self.urgencies reg_issueids = "|".join(allissueids) reg_sections = "|".join(allsections) reg_types = "|".join(alltypes) reg_urgencies = "|".join(allurgencies) reg_structuredtext = r'STX|structuredtext|structured-text' reg_issueids = re.compile(reg_issueids, re.I) reg_sections = re.compile(reg_sections, re.I) reg_types = re.compile(reg_types, re.I) reg_urgencies = re.compile(reg_urgencies, re.I) reg_structuredtext = re.compile(reg_structuredtext, re.I) correct_caser = self._getCorrectCase nemails = [] for email in emails: s = email.get('subject','').strip() if s == '': m = "Subject line can not be empty" self.sendReturnErrorEmail(email, m) continue subject, parsable, delimiter = self._getParsableSubject(s) parsable = [x.strip() for x in parsable.split(',')] # Sections sections = [] for eachpart in parsable[:]: if ss(eachpart) in allsections_ss: sections.append(correct_caser(eachpart, allsections)) #parsable.remove(eachpart) # Real0265 ss_remove(parsable, eachpart) if sections: email['sections'] = sections # Type types = [] for eachpart in parsable: types.extend(reg_types.findall(eachpart)) if types: email['type'] = correct_caser(types[0], alltypes) parsable.remove(types[0]) # Urgency urgencies = [] for eachpart in parsable: urgencies.extend(reg_urgencies.findall(eachpart)) if urgencies: email['urgency'] = correct_caser(urgencies[0], allurgencies) parsable.remove(urgencies[0]) # Structured or plain text # This one is a bit special. structured_text = [] for eachpart in parsable: structured_text.extend(reg_structuredtext.findall(eachpart)) if structured_text: email['display_format'] = 'structuredtext' parsable.remove(structured_text[0]) if parsable: leftovers = ', '.join(parsable) if delimiter in ['[',']']: subject = "[%s] %s"%(leftovers, subject) elif delimiter == ':': subject = "%s: %s"%(leftovers, subject) email['title'] = subject # Retrospect, and fill in with default values. # This is where we use the default* values from the # matching accepting email object acceptingemail = account.getAcceptingEmailbyTo(email['to']) email['acceptingemail'] = acceptingemail if not email.has_key('sections'): email['sections'] = acceptingemail.defaultsections if not email.has_key('type'): email['type'] = acceptingemail.default_type if not email.has_key('urgency'): email['urgency'] = acceptingemail.default_urgency email['reveal_issue_url'] = acceptingemail.revealIssueURL() extractor = self.preParseEmailString email['email'] = extractor(email['from'], allnotifyables=0) if email['email'] is None: # no valid email address was extracted continue assert isinstance(email['email'], basestring), \ "email['email'] not string (email['email']=%r, (%s))" % (email['email'], type(email['email'])) f = email['from'].replace(email['email'],'').strip() f = f.replace('<','').replace('>','').strip().replace('"','') email['fromname'] = f nemails.append(email) return nemails def sendReturnErrorEmail(self, email, msg): """ Send a simple email when there is an error """ # Check that the sitemaster_email has been set if self.sitemaster_email == DEFAULT_SITEMASTER_EMAIL: m = "Sitemaster email not changed from default. Email not sent. (%s)" m = m % self.absolute_url_path() LOG(self.meta_type, WARNING, m) return mFrom = self.sitemaster_email mTo = email['from'] mSubject = "[Autoreply] Inbound issue email incorrect" mBody = "There was an error in your inbound email to %s\n\n"%\ self.getRoot().getTitle() mBody = mBody + "Error: %s"%msg self.sendEmail(mBody, mTo, mFrom, mSubject, swallowerrors=True) def _getParsableSubject(self, subject): """ check the subject line for what is really the parsable bit and the textual bit. Return (textual, parsable, delimiter) Where delimiter is either '[' or ':'""" # default textual = subject delimiter = parsable = '' if subject[0]=='[' and subject[1:].find(']') > -1 and not \ subject[-1]==']': # Used like this - [Bug, Help] Bla bla bla parsable = subject[1:subject.find(']')].strip() textual = subject[subject.find(']')+1:].strip() delimiter = '[' elif subject.find(':')>0: # Used like this - Bug, Help: Bla bla bla parsable = subject[:subject.find(':')].strip() textual = subject[subject.find(':')+1:].strip() delimiter = ':' return textual, parsable, delimiter def _getCorrectCase(self, item, list): """ item might be 'abc' and list ['Abc','Def'] then return 'Abc' """ for correct_item in list: if item.lower()==correct_item.lower(): return correct_item else: return item def getPOP3Messages(self, pop3instance, account, dele=None, log=[]): """ get messages from pop3 object. Write all verbose messages as strings into the list @log. """ if dele is not None: import warnings m = 'getPOP3Messages() will not continue to accept the ' m += "'dele' parameter since it will no longer be able " m += "to delete emails." warnings.warn(m, DeprecationWarning) numMessages = len(pop3instance.list()[1]) log.append('0 messages in pop3 server to download') if not numMessages: return [] # no point going on emails = [] already_message_ids = self._getAllAlreadyMessageIds() basepath = os.path.join(INSTANCE_HOME, 'var') for i in range(numMessages): #emailfile = cStringIO.StringIO() emailstring = [] for line in pop3instance.retr(i+1)[1]: # XXX Hmm? Should this perhaps be # emailfile.write(line.encode('latin-1')+'\n') # instead. emailstring.append(line.rstrip()) # by stripping the lines above and then merging them with a \n # I can be certain each line is one \n apart emailstring = '\n'.join(emailstring) sub_log = [] email = self._processEmailString(emailstring, account, already_message_ids=already_message_ids, log=sub_log) log.extend(['\t%s' % x for x in sub_log]) if email: log.append('keeping message no. %s' % (i+1)) email['message_number'] = i + 1 emails.append(email) return emails def _getAllAlreadyMessageIds(self): """ return a list of message ids of emails previously processes """ already_message_ids = [x.getEmailMessageId() for x in self.getIssueObjects()] return [ss(x) for x in already_message_ids if x] def _processEmailString(self, emailstring, account, already_message_ids=None, log=[]): """ return a dictionary of the parsed email or None if the email isn't welcome and a list of verbose messages that are used by the caller of this function to display what happened here. The dictionary contains all the headers of the email plus a few extra keys: o is_spam (bool) o originalfile (file object containing the whole emailstring) o _character_set o fileattachments (dict) The @account parameter is expecting to be a POP3Account object that contains a list of accepting emails. If an email contains both a HTML part and a plaintext part the returned dictionary will have both 'body' and 'body_html' items. The parameter @already_message_ids is optional and is available as a parameter for optimization. If you're going to call _processEmailString() 10 times for 10 emails in an inbox you don't want to call _getAllAlreadyMessageIds() 10 times. Write all verbose messages as strings into the list @log. """ p = email_Parser.Parser() #emailfile.seek(0) # rewind for reading msg = p.parsestr(emailstring) # again, should that second line not be # cStringIO.StringIO(emailstring.encode('latin-1')) ?? e = {'is_spam': False, 'originalfile':cStringIO.StringIO(emailstring), '_character_set':'us-ascii', } e['fileattachments']={} charset_regex = re.compile(r'charset=["\']?([^"\']+)["\']?', re.I) if already_message_ids is None: already_message_ids = self._getAllAlreadyMessageIds() # this makes sure all the headers are written in lowercase # whitespace stripped for key, value in msg.items(): e[ss(key)] = value if ss(key) == 'content-type': if charset_regex.findall(value): e['_character_set'] = charset_regex.findall(value)[0] elif ss(key) == 'subject': if isinstance(value, str) and value.lower().find('?iso-8859') > -1: unicode_value, value_encoding = email_Header.decode_header(value)[0] if value_encoding is not None: value = unicode_value.decode(value_encoding) value = value.encode(value_encoding) else: value = unicode_value e[ss(key)] = value if not 'message_id' in e and ss(key) in ('message-id','messageid'): # This might seem stupid but it makes sure that if possible # there is a header in 'e' that is spellt exactly like # this. Not all emails might call the header 'Message-Id' e['message_id'] = value # this is a crucial check. The whole point of bothering about the # Message-ID is to prevent processing emails that have already # been uploaded. See above how we create the variable # 'already_message_ids' and now we can use that to test if this # email has already been processed if ss(value) in already_message_ids: # a ha! We have already processed this email as an issue! continue content_html = '' content_plain = '' for part in msg.walk(): if part.is_multipart(): #if part.get_content_type() == 'multipart': continue name = part.get_param('name') if name is None: name = part.get_filename() try: content = part.get_payload(decode=1) except: # This might happen if the part is too unnormal # for the email package to deal with. In that # case, this attachment is ignorable. Tough! continue if name is None: if str(part.get_content_type()).lower() in ('html', 'text/html'): content_html = content else: content_plain = content else: e['fileattachments'][name] = content if content_html and content_plain: e['body'] = content_plain e['body_html'] = content_html elif content_html: if self._isHTMLBody(content_html): if html2safehtml is not None: content_html = self._stripHTMLBody(content_html) e['display_format'] = 'plaintext' else: m = "stripogram module not installed to strip HTML emails" LOG(self.__class__.__name__, WARNING, m) e['display_format'] = 'html' e['body'] = content_html else: e['body'] = content_plain if SPAMBAYES_CHECK: # http://spambayes.sourceforge.net header = 'X-Spambayes-Classification' if e.get(header, '') == SPAMBAYES_CHECK or e.get(header.lower()) == SPAMBAYES_CHECK: # this is spam!! e['is_spam'] = True log.append('trapped as Spambayes spam!') else: log.append('passed Spambayes spam check') # Maybe it wasn't sent directly To, but CC if e.get('cc','') != '': e['to'] = "%s, %s"%(e.get('to',''), e['cc']) # check whom it's to extractor = self.preParseEmailString try: to = e['to'] except KeyError: # emails that don't have a To: part are dodgy logger.warn("One email is missing To: part (subject=%r, from=%s)" % \ (e.get('subject','*no subject*'), e.get('from','*no from*'))) log.append("unable to extract 'To:' header from email") return tolist = extractor(to, aslist=1, allnotifyables=0) tolist_simplified = [ss(x) for x in tolist] log.append('To %r' % str(tolist_simplified)) intersection = [] originator = self.preParseEmailString(e['from']) accepting_email_objects = account.getAcceptingEmails() for ae in accepting_email_objects: log.append('\tcomparing %r with %s' % (ae.getEmailAddress(), str(tolist_simplified))) if ss(ae.getEmailAddress()) in tolist_simplified: intersection.append(ae.getEmailAddress()) log.append('\t\tmatches on %s' % str(intersection)) try: if not ae.acceptOriginatorEmail(originator): log.append('\t\t\ttblacklisted from address (%r)' % originator) e['is_blacklisted'] = True except: LOG(self.__class__.__name__, WARNING, "Failed to do a white-/blacklist check on %s" % e['from'], error=sys.exc_info()) if intersection: e['to'] = intersection[0] return e del emailstring def _getIntersection(self, list1, list2): """ if 'A, C, D' in ['a','b'] should return True """ intersection = [] if list1 is None: return [] elif not isinstance(list1, list): list1 = [list1] if not isinstance(list2, list): list2 = [list2] list2lower = [x.lower().strip() for x in list2] for item in list1: if item.lower().strip() in list2lower: intersection.append(item) return intersection def _isHTMLBody(self, body): """ check if the body is html encoded """ if body is None: return False body = self._rmDoctype(body) return body.startswith('') and body.endswith('') def _rmDoctype(self, s): """ remove if s starts with """ s = s.lower().strip() if s.startswith('')+1:] return s.strip() def _stripHTMLBody(self, body): """ strip out all HTML if possible from the email """ accept_tags = ('b','strong','br','i','em','p','a', 'ol','ul','li','div') return html2safehtml(body, valid_tags=accept_tags) ## Menu def canLogout(self): """ return true if we have a method of logging this user out """ if self.get_cookie(LOGOUT_PAGE_COOKIEKEY): return True # defaulty return False def Logout(self, REQUEST): """ logout if possible via the web """ assert self.canLogout(), \ "No method for loggin out. Shut down your browser maybe" if self.has_cookie(LOGOUT_PAGE_COOKIEKEY): # This will most likely only happen if you have # logged in via a CookieCrumbler. Find it and go to # its logged_out method. url = self.get_cookie(LOGOUT_PAGE_COOKIEKEY) if url.startswith('/'): url = REQUEST.BASE0 + url elif not url.startswith('http'): url = self.getRootURL() + '/' + url return REQUEST.RESPONSE.redirect(url) # rough default return _("Logged out") security.declareProtected(VMS, 'getMenuItemsList') def getMenuItemsList(self): """ return the self.menu_items property if we have it """ return getattr(self, 'menu_items', DEFAULT_MENU_ITEMS) _getMenuItems = getMenuItemsList def _setMenuItems(self, menu_items): """ set the 'menu_items' property """ # validate if isinstance(menu_items, tuple): menu_items = list(menu_items) assert isinstance(menu_items, list), "menu_items is not a list" for item in menu_items: assert isinstance(item, dict), "%r is not a dict" % item # the dict should have three keys try: href = item['href'] assert isinstance(href, basestring), "href not a string" except KeyError: raise KeyError, "Every item must have a 'href'" try: inurl = item['inurl'] assert isinstance(inurl, (basestring, tuple, list)), \ "inurl must be string, tuple or list" except KeyError: raise KeyError, "Every item must have a 'inurl'" try: label = item['label'] assert isinstance(label, basestring), "inurl not a string" except KeyError: raise KeyError, "Every item must have a 'inurl'" # all menu items checked, save self.menu_items = menu_items def getMenuItems(self): """ return a list of three items (Title, Href, On) """ rooturl = self.getRoot().relative_url() inURL = self.thisInURL # massage the menu_items list (full of dicts) so that we turn # the 'inurl' info into a boolean based on where the user is now items = self.getMenuItemsList() menu = [] for e in items: if e['inurl'] == '': _inurl = inURL(e['inurl'], homepage=1) else: _inurl = inURL(e['inurl']) menu.append([e['label'], e['href'], _inurl, id]) issueuser = self.getIssueUser() zopeuser = self.getZopeUser() cmfuser = self.getCMFUser() if issueuser: menu.append([issueuser.getFullname(), '/User', inURL('User')]) elif cmfuser: menu.append([cmfuser.getProperty('fullname'), '/User', inURL('User')]) elif zopeuser: _name = zopeuser.getUserName() if self.getSavedUser('fullname'): _name = self.getSavedUser('fullname') menu.append([_name, '/User', inURL('User')]) else: menu.append(['Login', self.ManagerLink(1), False]) if self.has_cookie(LOGOUT_PAGE_COOKIEKEY) and (issueuser or zopeuser): # if we have this cookie, it means that we know the cookie # name of the cookie that logged the person in in the # first place. This we can use to log a user out. menu.append(['Log out', self.get_cookie(LOGOUT_PAGE_COOKIEKEY), False]) for i in range(len(menu)): href = menu[i][1] if href.startswith('/') and len(href.split('?')[0].split('/'))==2: menu[i][1] = rooturl + href return menu def displayMenuItem(self, menuinfo, underline_first_letter=None): """ proxy showing of the title through this and maybe we append a little gif with it. """ imgdata = MENUICONS_DATA # e.g. menuinfo = [title, url, on] title = show_title = menuinfo[0] if underline_first_letter and underline_first_letter.lower()==title[0].lower(): show_title = "%s%s" % (title[0], title[1:]) if self.imagesInMenu(): tmpl = '= since: status = issue.status.lower() if res.has_key(status): res[status]=res[status]+1 else: res[status] = 1 # Lastly we want to organize res by self.statuses order for status in self.getStatuses(): status = status.lower() if res.has_key(status): #sc = StatusCount(status, res[status]) tres.append([status, res[status]]) else: #sc = StatusCount(status) tres.append([status, 0]) return tres def totalCountStatus(self, statuslist): """ in a status list [['open',4], ...] sum up all the numbers """ count = 0 for item in statuslist: count += item[1] return count def CountSections(self): """ for every section, count how many for each status return as [['General', {'open':4, 'taken':6, ...}], ...] """ res = [] allsections = {} allissues = self.getIssueObjects() for issue in allissues: status = unicodify(issue.getStatus().lower()) for section in issue.getSections(): section = unicodify(section) if not allsections.has_key(section): allsections[section] = {} if not allsections[section].has_key(status): allsections[section][status] = 0 allsections[section][status] += 1 # add all zeros for section in self.getSectionOptions(): if not allsections.has_key(section): allsections[section] = {} allsections[section] = self._allStatuses(allsections[section]) res.append([section, allsections[section]]) #from pprint import pprint #pprint(res) #print "" #for count in res: # print count[0], count[1] # for status in self.getStatuses(): # print "\t", repr(status), count[1][status] # print return res def _allStatuses(self, dict): """ dict might be {'open':2, 'taken':0} then make sure it as all possible statuses """ for status in self.getStatuses(): if not dict.has_key(status.lower()): dict[status] = 0 return dict def totalCountSections(self, sectiondict): """ sum the total in {'open':2, 'taken':1, ...} """ count = 0 for value in sectiondict.values(): count += value return count def issueInflux(self, from_date=None, till_date=None, issues=None, returncount=0): """ calculate for different day periods approximately how many issues are coming in """ if from_date is not None and isinstance(from_date, basestring): from_date = DateTime(from_date) if issues is not None: allissues = issues else: allissues = self.getIssueObjects() allissues = sequence.sort(allissues, (('issuedate',),)) # if from_date is None, then make the first issue the from_date if from_date is None: from_date = allissues[0].issuedate if till_date is None: till_date = DateTime() count = 0 for issue in allissues: if issue.issuedate >= from_date and issue.issuedate < till_date: count += 1 day_span = till_date - from_date if returncount: return count else: issue_per_day = count / day_span return issue_per_day def issueInfluxbyPeriod(self, period=14): """ prepare a issues per period list """ allissues = self.getIssueObjects() allissues = sequence.sort(allissues, (('issuedate',),)) try: period = int(period) except: raise ValueError, "The period must be an integer" start_date = allissues[0].issuedate end_date = allissues[-1].issuedate difference_days = end_date - start_date influxes = [] today = DateTime() highest = 0 for i in range(int(difference_days/period)+1): from_date = start_date + i * period till_date = from_date + period if till_date > today: till_date = today data = {'from':from_date, 'till':till_date} influx = self.issueInflux(from_date, till_date, allissues, 1) data['influx'] = influx if influx > highest: highest = influx influxes.append(data) return influxes, highest def showTableRowsOfDates(self, influxes): """ return 3 TR rows of days, months, years with correct colspan """ days, months, years = [], [], [] prev_month = '' prev_year = '' month_counts = {} year_counts = {} c_m = 0 c_y = 0 for influx in influxes: day = influx['from'].strftime('%d') days.append(day) month = influx['from'].strftime('%m-%Y') if prev_month != month: months.append(month) prev_month = month if month_counts.has_key(month): month_counts[month] += 1 else: month_counts[month] = 1 year = influx['from'].strftime('%Y') if prev_year != year: years.append(year) prev_year = year if year_counts.has_key(year): year_counts[year] += 1 else: year_counts[year] = 1 _attrs = r'align="center" style="font-size:80%"' days_row = '' for day in days: days_row += '%s'%(_attrs, day) days_row += '\n' months_row = '' for month in months: _m = month.split('-')[0] if month_counts[month] > 1: months_row += '%s'%\ (month_counts[month], _attrs, _m) else: months_row += '%s'%(_attrs, _m) months_row += '\n' years_row = '' for year in years: if year_counts[year] > 1: years_row += '%s'%\ (year_counts[year], _attrs, year) else: years_row += '%s'%(_attrs, year) years_row += '\n' return days_row + months_row + years_row ## User related def getFilterlogic(self): """ not only inspect user object and cookies but also set if something new is in the REQUEST """ request = self.REQUEST key = 'Filterlogic' ok_values = ('show','block') issueuser = self.getIssueUser() if not request.has_key(key): if ss(key) in [ss(x) for x in request.keys()]: m = "If you want to set the Filterlogic parameter in REQUEST, " m += "use the correct case which is %s" % key m += "\n%s"%self.absolute_url() LOG(self.__class__.__name__, WARNING, m) for k,v in request.items(): if ss(key)==ss(k): request.set(key, v) break if ss(str(request.get(key,''))) in ok_values: # save it value = request.get(key) save_value = True if request.has_key('remember-filterlogic'): save_value = Utils.niceboolean(request.get('remember-filterlogic')) if save_value: if issueuser: issueuser.setMiscProperty(key,value) else: self.set_cookie(key, value) self.set_session(key, value, True) # faster to read from return value else: default = 'block' if issueuser: return issueuser.getMiscProperty(key, default) else: return request.get(key, self.get_session(key, self.get_cookie(key, default))) def getZopeUser(self): """ return the user object iff not Anonymous """ #user = self.REQUEST.AUTHENTICATED_USER user = getSecurityManager().getUser() uname = user.getUserName() if uname != 'Anonymous User': return user else: return None def getCMFUser(self): """ return the user object if it's got the portal_memberdata functions """ if CMF_getToolByName is None: return None try: mtool = CMF_getToolByName(self, 'portal_membership') authenticated_member = mtool.getAuthenticatedMember() assert authenticated_member.getProperty('fullname') assert authenticated_member.getProperty('email') return authenticated_member except AssertionError: debug("No 'fullname' or 'email' property") return None except AttributeError: # then an authenticated user that is not a IssueUser return None def getIssueUser(self): """ use REQUEST to get the IssueUser object or None """ user = getSecurityManager().getUser() try: user.getIssueUserPath() return user except AttributeError: # then an authenticated user that is not a IssueUser return None def getIssueUserObject(self, identifier): """ deconstruct an identifier to find the actual user object """ if not identifier: return None acl_path, username = identifier.split(',') userfolder = self.unrestrictedTraverse(acl_path) return userfolder.data[username] def isIssueUser(self): """ return True if self.getIssueUser() is not None """ return self.getIssueUser() is not None security.declareProtected('View', 'getNextActionIssuesWeb') def getNextActionIssuesWeb(self): """ this wraps the getNextActionIssues() function but prepares it a bit more for the web. """ issues, reasonsdict = self.getNextActionIssues() self.REQUEST.set('nextaction_reasons', reasonsdict) return issues security.declareProtected('View', 'getNextActionIssues') def getNextActionIssues(self, skip_sort=False): """ return a list of issues sorted by urgency that points to the current user """ zopeuser = self.getZopeUser() issueuser = self.getIssueUser() if not issueuser and not zopeuser: fromname = self.getSavedUser('fromname') email = always_email = self.getSavedUser('email') acl_user = None if issueuser: acl_user = ','.join(issueuser.getIssueUserIdentifier()) email = always_email = issueuser.getEmail() elif zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_user = path+','+name email = always_email = self.getACLCookieEmails().get(name, None) if not always_email: email = always_email = self.getSavedUser('email') always_notify_emails = self.getAlwaysNotify() always_notify_emails = self.preParseEmailString(','.join(always_notify_emails), aslist=True) # convert that string to a bool always_email = ss(always_email) in [ss(x) for x in always_notify_emails] include_statuses = [ss(x) for x in self.getStatuses()[:2]] issues = [x for x in self.getIssueObjects() \ if ss(x.getStatus()) in include_statuses] # now, look for all issues where You haven't had the last word such as # issues you've added but haven't posted the last followup, # issues assigned to your name, # issues you have taken, keep_issues = [] # we assign each issue with a score based on how it's matched highest_score = len(self.getUrgencyOptions()) #+ 1 _ASSIGNED = (_("Because it is assigned to you"), highest_score) _TAKEN = (_("Because it is taken by you"), highest_score + 1) urgency_scores = {} urgency_options = self.getUrgencyOptions() for i in range(len(urgency_options)): urgency_scores[urgency_options[i]] = i today = DateTime() for issue in issues: # check assignment assignments = issue.getAssignments() if assignments: last_ass = assignments[-1] if acl_user and last_ass.getACLAssignee() == acl_user: keep_issues.append(dict(issue=issue, reason=_ASSIGNED)) continue threads = issue.ListThreads() _taken_match = False # check if it's taken by you if ss(issue.getStatus()) == _('taken'): if not threads: if acl_user and issue.getACLAdder() == acl_user: _taken_match = True elif not acl_user and oemail and issue.getEmail() == email: _taken_match = True elif threads: # did you post a followup that changed the status? for thread in threads: if thread.getTitle().lower().endswith(_('taken')): if acl_user and thread.getACLAdder() == acl_user: _taken_match = True break elif not acl_user and email and thread.getEmail() == email: _taken_match = True break if _taken_match: keep_issues.append(dict(issue=issue, reason=_TAKEN)) continue # check if you participated but not posted the last followup if threads: _participated = False if acl_user and issue.getACLAdder() == acl_user: _participated = True elif not acl_user and email and issue.getEmail() == email: _participated = True for thread in threads[:-1]: if acl_user and thread.getACLAdder() == acl_user: _participated = True elif not acl_user and email and thread.getEmail() == email: _participated = True if not _participated: # can't have NOT had the last word continue last_thread = threads[-1] _other_match = False # it could however be that the last thread was submitted via email. # Then the acl_adder test will never match, only an email match if last_thread.getSubmissionType()=='email': if acl_user and last_thread.getEmail() != email: _other_match = True elif not acl_user and email and last_thread.getEmail() != email: _other_match = True else: if acl_user and last_thread.getACLAdder() != acl_user: _other_match = True elif not acl_user and email and last_thread.getEmail() != email: _other_match = True if _other_match: urgency_score = urgency_scores.get(issue.getUrgency(), 1) reason = (_("Because you have not had the last word"), urgency_score) keep_issues.append(dict(issue=issue, reason=reason)) continue elif always_email: # no threads # let's only do this for those issues that are relatively young if today - issue.getIssueDate() > 14: # 14 days old and they can be ignore now continue # lastly, was it not opened by you but you're one of the # always-notify people urgency_score = urgency_scores.get(issue.getUrgency(), 1) # because this is least priority...: urgency_score -= 1 reason = (_("Because you have been emailed about it"), urgency_score) keep_issues.append(dict(issue=issue, reason=reason)) if not skip_sort: def sorter(x, y): diff = cmp(x['reason'][1], y['reason'][1]) if diff == 0: return cmp(x['issue'].getIssueDate(), y['issue'].getIssueDate()) else: return diff keep_issues.sort(sorter) keep_issues.reverse() r = [] reasons_dict = {} for d in keep_issues: r.append(d['issue']) reasons_dict[d['issue'].getId()] = d['reason'][0] return r, reasons_dict def getMyIssuesAndThreads(self, sort=None, issueuser=None, include_subscriptions=False): """ Get all assigned issues and all issues that have acl_adder == issueuser or issueuser.name and issueuser.email == issue.name and issue.email """ zopeuser = self.getZopeUser() if issueuser is None: issueuser = self.getIssueUser() if not issueuser: if not zopeuser: if include_subscriptions: return [], [], [], 0, [] else: return [], [], [], 0 if issueuser: acl_user = ','.join(issueuser.getIssueUserIdentifier()) else: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_user = path+','+name assignments = [] issues = [] subscriptionissues = [] threads = [] root = self.getRoot() # prepare with what we will compare with if issueuser: user_fullname = ss(issueuser.getFullname()) user_email = ss(issueuser.getEmail()) else: user_fullname = self.getSavedUser('fromname') user_email = self.getSavedUser('email') # a dict that keeps control of thread.absolute_url and # their counted number in the order it appears threadcounts = {} # loop through all issues for issue in root.getIssueObjects(): # simplyfy fromname and email without a check from # the issue. issue_fromname = issue.getFromname(issueusercheck=0) issue_email = issue.getEmail(issueusercheck=0) if issue_fromname is None or issue_email is None: # an issue that is no longer attached to a username, # can't be matched continue fromname = ss(issue_fromname) email = ss(issue_email) # check if any of it matches if issue.getACLAdder() == acl_user: issues.append(issue) elif unicodify(fromname) == user_fullname and \ email == user_email: issues.append(issue) if include_subscriptions: _subscribers = issue.getSubscribers() if acl_user in _subscribers or user_email in _subscribers: subscriptionissues.append(issue) # loop through all assignments in this issue issue_assignments = issue.objectValues(ISSUEASSIGNMENT_METATYPE) if issue_assignments: if issue_assignments[-1].getACLAssignee() == acl_user: assignments.append(issue_assignments[-1]) # loop through all threads in this issue count = 1 for thread in issue.objectValues(ISSUETHREAD_METATYPE): # simplyfy fromname and email without a check from # the thread fromname = ss(thread.getFromname(issueusercheck=0)) email = ss(thread.getEmail(issueusercheck=0)) # check if any of it matches if thread.getACLAdder() == acl_user: threads.append(thread) threadcounts[thread.absolute_url()] = count elif unicodify(fromname) == user_fullname and \ email == user_email: threads.append(thread) threadcounts[thread.absolute_url()] = count count += 1 if sort: _sorter = self.sortSequence assignments = _sorter(assignments, (('assignmentdate',),)) assignments.reverse() issues = _sorter(issues, (('issuedate',),)) issues.reverse() threads = _sorter(threads, (('threaddate',),)) threads.reverse() subscriptionissues = _sorter(subscriptionissues, (('issuedate',),)) subscriptionissues.reverse() if include_subscriptions: return assignments, issues, threads, threadcounts, subscriptionissues else: # legacy reasons return assignments, issues, threads, threadcounts ## Access keys stuff ## security.declareProtected('View', 'enableAccessKeys') def enableAccessKeys(self, REQUEST=None): """ set a user setting for AccessKeys or cookie """ issueuser = self.getIssueUser() if issueuser: issueuser.setAccessKeys(True) else: c_key = self.getCookiekey('use_accesskeys') self.set_cookie(c_key, 1) msg = 'Keyboard shortcuts enabled' if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected('View', 'disableAccessKeys') def disableAccessKeys(self, REQUEST=None): """ set a user setting for AccessKeys or cookie """ issueuser = self.getIssueUser() if issueuser: issueuser.setAccessKeys(False) else: c_key = self.getCookiekey('use_accesskeys') self.set_cookie(c_key, 0) msg = 'Keyboard shortcuts disabled' if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg ## Remember Savedfilter Persistently stuff ## security.declareProtected('View', 'enableRememberSavedfilterPersistently') def enableRememberSavedfilterPersistently(self, REQUEST=None): """ remember that the user wants to remember filters persistently """ issueuser = self.getIssueUser() if issueuser: issueuser.setRememberSavedfilterPersistently(True) else: c_key = self.getCookiekey('remember_savedfilter_persistently') self.set_cookie(c_key, 1) msg = 'Used filter will be remembered persistently' if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected('View', 'disableRememberSavedfilterPersistently') def disableRememberSavedfilterPersistently(self, REQUEST=None): """ remember that the user wants to remember filters persistently """ issueuser = self.getIssueUser() if issueuser: issueuser.setRememberSavedfilterPersistently(False) else: c_key = self.getCookiekey('remember_savedfilter_persistently') self.set_cookie(c_key, 0) m = 'Used filters will only be remembered within the session' if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':m}) REQUEST.RESPONSE.redirect(url) else: return m ## Use 'Your next action issues' ## security.declareProtected('View', 'enableShowNextactionIssues') def enableShowNextactionIssues(self, REQUEST=None): """ remember that the user wants to show next actions on the homepage """ issueuser = self.getIssueUser() if issueuser: issueuser.setUseNextActionIssues(True) else: c_key = self.getCookiekey('show_nextactions') self.set_cookie(c_key, 1) msg = "'Your next actions issues' shown on home page" if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected('View', 'disableShowNextactionIssues') def disableShowNextactionIssues(self, REQUEST=None): """ remember that the user wants to show next actions on the homepage """ issueuser = self.getIssueUser() if issueuser: issueuser.setUseNextActionIssues(False) else: c_key = self.getCookiekey('show_nextactions') self.set_cookie(c_key, 0) msg = "No 'Your next actions issues' on home page" if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg ## AutoLogin stuff ## def testAutoLogin(self): """ return "" or redirect to login """ do_redirect = False if not self.get_session('tested_autologin'): # make sure we never have to do this again in the # near future self.set_session('tested_autologin',1) # check if the user has the cookie to True if self.doAutoLogin(): # proceed only if user us anonymous #a_user = self.REQUEST.AUTHENTICATED_USER a_user = getSecurityManager().getUser() user_roles = a_user.getRolesInContext(self) if 'Anonymous' in user_roles: do_redirect = True if do_redirect: loginlink = self.ManagerLink(absolute_url=True) self.REQUEST.RESPONSE.redirect(loginlink) else: return "" def showAutoLoginOption(self): """ return True if there is a point to having the auto login checkbox displayed. """ # # We might want to crawl further up the tree # to see if the view permission is switched off # there too. # if self.isViewPermissionOn(): return True else: return False def doAutoLogin(self): """ return True if this user has enabled the cookie for auto_login """ c_key = self.getCookiekey('autologin') default = 0 value = self.get_cookie(c_key, default) try: value = int(value) except ValueError: value = default return not not value security.declareProtected('View', 'enableAutoLogin') def enableAutoLogin(self, REQUEST=None): """ set a cookie for autologin """ c_key = self.getCookiekey('autologin') self.set_cookie(c_key, 1) msg = 'Auto login enabled' if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected('View', 'disableAutoLogin') def disableAutoLogin(self, REQUEST=None): """ set a cookie for autologin """ c_key = self.getCookiekey('autologin') self.set_cookie(c_key, 0) msg = 'Auto login disabled' if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected('View', 'changeUserOptions') def changeUserOptions(self, remember_savedfilter_persistently=False, autologin=False, use_accesskeys=False, show_nextactions=False, REQUEST=None): """ if you submit the form on User.zpt that asks the various questions such as use accesskeys, autologin and persistent filters this is the method it goes to. It means that we have to assume false for all those options and call the various enable* and disable* functions above. """ msgs = [] was_remember = self.rememberSavedfilterPersistently() if remember_savedfilter_persistently: m = self.enableRememberSavedfilterPersistently() if not was_remember: msgs.append(m) else: m = self.disableRememberSavedfilterPersistently() if was_remember: msgs.append(m) was_autologin = self.doAutoLogin() if autologin: m = self.enableAutoLogin() if not was_autologin: msgs.append(m) else: m = self.disableAutoLogin() if was_autologin: msgs.append(m) was_use_accesskeys = self.useAccessKeys() if use_accesskeys: m = self.enableAccessKeys() if not was_use_accesskeys: msgs.append(m) else: m = self.disableAccessKeys() if was_use_accesskeys: msgs.append(m) was_show_nextactions = self.showNextActionIssues() if show_nextactions: m = self.enableShowNextactionIssues() if not was_show_nextactions: msgs.append(m) else: m = self.disableShowNextactionIssues() if was_show_nextactions: msgs.append(m) msg = ', '.join(msgs) if REQUEST is not None: url = self.getRootURL()+'/User' url = Utils.AddParam2URL(url, {'changemsg':msg}) REQUEST.RESPONSE.redirect(url) else: return msg security.declareProtected('View', 'UserChangeDetails') def UserChangeDetails(self, fullname, email, display_format, REQUEST=None): """ change the password of the issueuser object """ SubmitError = {} issueuser = self.getIssueUser() cmfuser = self.getCMFUser() zopeuser = self.getZopeUser() if not (issueuser or cmfuser or zopeuser): raise Unauthorized, "Not logged in" if issueuser: path = issueuser.getIssueUserIdentifier()[0] userfolder = self.unrestrictedTraverse(path) user = userfolder.data[issueuser.getUserName()] # perform some checking if not fullname.strip(): SubmitError['fullname'] = "Missing" if not email.strip(): SubmitError['email'] = "Missing" elif not Utils.ValidEmailAddress(email.strip()): SubmitError['email'] = "Invalid" if SubmitError and REQUEST is not None: REQUEST.set('change','details') return self.User(REQUEST, SubmitError=SubmitError) elif SubmitError: raise DataSubmitError, SubmitError # Go on, make the changes # 1. Change the details of the user object fullname = unicodify(fullname.strip()) email = email.strip() if issueuser: userfolder._changeUserDetails(issueuser.name, fullname, email) issueuser.setDisplayFormat(display_format) # Change all issues and followups # since when this user adds an issue or followup the fullname and # email is stored too. self._changeACLadds(issueuser or zopeuser, fullname, email) elif cmfuser and CMF_getToolByName: mtool = CMF_getToolByName(self, 'portal_membership') authenticated_member = mtool.getAuthenticatedMember() authenticated_member.setProperties(fullname=fullname) authenticated_member.setProperties(email=email) #XXX not yet self._changeACLadds(self, else: self.set_cookie(self.getCookiekey('fullname'), fullname) self.setACLCookieName(fullname) self.set_cookie(self.getCookiekey('email'), email) self.setACLCookieEmail(email) self.set_cookie(self.getCookiekey('display_format'), display_format) self.setACLCookieDisplayformat(display_format) # Leave m = "Details changed" if REQUEST is not None: url = self.getRoot().absolute_url()+'/User' url += '?changemsg=%s'%Utils.url_quote_plus(m) REQUEST.RESPONSE.redirect(url) else: return m security.declareProtected('View', 'IssueUserChangeDetails') def IssueUserChangeDetails(self, fullname, email, REQUEST=None): """ change the password of the issueuser object """ SubmitError = {} issueuser = self.getIssueUser() if not issueuser: m = "Not logged in as a user of Issue User Folder" raise UserSubmitError, m path = issueuser.getIssueUserIdentifier()[0] userfolder = self.unrestrictedTraverse(path) user = userfolder.data[issueuser.getUserName()] # perform some checking if not fullname.strip(): SubmitError['fullname'] = "Missing" if not email.strip(): SubmitError['email'] = "Missing" elif not Utils.ValidEmailAddress(email.strip()): SubmitError['email'] = "Invalid" if SubmitError and REQUEST is not None: REQUEST.set('change','details') return self.User(REQUEST, SubmitError=SubmitError) elif SubmitError: raise DataSubmitError, SubmitError # Go on, make the changes # 1. Change the details of the user object fullname = fullname.strip() email = email.strip() userfolder._changeUserDetails(issueuser.name, fullname, email) # 2. Change all issues and followups # since when this user adds an issue or followup the fullname and # email is stored too. self._changeACLadds(issueuser, fullname, email) # Leave m = "Details changed" if REQUEST is not None: url = self.getRoot().absolute_url()+'/User' url += '?changemsg=%s'%Utils.url_quote_plus(m) REQUEST.RESPONSE.redirect(url) else: return m def _changeACLadds(self, issueuser, fullname, email): """ change the fromname and email of all issues and threads that belong to this issueuser """ data = self.getMyIssuesAndThreads(sort=None, issueuser=issueuser) assignments, issues, threads, threadcounts = data for issue in issues: issue.fromname = fullname issue.email = email for thread in threads: thread.fromname = fullname thread.email = email for assignment in assignments: assignment.fromname = fullname assignment.email = email def IssueUserChangePasswordFirsttime(self, new, confirm, REQUEST, came_from=None): """ accompanying method to the 'User_must_change_password' template. The difference between this method and that of IssueUserChangePassword() is that here we don't require to match the old password and the user object must be such that he has to change password (using mustChangePassword()) """ SubmitError = {} # Check 1. Must be a IssueUser() issueuser = self.getIssueUser() if not issueuser: m = "Not logged in as a user of Issue User Folder" raise UserSubmitError, m # Check 2. Must have to change password if not issueuser.mustChangePassword(): m = "You do not *have* to change password" raise UserSubmitError, m path = issueuser.getIssueUserIdentifier()[0] userfolder = self.unrestrictedTraverse(path) # Check 3. Is the new password good enough if not new: SubmitError['new'] = "Empty" elif new != confirm: SubmitError['confirm'] = "Mismatch" else: # they might be lazy and set a new one that is # identical to the old. That is wrong. user = userfolder.data[issueuser.getUserName()] if userfolder._isPasswordEncrypted(user._getPassword()): if userfolder._encryptPassword(new) == user._getPassword(): SubmitError['new'] = "Not different from before" else: if new == user._getPassword(): SubmitError['new'] = "Not different from before" if SubmitError: page = self.User_must_change_password return page(self, REQUEST, SubmitError=SubmitError) #else: # cool, let's do it! vars = {'name':issueuser.getUserName(), 'password':new, 'confirm':confirm, 'roles':issueuser.getRoles()} ok = userfolder.manage_users(submit="Change", REQUEST=vars) # report back that this has been done issueuser._unmust_mustChangePassword() issueuser.authenticate(new, REQUEST) if came_from: url = came_from else: url = self.getRoot().absolute_url()+'/User' REQUEST.RESPONSE.redirect(url) # security.declareProtected(PERM_ACCESS_ISSUEUSER_INFORMATION, # 'IssueUserChangePassword') def IssueUserChangePassword(self, old, new, confirm, REQUEST=None): """ change the password of the issueuser object """ SubmitError = {} issueuser = self.getIssueUser() if not issueuser: m = "Not logged in as a user of Issue User Folder" raise UserSubmitError, m path = issueuser.getIssueUserIdentifier()[0] userfolder = self.unrestrictedTraverse(path) user = userfolder.data[issueuser.getUserName()] if userfolder._isPasswordEncrypted(user._getPassword()): if not userfolder._encryptPassword(old) == user._getPassword(): SubmitError['old'] = "Incorrect" else: if not old == user._getPassword(): SubmitError['old'] = "Incorrect" # Check that the new password matches the second if not new: SubmitError['new'] = "Empty" elif new != confirm: SubmitError['confirm'] = "Mismatch" # Did everything work as expected? if SubmitError and REQUEST is not None: page = self.User REQUEST.set('change', 'password') return page(REQUEST, SubmitError=SubmitError) elif SubmitError: raise DataSubmitError, SubmitError # Cool, let's move on vars = {'name':issueuser.getUserName(), 'password':new, 'confirm':confirm, 'roles':issueuser.getRoles()} ok = userfolder.manage_users(submit="Change", REQUEST=vars) issueuser._unmust_mustChangePassword() issueuser.authenticate(new, REQUEST) m = "Password changed" if REQUEST is not None: url = self.getRoot().absolute_url()+'/User' url += '?changemsg=%s'%Utils.url_quote_plus(m) REQUEST.RESPONSE.redirect(url) else: return m ## Overridden template definitions def getDraftsContainer(self): """ makes sure and returns a folder where the drafts are saved """ root = self.getRoot() folderid = DRAFTSFOLDER_ID if not folderid in root.objectIds(['Folder','BTreeFolder2']): _adder = root.manage_addFolder if self.manage_canUseBTreeFolder(): try: _adder = root.manage_addProduct['BTreeFolder2'].manage_addBTreeFolder except: pass _adder(folderid) return getattr(root, folderid) def getMyFollowupDrafts(self, skip_draft_id=None, autosaved_only=False): """ return a list of thread draft objects """ if not self.SaveDrafts(): return [] ids = self._getDraftThreadIds() container = self.getDraftsContainer() objects = [] for id in ids: if id == skip_draft_id: continue if hasattr(container, id): object = getattr(container, id) if object.meta_type == ISSUETHREAD_DRAFT_METATYPE: if not autosaved_only or object.isAutoSave(): objects.append(object) return objects def _getDraftThreadIds(self, separate=False): """ return the possible draft ids (of threads) for this user """ c_key = self.getCookiekey('draft_followup_ids') c_key = self.defineInstanceCookieKey(c_key) #ids_cookie = self.get_cookie(c_key, '') # in the code cleanup the variable 'draft_thread_id(s)' was changed to # 'draft_followup_id(s)'. For legacy reasons we here dig out if the # user has some old cookies left under that name. This legacy hack # can be removed in 2006. _legacy_c_key = '__issuetracker_draft_thread_ids' ids_cookie = self.get_cookie(c_key, self.get_cookie(_legacy_c_key, '')) ids_cookie = [x.strip() for x in ids_cookie.split('|') if x.strip()] issueuser = self.getIssueUser() ids_user = [] if issueuser: container = self.getDraftsContainer() all_draftobjects = container.objectValues(ISSUETHREAD_DRAFT_METATYPE) acl_adder = ','.join(issueuser.getIssueUserIdentifier()) for draft in all_draftobjects: if draft.getACLAdder()==acl_adder: ids_user.append(draft.getId()) if separate: return Utils.uniqify(ids_cookie), Utils.uniqify(ids_user) else: return Utils.uniqify(ids_cookie+ids_user) def getMyIssueDrafts(self, skip_draft_issue_id=None, autosaved_only=False): """ return a list of issue draft objects """ if not self.SaveDrafts(): return [] ids = self._getDraftIssueIds() if not ids: return [] container = self.getDraftsContainer() objects = [] for id in ids: if id == skip_draft_issue_id: continue if hasattr(container, id): object = getattr(container, id) if object.meta_type == ISSUE_DRAFT_METATYPE: if not autosaved_only or object.isAutoSave(): objects.append(object) return objects def getMyIssueDraftsSeparated(self): """ return a tuple of length 2 of issue drafts and autosaved issues """ drafts=[]; autosaves=[] for draft in self.getMyIssueDrafts(): if draft.isAutoSave(): autosaves.append(draft) else: drafts.append(draft) return drafts, autosaves def _getDraftIssueIds(self, separate=False): """ return the possible draft ids we have """ c_key = self.getCookiekey('draft_issue_ids') c_key = self.defineInstanceCookieKey(c_key) ids_cookie = self.get_cookie(c_key, '') ids_cookie = [x.strip() for x in ids_cookie.split('|') if x.strip()] issueuser = self.getIssueUser() ids_user = [] if issueuser: container = self.getDraftsContainer() all_draftobjects = container.objectValues(ISSUE_DRAFT_METATYPE) acl_adder = ','.join(issueuser.getIssueUserIdentifier()) for draft in all_draftobjects: if draft.getACLAdder()==acl_adder: ids_user.append(draft.getId()) if separate: return Utils.uniqify(ids_cookie), Utils.uniqify(ids_user) else: return Utils.uniqify(ids_cookie+ids_user) def _dropDraftIssue(self, id): """ remove this draft issue object if it exists """ container = self.getDraftsContainer() # remove potential client cookie ids_cookie, ids_user = self._getDraftIssueIds(separate=True) issueuser = self.getIssueUser() if id in ids_cookie: ids_cookie.remove(id) # shorten the list of ids_cookie to only contain those # where draft objects exits ids_cookie = [x for x in ids_cookie if hasattr(container, x)] all_draft_ids = '|'.join(ids_cookie) c_key = self.getCookiekey('draft_issue_ids') c_key = self.defineInstanceCookieKey(c_key) if all_draft_ids: self.set_cookie(c_key, all_draft_ids, days=14) else: self.expire_cookie(c_key) # remove draft object if hasattr(container, id): container.manage_delObjects([id]) def _dropMatchingDraftIssues(self, issue): """ delete (if any) all issue drafts that match this issue """ title = issue.getTitle() description = issue.getDescription() del_draft_ids = [] container = self.getDraftsContainer() # the requirement for matching what to delete is if a draft matches # either: # - exactly on title and description # - exactly on title, starts on description # - starts on title, exactly on description for draft in container.objectValues(ISSUE_DRAFT_METATYPE): if not draft.getTitle() or not draft.getDescription(): # odd draft! continue draft_desc = unicodify(draft.getDescription()) draft_title = unicodify(draft.getTitle()) if draft_title == title and draft_desc == description: self._dropDraftIssue(draft.getId()) elif title.startswith(draft_title) and draft_desc == description: self._dropDraftIssue(draft.getId()) elif description.startswith(draft_desc) and draft_title == title: self._dropDraftIssue(draft.getId()) def _createDraftIssue(self, id): """ create a draftissue and return it """ root = self.getDraftsContainer() inst = IssueTrackerDraftIssue(id) root._setObject(id, inst) object = root._getOb(id) return object def showExternalEditorDraftLink(self, draft_issue_id): """ return the link for the AddIssue template """ if not draft_issue_id: return "" if not _has_ExternalEditor: return "" container = self.getDraftsContainer() if not hasattr(container, draft_issue_id): return "" #draftobjects = getattr(container, draft_issue_id) url = container.absolute_url()+'/externalEdit_/'+draft_issue_id out = ''%url out += '' out += '' return out security.declareProtected('View', 'DeleteDraftIssue') def DeleteDraftIssue(self, id, return_show_drafts_simple=False, return_show_drafts=False, REQUEST=None): """ delete this id from issue user or cookies and delete the draft issue object. """ ids_cookie, ids_user = self._getDraftIssueIds(separate=True) matched = False if id in ids_cookie: matched = True ids_cookie.remove(id) # save this c_key = self.getCookiekey('draft_issue_ids') c_key = self.defineInstanceCookieKey(c_key) all_draft_ids = '|'.join(ids_cookie) self.set_cookie(c_key, all_draft_ids, days=14) issueuser = self.getIssueUser() if id in ids_user and issueuser: matched = True if matched: # mark the draft issue as obsolete container = self.getDraftsContainer() container.manage_delObjects([id]) if REQUEST is not None: self.StopCache() if Utils.niceboolean(return_show_drafts_simple): # Exceptional case where we render and return the show_drafts_simple # template again. return self.show_drafts_simple(self, self.REQUEST) elif Utils.niceboolean(return_show_drafts): # Another exceptional case where we render and return the # show_drafts template. This featurette is exploited by # the AJAX calling DeleteDraftIssue from index_html r = self.show_drafts(self, self.REQUEST) return r if REQUEST is not None: if REQUEST.get('back','').lower() == 'home': url = self.absolute_url() else: url = self.absolute_url()+'/AddIssue' REQUEST.RESPONSE.redirect(url) security.declareProtected('View', 'DeleteDraftFollowup') def DeleteDraftFollowup(self, id, return_show_drafts_simple=False, return_show_drafts=False, REQUEST=None): """ delete this id from issue user or cookies and delete the draft issue object. """ ids_cookie, ids_user = self._getDraftThreadIds(separate=True) matched = False issueID = None if id in ids_cookie: matched = True ids_cookie.remove(id) # save this c_key = self.getCookiekey('draft_thread_ids') c_key = self.defineInstanceCookieKey(c_key) all_draft_ids = '|'.join(ids_cookie) self.set_cookie(c_key, all_draft_ids, days=14) issueuser = self.getIssueUser() if id in ids_user and issueuser: matched = True if matched: # mark the draft issue as obsolete container = self.getDraftsContainer() if hasattr(container, id): draft = getattr(container, id) issueID = draft.getIssueId() container.manage_delObjects([id]) if Utils.niceboolean(return_show_drafts_simple): # Exceptional case where we render and return the show_drafts_simple # template again. return self.show_drafts_simple(self, self.REQUEST) elif Utils.niceboolean(return_show_drafts): # Another exceptional case where we render and return the # show_drafts template. This featurette is exploited by # the AJAX calling DeleteDraftIssue from index_html return self.show_drafts(self, self.REQUEST) if REQUEST is not None: if REQUEST.get('back','').lower() == 'home': url = self.absolute_url() elif issueID: url = self.absolute_url()+'/%s' % issueID else: url = self.absolute_url() REQUEST.RESPONSE.redirect(url) security.declareProtected('View', 'SaveDraftIssue') def SaveDraftIssue(self, REQUEST, draft_issue_id=None, prevent_preview=True, *args, **kw): """ basically just show AddIssue again except that we save a draft on the side. """ if prevent_preview: REQUEST.set('previewissue', False) __saver = self._saveDraftIssue if self.SaveDrafts() and \ (\ (draft_issue_id is None and self._reason2saveDraft(REQUEST)) \ or \ draft_issue_id is not None \ ): draft_issue_id = __saver(REQUEST, draft_issue_id) kw['draft_issue_id'] = draft_issue_id kw['draft_saved'] = True return self.AddIssue(REQUEST, *args, **kw) security.declareProtected('View', 'AutoSaveDraftIssue') def AutoSaveDraftIssue(self, REQUEST, draft_issue_id=None): """ called potentially by the Ajax script """ _saver = self._saveDraftIssue if self.SaveDrafts() and REQUEST.form and \ (\ (not draft_issue_id and self._reason2saveDraft(REQUEST)) \ or \ draft_issue_id \ ): draft_issue_id = _saver(REQUEST, draft_issue_id, is_autosave=True) return draft_issue_id else: return "" def _reason2saveDraft(self, request): """ no draft has been created. Inspect this 'request' see if there is reason enough to save a draft. """ enough_request_data = False for key in ('title','description'): if Utils.SimpleTextPurifier(request.get(key,'')): enough_request_data = True break if enough_request_data: # check that a draft like this doesn't exist already _finder = self._findMatchingIssueDraft draft = _finder(unicodify(request.get('title','')), unicodify(request.get('description',''))) if draft: return False return enough_request_data def _findMatchingIssueDraft(self, title, description): """ return drafts that match exactly. Return None if nothing found """ container = self.getDraftsContainer() draftobjects = container.objectValues(ISSUE_DRAFT_METATYPE) for draft in draftobjects: if unicodify(draft.title) == title and unicodify(draft.description) == description: return draft return None def _saveDraftIssue(self, REQUEST, draft_issue_id=None, is_autosave=False): """ return the id this created """ draftscontainer = self.getDraftsContainer() if draft_issue_id: if not hasattr(draftscontainer, draft_issue_id): # you're lying! draft_issue_id = None if not draft_issue_id: # need to create a draft issue object id = self.generateID(5, prefix='issue-', meta_type=ISSUE_DRAFT_METATYPE, incontainer=draftscontainer ) # create a draft issue draftissue = self._createDraftIssue(id) draft_issue_id = id else: draftissue = getattr(draftscontainer, draft_issue_id) issueuser = self.getIssueUser() acl_adder = None if issueuser: acl_adder = ','.join(issueuser.getIssueUserIdentifier()) # now, populate this draftissue with as much data as # we can find modifier = draftissue.ModifyIssue rget = REQUEST.get modifier(title=unicodify(rget('title')), description=unicodify(rget('description')), fromname=unicodify(rget('fromname')), email=rget('email'), acl_adder=acl_adder, display_format=rget('display_format', self.getSavedTextFormat()), status=rget('status'), type=rget('type'), urgency=rget('urgency'), sections=rget('sections'), url2issue=rget('url2issue'), confidential=rget('confidential'), hide_me=rget('hide_me'), is_autosave=is_autosave, Tempfolder_fileattachments=rget('Tempfolder_fileattachments'), ) # remember this issueuser = self.getIssueUser() if not issueuser: # stick this in a cookie c_key = self.getCookiekey('draft_issue_ids') c_key = self.defineInstanceCookieKey(c_key) all_draft_ids = self._getDraftIssueIds() if draft_issue_id not in all_draft_ids: all_draft_ids.append(draft_issue_id) all_draft_ids = '|'.join(all_draft_ids) self.set_cookie(c_key, all_draft_ids, days=14) # also save, the name if we didn't already have it if rget('fromname') and not self.getSavedUser('fromname', use_request=False): self.set_cookie(self.getCookiekey('name'), rget('fromname')) if rget('email') and not self.getSavedUser('email', use_request=False): self.set_cookie(self.getCookiekey('email'), rget('email')) return draft_issue_id def _getIssueDraftObject(self, id): """ return the object from the id """ container = self.getDraftsContainer() return getattr(container, id, None) def getWhoYouAre(self, issueuser=None): """ return the issueuser identifier or '' for this current issueuser """ if issueuser is None: return "" else: return issueuser.getIssueUserIdentifierstring() def Cancel(self, REQUEST, *args, **kw): """ Button pressable when in form_followup. """ return REQUEST.RESPONSE.redirect(self.absolute_url()) def AddIssue(self, REQUEST, *args, **kw): """ Override this template so we can upload temp file attachments when needing to """ try: self._uploadTempFiles() except NotAFileError: REQUEST.set('previewissue', None) m = _("Filename entered but no actual file content") if kw.has_key('SubmitError'): kw['SubmitError']['fileattachment'] = m else: kw['SubmitError'] = {'fileattachment':m} if REQUEST.get('previewissue') and self.SaveDrafts(): draft_issue_id = REQUEST.get('draft_issue_id') draft_issue_id = self._saveDraftIssue(REQUEST, draft_issue_id) if draft_issue_id: REQUEST.set('draft_issue_id', draft_issue_id) kw['draft_saved'] = True elif REQUEST.get('draft_issue_id') and self.SaveDrafts(): object = self._getIssueDraftObject(REQUEST.get('draft_issue_id')) if object: object.populateREQUEST(REQUEST) return self.AddIssueTemplate(self, REQUEST, **kw) def getPreviewSections(self): """ Return a string of suitable sections. Helper for when you preview the issue. """ newsection = None rget = self.REQUEST.get if self.CanAddNewSections() and rget('newsection'): if rget('newsection') != 'New section...': newsection = rget('newsection') sections = rget('sections', []) if newsection: sections.insert(0, newsection) sections = Utils.uniqify(sections) return ', '.join(sections) def cleanSectionsList(self, sections): """ return the list of sections as unicode strings if they're not """ for i, item in enumerate(sections): if isinstance(item, str): try: sections[i] = unicodify(item) except TypeError: logger.error("Tried to convert %r to unicode in %r" %(item, sections)) raise return sections def QuickAddIssue(self, REQUEST, **kw): """ override this template if we need to do anything special before we show the template """ return self.QuickAddIssueTemplate(self, REQUEST, **kw) def AddManyIssues(self, REQUEST, **kw): """ override this template if we need to do anything special before we show the template """ return self.AddManyIssuesTemplate(self, REQUEST, **kw) def _getListsToExpand(self): """ the user is either a Zope ACL user or a IssueTracker User. Inspect their data and cookies for information about which lists to expand on the User page. """ issueuser = self.getIssueUser() zopeuser = self.getZopeUser() all_possible = POSSIBLE_USER_LISTS if issueuser: lists = issueuser.getUserLists() if lists is None: return all_possible else: return lists #elif zopeuser: # Need to rely on cookies :( if self.REQUEST.get('_user_lists_request'): return self.REQUEST.get('_user_lists_request') elif self.has_cookie('_user_lists'): stringlist = self.get_cookie('_user_lists') return stringlist.split(',') else: self.set_cookie('_user_lists', ','.join(all_possible)) return all_possible #else: # # something's gone wrong # return [] def _setListsToExpand(self, newlist): """ save it to user or zope user (cookie) """ issueuser = self.getIssueUser() zopeuser = self.getZopeUser() if issueuser: issueuser.setUserLists(newlist) self.set_cookie('_user_lists', ','.join(newlist)) self.REQUEST.set('_user_lists_request', newlist) def _changeListsToExpand(self, hide=[], add=[]): """ change the user list the user has """ before = self._getListsToExpand() all_possible = POSSIBLE_USER_LISTS if not isinstance(hide, list): hide = [hide] for each in hide: if each in before: before.remove(each) if not isinstance(add, list): add = [add] for each in add: if each in all_possible: before.append(each) self._setListsToExpand(Utils.uniqify(before)) def User(self, REQUEST, **kw): """ Override this template and pass also the myissues and mythreads from getMyIssuesAndThreads() """ # 1. Make sure we're logged in if self.getZopeUser() is None and self.getIssueUser() is None: REQUEST.RESPONSE.redirect(self.ManagerLink(absolute_url=True)) return # 2. Potentially modify user_lists if REQUEST.get('hide'): self._changeListsToExpand(hide=REQUEST.get('hide')) elif REQUEST.get('expand'): self._changeListsToExpand(add=REQUEST.get('expand')) # 3. Get the assignments, issues and threads data = self.getMyIssuesAndThreads(sort=True, include_subscriptions=1) myassignments, myissues, mythreads, threadcounts, mysubscriptions = data kw['myassignments'] = myassignments kw['myissues'] = myissues kw['mythreads'] = mythreads kw['threadcounts'] = threadcounts kw['mysubscriptions'] = mysubscriptions kw['user_lists'] = self._getListsToExpand() # Since we might be using CheckoutableTemplates and macro # templates are very special we are forced to do the following # magic to get the macro 'standard' from a potentially checked # out StandardHeader zodb_id = 'User.zpt' template = getattr(self, zodb_id, self.UserTemplate) return apply(template, (self, REQUEST), kw) def getMyIssues(self, i): """ return a sequence of issue objects that belong to this user. """ if ss(i) == 'assigned': data = self.getMyIssuesAndThreads() myassignments = data[0] issues = [] for assignment in myassignments: if assignment.aq_parent not in issues: issues.append(assignment.aq_parent) elif ss(i) == 'added': data = self.getMyIssuesAndThreads() #myassignments, myissues, mythreads, threadcounts = data issues = data[1] elif ss(i) == 'followedup': data = self.getMyIssuesAndThreads() mythreads = data[2] issues = [] for thread in mythreads: if thread.aq_parent not in issues: issues.append(thread.aq_parent) elif ss(i) == 'subscribed': data = self.getMyIssuesAndThreads(include_subscriptions=1) #myassignments, myissues, mythreads, threadcounts, subscriptions = data issues = data[4] return issues def getUserAchievements(self): """ return a dict of dicts which (at the deepest level) tells how many issues you have opened and closed within each level of timeperiod. The dict should then look like this: {'today': {'opened':2, 'closed':3}, 'week': {'opened':4, 'closed':9}, 'last_week': {'opened':12, 'closed':3}, 'month': {'opened':23, 'closed':18}, 'last_month': {'opened':33, 'closed':8}, 'ever': {'opened':79, 'closed':49}, } For each key in the dict, don't include it if the value is {'opened':0, 'closed':0} """ statuses_closed = self.getStatuses()[-2:] bucket = {} today = DateTime() yesterday = today - 1 last_week = DateTime()-7 yyyy = int(today.strftime('%Y')) mm = int(today.strftime('%m')) last_month = mm -1 if last_month < 1: last_month = 12 yyyy -= 1 today_date = today.strftime('%Y%m%d') yesterday_date = today.strftime('%Y%m%d') this_week_date = today.strftime('%U%Y') last_week_date = last_week.strftime('%U%Y') this_month_date = today.strftime('%Y%m') last_month_date = '%s%s' % (yyyy, last_month) zopeuser = self.getZopeUser() issueuser = self.getIssueUser() acl_user = None if issueuser: acl_user = ','.join(issueuser.getIssueUserIdentifier()) fromname = ss(issueuser.getFullname()) email = ss(issueuser.getEmail()) else: if zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_user = path+','+name fromname = ss(self.getSavedUser('fromname')) email = ss(self.getSavedUser('email')) if not fromname and not email: return [] # loop through all the issues and slot into the buckets for issue in self.getIssueObjects(): # Start by assuming that this issue wasn't opened by you opened = False issue_fromname = issue.getFromname(issueusercheck=0) issue_email = issue.getEmail(issueusercheck=0) if issue_fromname is None: issue_fromname = '' if issue_email is None: issue_email = '' issue_fromname = ss(issue_fromname) issue_email = ss(issue_email) if issue.getACLAdder() == acl_user: opened = True elif unicodify(issue_fromname) == fromname and \ issue_email == email: opened = True if opened: date = issue.getIssueDate() if date.strftime('%Y%m%d') == today_date: self._add2bucket(bucket, 'today', opened=1) elif date.strftime('%Y%m%d') == yesterday_date: self._add2bucket(bucket, 'yesterday', opened=1) if date.strftime('%U%Y') == this_week_date: self._add2bucket(bucket, 'week', opened=1) elif date.strftime('%U%Y') == last_week_date: self._add2bucket(bucket, 'last_week', opened=1) if date.strftime('%Y%m') == this_month_date: self._add2bucket(bucket, 'month', opened=1) elif date.strftime('%Y%m') == last_month_date: self._add2bucket(bucket, 'last_month', opened=1) self._add2bucket(bucket, 'ever', opened=1) if issue.getStatus().lower() in statuses_closed: # yeah, find out which thread was the closing one expect_title_start = 'Changed status ' expect_title_end = 'to %s' % issue.getStatus().lower() # now, check if YOU closed it (status -> Completed or Rejected) for thread in issue.getThreadObjects(): t = thread.getTitle().lower() if t.endswith(expect_title_end.lower()) and \ t.startswith(expect_title_start.lower()): # It was closed, but by you? thread_fromname = thread.getFromname(issueusercheck=0) thread_email = thread.getEmail(issueusercheck=0) if thread_fromname is None: thread_fromname = '' if thread_email is None: thread_email = '' thread_fromname = ss(thread_fromname) thread_email = ss(thread_email) closed = False if thread.getACLAdder() == acl_user: closed = True elif thread_fromname and thread_email: if thread_fromname == fromname and thread_email == email: closed = True elif thread_fromname and not thread_email: if thread_fromname == fromname: closed = True elif not thread_fromname and thread_email: if thread_email == email: closed = True if not closed: break # Wow! You closed this issue date = thread.getThreadDate() if date.strftime('%Y%m%d') == today_date: self._add2bucket(bucket, 'today', closed=1) elif date.strftime('%Y%m%d') == yesterday_date: self._add2bucket(bucket, 'yesterday', closed=1) if date.strftime('%U%Y') == this_week_date: self._add2bucket(bucket, 'week', closed=1) elif date.strftime('%U%Y') == last_week_date: self._add2bucket(bucket, 'last_week', closed=1) if date.strftime('%Y%m') == this_month_date: self._add2bucket(bucket, 'month', closed=1) elif date.strftime('%Y%m') == last_month_date: self._add2bucket(bucket, 'last_month', closed=1) self._add2bucket(bucket, 'ever', closed=1) break return bucket def _add2bucket(self, bucket, key, opened=False, closed=False): """ read the doc commment of getUserAchievements() """ assert opened or closed value = bucket.get(key, {'opened':0, 'closed':0}) if opened: value['opened'] = value.get('opened', 0) + 1 else: value['closed'] = value.get('closed', 0) + 1 bucket[key] = value def ListMyIssues(self, REQUEST, i, Complete=0, *args, **kws): """ Return ListIssues or CompleteList but with a sequence of issues that we generate here instead.""" if ss(i) == 'assigned': data = self.getMyIssuesAndThreads() myassignments = data[0] issues = [] for assignment in myassignments: if assignment.aq_parent not in issues: issues.append(assignment.aq_parent) pagetitle = "Issue assigned to you " elif ss(i) == 'added': data = self.getMyIssuesAndThreads() #myassignments, myissues, mythreads, threadcounts = data issues = data[1] pagetitle = "Issues you have added " elif ss(i) == 'followedup': data = self.getMyIssuesAndThreads() mythreads = data[2] issues = [] for thread in mythreads: if thread.aq_parent not in issues: issues.append(thread.aq_parent) pagetitle = "Issues you have followed up on " else: raise ValueError, "No recognized action of what to list" nr_issues = len(issues) if nr_issues == 0: pagetitle += "(none)" elif nr_issues == 1: pagetitle += "(1 issue)" else: pagetitle += "(%s issues)"%nr_issues REQUEST.set('TotalNoIssues', len(issues)) try: Complete = int(Complete) except ValueError: Complete = True if Complete: page = self.CompleteList else: page = self.ListIssues issues = self._ListIssuesFiltered(issues) return page(self, REQUEST, filteredissues=issues, pagetitle=pagetitle) ## ## Reports related code ## def getReportsContainer(self): """ return the folder where all the Reports are in """ zodb_id = "Reports" root = self.getRoot() rootbase = getattr(root, 'aq_base', root) if not hasattr(rootbase, zodb_id): inst = ReportsContainer(zodb_id) root._setObject(zodb_id, inst) return getattr(root, zodb_id) ## ## Error helping functions ## # ignored_exceptions = e_log.getProperties().get('ignored_exceptions', []) def createErrorFileObject(self, options): """ create a Zope File object called error-[date].log """ err_type = options.get('error_type') err_message = options.get('error_message') err_tb = options.get('error_tb') err_value = options.get('error_value') err_traceback = options.get('error_traceback') err_log_url = options.get('error_log_url') # stop this madness if we can find a reason for ignoring the error try: e_log = self.error_log ignorables = e_log.getProperties().get('ignored_exceptions', []) if err_type in ignorables: return None except: # carry on then pass file = cStringIO.StringIO() file.write("Bug Reporting File\n%s\n\n" % DateTime()) file.write("Error type: %s\n" % err_type) file.write("Error value: %r\n\n" % err_value) error_log = self.error_log try: security_user = getSecurityManager().getUser() def _check_permission(perm, object, user=security_user): return user.has_permission(perm, object) except: def _check_permission(*a, **k): return False LOG("standard_error_message", ERROR, "_check_permission() function disabled", error=sys.exc_info()) try: if _check_permission(VMS, error_log): entries = error_log.getLogEntries() last_entry = entries[0] file.write(error_log.getLogEntryAsText(id=last_entry.get('id'))) file.write("\n\n") except: LOG("standard_error_message", ERROR, "Could not get the last traceback", error=sys.exc_info()) version = self.getIssueTrackerVersion() file.write("IssueTrackerProduct version: %s\n"%version) if _check_permission(VMS, self.Control_Panel): cp = self.Control_Panel try: file.write("Zope: %s\n"%cp.version_txt()) except: pass try: file.write("Python: %s\n"%cp.sys_version()) except: pass try: file.write("Platform: %s\n"%cp.sys_platform()) except: pass temp_folder_id = self._generateTempFolder() temp_folder = self._getTempFolder()[temp_folder_id] fileid = DateTime().strftime('Error-%d%B%Y.log') try: temp_folder.manage_addFile(fileid, file=file, content_type='text/plain') except: LOG("standard_error_message", ERROR, "Could not create error file object", error=sys.exc_info()) return None fileobject = getattr(temp_folder, fileid) # necessary to be able to keep the file persistently # when in an error. if transaction is None: get_transaction().commit() else: # the modern way of doing it transaction.get().commit() return fileobject def ignoreExceptionType(self, error_type): """ return true if this type of exception can be ignored """ ignored_exceptions = self.error_log.getProperties().get('ignored_exceptions', []) return error_type in ignored_exceptions def bugreportingURL(self, error_type=None, error_value=None, error_traceback=None): """ return a quoted url for reporting bugs """ url, params = self._getBugReportingParameters(error_type=error_type, error_value=error_value, error_traceback=error_traceback) return Utils.AddParam2URL(url, params, unicode_encoding=UNICODE_ENCODING) def bugreportingForm(self, error_type=None, error_value=None, error_traceback=None, submit_value='Issue Tracker'): url, params = self._getBugReportingParameters(error_type=error_type, error_value=error_value, error_traceback=error_traceback) html = ['
' % url] for k, v in params.items(): html.append(u'' % (k, Utils.html_quote(v))) html.append(u'' % submit_value) html.append('
') return '\n'.join(html) def _getBugReportingParameters(self, error_type=None, error_value=None, error_traceback=None): url = "http://real.issuetrackerproduct.com/AddIssue" params = {'type':'bug report'} this_name = self.getSavedUser('fromname') if this_name: params['fromname'] = this_name this_email = self.getSavedUser('email') if this_email: params['email'] = this_email display_format = self.getSavedTextFormat() if display_format: params['display_format'] = display_format text = u"An error occured when I tried to...\n\n" text += u"\n"+"-"*50+"\n" if error_type: text += u"Error type: %s\n"%error_type if error_value: text += u"Error value: %s\n" % unicodify(error_value) if error_traceback: try: security_user = getSecurityManager().getUser() def _check_permission(perm, object, user=security_user): return user.has_permission(perm, object) except: def _check_permission(*a, **k): return False logger.error("_check_permission() function disabled", exc_info=True) try: error_log = self.error_log if _check_permission(VMS, error_log): entries = error_log.getLogEntries() last_entry = entries[0] error_traceback = error_log.getLogEntryAsText(id=last_entry.get('id')) except: LOG("bugreportingURL()", ERROR, "Could not get the last traceback", error=sys.exc_info()) text += "\n%s"%error_traceback params['description'] = text return url, params def guessPages(self, url=None, howmany=10): """ return [[URL,Title], ...] alternatives if any. This is used on the Page Not Found error page.""" if url is None: url = self.REQUEST.URL root = self.getRoot() rooturl = root.absolute_url() assert url.lower().startswith(rooturl.lower()) guesses = [] # traversable path = url.replace(rooturl, '') if self._isUsingBTreeFolder(): _issue = self.restrictedTraverse(BTREEFOLDER2_ID+path, None) if _issue and _issue.meta_type == ISSUE_METATYPE: _issue_url = _issue.absolute_url() if self.REQUEST.QUERY_STRING: _issue_url += "?%s"%self.REQUEST.QUERY_STRING self.REQUEST.RESPONSE.redirect(_issue_url, lock=1) return [[_issue.absolute_url(), _issue.getTitle()]] elif path.find(BTREEFOLDER2_ID) > -1: try: fixedpath = self.REQUEST.PATH_INFO.replace('/%s'%BTREEFOLDER2_ID,'') except: fixedpath = path.replace('/%s'%BTREEFOLDER2_ID,'') _issue = self.restrictedTraverse(fixedpath, None) if _issue and _issue.meta_type == ISSUE_METATYPE: _issue_url = _issue.absolute_url() if self.REQUEST.QUERY_STRING: _issue_url += "?%s"%self.REQUEST.QUERY_STRING self.REQUEST.RESPONSE.redirect(_issue_url, lock=1) return [[_issue.absolute_url(), _issue.getTitle()]] case_corrections = ('check4MailIssues','About.html') for case in case_corrections: if path.lower().endswith(case.lower()) and not path.endswith(case): # case insensitive method for this one _url = rooturl+'/'+case self.REQUEST.RESPONSE.redirect(_url, lock=1) return [[_url,_url]] unpadded_zeros_regex = re.compile(r'/(\d\d+)$') if unpadded_zeros_regex.findall(url): # the user most likely use /issuetracker/177 # when she was supposed to use /issuetracker/0177 digits = unpadded_zeros_regex.findall(url)[0] if len(digits) < self.randomid_length: issueid = string.zfill(digits, self.randomid_length) if self.hasIssue(issueid): _issue = self.getIssueObject(issueid) self.REQUEST.RESPONSE.redirect(_issue.absolute_url(), lock=1) return [[_issue.absolute_url(), _issue.getTitle()]] elif url.find('/user') > -1: # It's spelled 'User' not 'user' url = url.replace('/user','/User') return self.REQUEST.RESPONSE.redirect(url, lock=1) typicals = {'/AddIssue':'Add Issue', '/QuickAddIssue':'Quick Add Issue', '/ListIssues':'List Issues', '/CompleteList':'Complete List', } for k, v in typicals.items(): if path.lower()==k.lower() and path != k: return [[rooturl+k,v]] if url.lower().endswith('management'): guesses.append([rooturl+'/manage_ManagementForm', 'Management']) elif url.lower().endswith('properties'): guesses.append([rooturl+'/manage_editIssueTrackerPropertiesForm', 'Properties (Zope)']) id_with_junk = re.compile('/(' + '\d'*self.randomid_length + ')\w+') if id_with_junk.findall(path): issueid = id_with_junk.findall(path)[0] # does it exit? for objectid, object in root.getIssueItems(): if objectid == issueid: title = object.getTitle() objecturl = object.absolute_url() guesses.append([objecturl, title]) break guesses.append([rooturl,'Home page']) return guesses ## ## Status scores related ## def getStatusScoreValues(self, return_incomplete=False): """ return a dict where the keys are from getStatus() and the values are integers (or None) from 0-100. """ status_values = getattr(self, '_status_score_values', {}) assert type(status_values) == type({}) if return_incomplete: # don't do a validity check on it return status_values # perform a validity check... if Set is not None: # ...using sets # use sets to check that status_keys = self.getStatuses() if not Set(status_keys) == Set(status_values.keys()): return None else: # ...using slow loops for status_key in status_keys: if status_key not in status_values.keys(): return None for key in status_values.keys(): if key not in status_keys: return None return status_values def hasStatusValues(self, values=None): """ check if the status values are sufficiently set """ if values is None: values = self.getStatusScoreValues() if not values: # values is an empty dict return False else: # must have a summable values try: Utils.sum(values.values()) return True except: return False def manage_saveStatusScores(self, used_statuses, values, REQUEST=None): """ used_statuses is a list of statuses that was used to set values on each status. """ assert len(used_statuses) == len(values) status_values = self.getStatusScoreValues(return_incomplete=True) for i in range(len(used_statuses)): status = used_statuses[i] value = values[i] if value == '': value = None else: value = int(value) assert value >= 0 and value <= 100, "Invalid value for score on status %s" % status status_values[status] = value # save this self._status_score_values = status_values if REQUEST is not None: url = self.getRootURL()+'/manage_PropertiesStatusScores' url += '?manage_tabs_message=Status+scores+saved' REQUEST.RESPONSE.redirect(url) def calculateStatusScoreProgress(self, status_values): """ return a calculated average score as an integer between 1-100 """ statuslist = self.CountStatuses() statuslist_count = self.totalCountStatus(statuslist) statuslist_dict = {} for status, count in statuslist: statuslist_dict[status] = count # status_values is a dict where each key is a status. # The calculation is the sum of count*score divided by # the sum of all counts. See the source code _statuscount_times_values = [status_values[x] * y for (x, y) in statuslist_dict.items() if status_values[x] is not None] _statuses_valued = [count for (x, count) in statuslist_dict.items() if status_values[x] is not None] return Utils.sum(_statuscount_times_values) / \ float(Utils.sum(_statuses_valued)) ## ## Upgrade related ## def _getVersionControllerInstance(self): """ return an instance of the upgrade.VersionController class """ here = package_home(globals()) assert here.endswith('IssueTrackerProduct') return VersionController(here) security.declareProtected(VMS, 'manage_canUpgrade') def manage_canUpgrade(self): """ return true or false if the issuetracker can be upgraded """ vc = self._getVersionControllerInstance() if vc.isUsingCVS(): ## currently we do can't support this return False else: return vc.canUpgrade() security.declareProtected(VMS, 'manage_getUpgradeInfo') def manage_getUpgradeInfo(self): """ return which version we can upgrade to """ vc = self._getVersionControllerInstance() return {'version':vc.latest_version, 'url':vc.latest_version_url} security.declareProtected(VMS, 'manage_isUsingCVS') def manage_isUsingCVS(self): """ return true or false on whether we're using CVS for this installation. """ vc = self._getVersionControllerInstance() return vc.isUsingCVS() security.declareProtected(VMS, 'manage_doUpgrade') def manage_doUpgrade(self, REQUEST=None): """ perform a IssueTrackerProduct using the upgrade script """ assert self.manage_canUpgrade() output = cStringIO.StringIO() errors = cStringIO.StringIO() old_stdout = sys.stdout old_stderr = sys.stderr try: sys.stdout = output sys.stderr = errors vc = self._getVersionControllerInstance() vc.upgrade() finally: sys.stdout = old_stdout sys.stderr = sys.stderr errors_value = errors.getvalue() output_value = output.getvalue() msg = output_value if errors_value: msg += "\n%s" % errors_value # Note: we create this URL here _before_ we call _refreshIssueTrackerProduct() # because after that function has been called, the whole product goes into # asyncrounous refreshingstate meaning that all modules become None (dont' # ask me to explain it). All code below the _refreshIssueTrackerProduct() does # not use any of the IssueTrackerProduct modules and should thus be safe. management_url = self.getRootURL()+'/manage_ManagementUpgrade' try: self._refreshIssueTrackerProduct() except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Could not perform product refresh", error=sys.exc_info()) msg += "\n**COULD NOT PERFORM PRODUCT REFRESH. See error_log**" msg = msg.strip() del output, errors if REQUEST is not None: #url = Utils.AddParam2URL(management_url, {'manage_tabs_message':msg}) from urllib import quote url = management_url + '?manage_tabs_message=%s' % quote(msg) #REQUEST.RESPONSE.redirect(url) return '''

Refreshing...

Please wait while the IssueTrackerProduct is being refreshed

''' % {'url':url} else: return msg def _refreshIssueTrackerProduct(self): """ perform a refresh of the IssueTrackerProduct """ itp = self.Control_Panel.Products.IssueTrackerProduct itp.manage_performRefresh() def _emptyFunction(self, REQUEST, RESPONSE): """ fake empty function """ return REQUEST ## ## Spam protection stuff ## def getCaptchaNumbersHTML(self, keys=None, howmany=4): """ return the HTML needed to be included in the forms to catch out spambots. """ ckey = ALREADY_NOT_SPAMBOT_COOKIE_KEY if self.get_cookie(ckey): return '' parts = [] if keys: for key in keys: src = key parts.append('number?' % src) parts.append('' % src) else: keys = self.captcha_numbers_map.keys() random.shuffle(keys) for i in range(howmany): src = keys[i % len(keys)] parts.append('number?' % src) parts.append('' % src) return ''.join(parts) def containsSpamKeywords(self, text, verbose=False): """ find any spam keywords in the text if possible. """ keywords = self.getSpamKeywords() listtest = lambda x: isinstance(x, list) text = text.lower() def exit(*words): if verbose: if len(words) > 1: msg = "Matched spam keywords: %s" % ', '.join(words) else: msg = "Matched spam keyword: %s" % words[0] LOG("IssueTrackerProduct Spam Protection", INFO, msg) # return True means that Yes, there are spam keywords in text return True def testmatch(keyword, text): """ if the keyword we're looking for is something like 'poker' that we'll do a word delimiter around the keyword for the match. If it contains anything else, we do a regular string find match. """ if re.findall('[^\w]', keyword): # this keyword contains other stuff than just A-z return text.lower().find(keyword.lower()) > -1 else: regex = re.compile(r'\b%s\b' % re.escape(keyword), re.I) return regex.findall(text) sub_keywords = {} single_keywords = [] for i, keyword in enumerate(keywords): is_part = False try: next_keyword = keywords[i+1] if listtest(next_keyword): sub_keywords[keyword] = next_keyword elif not listtest(keyword): single_keywords.append(keyword) except IndexError: if not listtest(keyword): single_keywords.append(keyword) for keyword in single_keywords: if testmatch(keyword, text): return exit(keyword) for keyword, keywords in sub_keywords.items(): if testmatch(keyword, text): for keyword_ in keywords: if testmatch(keyword_, text): return exit(keyword, keyword_) return False security.declareProtected(VMS, 'manage_saveSpamKeywords') def manage_saveSpamKeywords(self, keywords, REQUEST=None): """ save the 'spam_keywords' """ checked = [] # remove blank lines keywords = [x for x in keywords if x.strip()] subwordtest = lambda x: x.startswith(' ') or x.startswith('\t') subwords = None for word in keywords: if subwordtest(word): if checked: if subwords is None: subwords = [word.rstrip()] else: subwords.append(word.rstrip()) else: if subwords: subwords = [x.strip() for x in subwords] subwords.sort() checked.append(Utils.iuniqify(subwords)) subwords = None checked.append(word.strip()) if subwords: subwords = [x.strip() for x in subwords] subwords.sort() checked.append(Utils.iuniqify(subwords)) def merge_duplicates(list_of_lists): """ suppose you have a list like this: ['foo', 'Key1', ['a','b','c'], 'bar', 'Key1', ['d','e','f'] 'foobar', ... That means that the values of the two Key1 can be merged into one: ['foo', 'Key1', ['a','b','c','d','e','f'], 'bar', 'foobar', ... """ all = {} listtest = lambda x: isinstance(x, list) skip_next = False for i, item in enumerate(list_of_lists): if skip_next: skip_next = False continue try: next_item = list_of_lists[i+1] if listtest(next_item): p = all.get(item, []) p.extend(next_item) all[item] = p skip_next = True else: all[item] = [] except IndexError: # we're at the last item all[item] = [] _keys = all.keys() _keys.sort(lambda x,y:cmp(x.lower(), y.lower())) new_list_of_lists = [] for k in _keys: v = all[k] new_list_of_lists.append(k) if v: new_list_of_lists.append(v) return new_list_of_lists checked = merge_duplicates(checked) self.spam_keywords = checked if REQUEST is not None: url = self.getRootURL()+'/manage_ManagementSpamProtection' url += '?manage_tabs_message=Spam+keywords+saved' REQUEST.RESPONSE.redirect(url) security.declareProtected(VMS, 'manage_findIssuesContainingSpam') def manage_findIssuesContainingSpam(self): """ return all issues that contain spam """ issues = [] for issue in self.getIssueObjects(): text = ' '.join([issue.getTitle(), issue.getDescription(), issue.getFromname(), issue.getEmail()]) if self.containsSpamKeywords(text): issues.append(issue) return issues security.declareProtected(VMS, 'manage_findThreadsContainingSpam') def manage_findThreadsContainingSpam(self): """ return all threads that contain spam """ threads = [] thread_counts = {} for issue in self.getIssueObjects(): count = 1 for thread in issue.getThreadObjects(): text = ' '.join([thread.getComment(), thread.getFromname(), thread.getEmail()]) if self.containsSpamKeywords(text): thread_counts[thread.absolute_url_path()] = count threads.append(thread) count += 1 # The reason for maintaining this dict is so that on # manage_ManagementSpamProtection we can link to followups # with the correct anchor link. self.REQUEST.set('thread_counts', thread_counts) return threads security.declareProtected(VMS, 'manage_deleteIssuesAndThreads') def manage_deleteIssuesAndThreads(self, issuepaths=[], threadpaths=[], REQUEST=None): """ used on the ManagementSpamProtection page when you've found some issues with spam in it. """ rooturl = self.getRoot().absolute_url() # check each path dels = {} all_paths = issuepaths + threadpaths for path in all_paths: obj = self.restrictedTraverse(path) if obj.absolute_url().find(rooturl) == -1: raise DataSubmitError, "Invalid path to object %r" % path container = aq_parent(aq_inner(obj)) container.manage_delObjects([obj.getId()]) if REQUEST is not None: url = self.getRootURL()+'/manage_ManagementSpamProtection' if all_paths: url += '?manage_tabs_message=Issues+and+followups+deleted' else: url += '?manage_tabs_message=Nothing+deleted' REQUEST.RESPONSE.redirect(url) #---------------------------------------------------------------------------- zpts = ('zpt/StandardHeader', {'f':'zpt/QuickAddIssue', 'n':'QuickAddIssueTemplate', 'optimize':OPTIMIZE and 'xhtml'}, {'f':'zpt/AddManyIssues', 'n':'AddManyIssuesTemplate', 'optimize':False},#OPTIMIZE and 'xhtml'}, 'zpt/preview_issue', {'f':'zpt/index_html', 'optimize':OPTIMIZE and 'xhtml'}, 'zpt/list_issues_top_bar', {'f':'zpt/ListIssues', 'optimize':0}, #OPTIMIZE and 'xhtml'}, {'f':'zpt/CompleteList', 'optimize':0}, #OPTIMIZE and 'xhtml'}, 'zpt/show_submissionerror_message', {'f':'zpt/AddIssue', 'n':'AddIssueTemplate', 'optimize':OPTIMIZE and 'xhtml'}, {'f':'zpt/User', 'n':'UserTemplate', 'optimize':OPTIMIZE and 'xhtml'}, 'zpt/User_must_change_password', 'zpt/show_drafts', 'zpt/show_drafts_simple', 'zpt/show_next_actions', 'zpt/filter_options', 'zpt/richList', 'zpt/compactList', 'zpt/search_widget', 'zpt/recent_history_widget', 'zpt/Statistics', 'zpt/ShowIssueData', 'zpt/ShowIssueThreads', 'zpt/What-is-StructuredText', 'zpt/What-is-WYSIWYG', 'zpt/Keyboard-shortcuts', 'zpt/Your-next-action-issues', ('zpt/rdf', 'rdf_template'), 'zpt/show_user_achievements', 'zpt/show_outlook', ) #addTemplates2Class(IssueTracker, zpts, extension='zpt') dtmls = ({'f':'dtml/screen.css', 'optimize':OPTIMIZE and 'css'}, {'f':'dtml/print.css', 'optimize':OPTIMIZE and 'css'}, {'f':'dtml/home.css', 'optimize':OPTIMIZE and 'css'}, 'dtml/tw-sack.js', # here for legacy 'dtml/js-core.js', # here for legacy ('dtml/editIssueTrackerPropertiesForm', 'manage_editIssueTrackerPropertiesForm'), ('dtml/configureMenuForm', 'manage_configureMenuForm'), 'dtml/management_tabs', ('dtml/ManagementForm','manage_ManagementForm'), ('dtml/POP3ManagementForm', 'manage_POP3ManagementForm'), ('dtml/ManagementNotifyables','manage_ManagementNotifyables'), ('dtml/ManagementUsers','manage_ManagementUsers'), ('dtml/ManagementUpgrade','manage_ManagementUpgrade'), ('dtml/ManagementSpamProtection','manage_ManagementSpamProtection'), 'dtml/tabtastic-combined.js', {'f':'dtml/keyboardshortcuts.js', 'optimize':OPTIMIZE and 'js', }, {'f':'dtml/AddIssueJavascript', 'n':'addissue.js', 'optimize':OPTIMIZE and 'js', }, {'f':'dtml/QuickAddIssueJavascript', 'n':'quickaddissue.js', 'optimize':OPTIMIZE and 'js', }, {'f':'dtml/followup.js', 'optimize':OPTIMIZE and 'js', }, {'f':'dtml/home.js', 'optimize':OPTIMIZE and 'js', }, ('dtml/PropertiesStatusScores', 'manage_PropertiesStatusScores'), # TinyMCE stuff {'f':'dtml/tiny_mce_itp.js', 'optimize':OPTIMIZE and 'js'}, ) # Attach some tiny GIFs that are numbers. Make the Id's slightly more random # so that spambots can't work out that: # == 041 numbers_map = {} _home = package_home(globals()) _imageshome = os.path.join(_home,'www/numbers') for e in [x for x in os.listdir(_imageshome) if x.endswith('.gif')]: attribute_id = Utils.getRandomString(3)+'.gif' while numbers_map.has_key(attribute_id): attribute_id = Utils.getRandomString(4)+'.gif' setattr(IssueTracker, attribute_id, ImageFile(os.path.join(_imageshome, e))) numbers_map[attribute_id] = int(e.replace('.gif','')) setattr(IssueTracker, 'captcha_numbers_map', numbers_map) all = list(dtmls+zpts) if not DEBUG: all.append('zpt/standard_error_message') addTemplates2Class(IssueTracker, tuple(all)) setattr(IssueTracker, 'About.html', IssueTracker.About) setattr(IssueTracker, 'rss-0.91.xml', IssueTracker.RSS091) # default RSS setattr(IssueTracker, 'rss.xml', IssueTracker.RSS10) setattr(IssueTracker, 'rdf.xml', IssueTracker.RDF) # CSV link setattr(IssueTracker, 'export.csv', IssueTracker.CSVExport) # CSV link 2 setattr(IssueTracker, 'ListIssues.csv', IssueTracker.ListIssues_CSV) # Set some of the security declaration outside the class security = ClassSecurityInfo() security.declareProtected('View', 'index_html') #security.declareProtected('View', 'ShowIssue') security.declareProtected('View', 'Statistics') security.declareProtected('View', 'CompleteList') security.declareProtected('View', 'ListIssues') security.declareProtected('View', 'export.csv') security.declareProtected('View', 'ListIssues.csv') security.declareProtected('View', 'rss.xml') security.declareProtected('View', 'rdf.xml') security.declareProtected(VMS, 'manage_POP3ManagementForm') security.declareProtected(VMS, 'manage_configureMenuForm') security.declareProtected(VMS, 'manage_ManagementNotifyables') security.declareProtected(VMS, 'manage_ManagementNotifyables') security.declareProtected(VMS, 'manage_editIssueTrackerPropertiesForm') security.declareProtected(VMS, 'manage_ManagementForm') security.declareProtected(VMS, 'manage_ManagementUsers') security.declareProtected(VMS, 'manage_ManagementUpgrade') security.declareProtected(VMS, 'manage_PropertiesWizard') security.declareProtected(VMS, 'manage_PropertiesStatusScores') security.declareProtected(AddIssuesPermission, 'AddIssue') security.declareProtected(AddIssuesPermission, 'QuickAddIssue') security.apply(IssueTracker) InitializeClass(IssueTracker) setattr(IssueTracker, 'UNICODE_ENCODING', UNICODE_ENCODING) #---------------------------------------------------------------------------- # Need to import these here otherwise from Notification import IssueTrackerNotification from Issue import IssueTrackerIssue, IssueTrackerDraftIssue from Thread import IssueTrackerIssueThread from Email import POP3Account #---------------------------------------------------------------------------- class FilterValuer(SimpleItem.SimpleItem, PropertyManager.PropertyManager, CatalogAware): """ a simple class that helps us remember a set of filter options. """ meta_type = FILTEROPTION_METATYPE _properties = ({'id':'title', 'type':'ustring', 'mode':'w'}, {'id':'adder_fromname','type':'ustring', 'mode':'w'}, {'id':'adder_email', 'type':'string', 'mode':'w'}, {'id':'acl_adder', 'type':'string', 'mode':'w'}, {'id':'key', 'type':'string', 'mode':'r'}, {'id':'mod_date', 'type':'date', 'mode':'w'}, {'id':'filterlogic', 'type':'string', 'mode': 'w'}, {'id':'statuses', 'type':'ulines', 'mode': 'w'}, {'id':'sections', 'type':'ulines', 'mode': 'w'}, {'id':'urgencies', 'type':'ulines', 'mode': 'w'}, {'id':'types', 'type':'ulines', 'mode': 'w'}, {'id':'fromname', 'type':'ustring', 'mode': 'w'}, {'id':'email', 'type':'string', 'mode': 'w'}, ) manage_options = PropertyManager.PropertyManager.manage_options def __init__(self, id, title): self.id = id self.title = title self.acl_adder = '' self.adder_fromname = u'' self.adder_email = '' self.key = '' # Used by people who don't have a name self.mod_date = DateTime() self.usage_count = 0 def getId(self): return self.id def getTitle(self, length_limit=None): title = self.title if length_limit is not None: if len(title) > length_limit: return title[:length_limit] + '...' return title def getModificationDate(self): """ return when it was last changed """ return self.mod_date def getKey(self): return getattr(self, 'key', None) def set(self, key, value): assert key in [x['id'] for x in self._properties], "Unrecognized property key" self.__dict__[key] = value def populateRequest(self, request): """ put all the filter values in this class into self.REQUEST """ rset = request.set rset('Filterlogic', self.filterlogic) rset('f-statuses', self.statuses) rset('f-sections', self.sections) rset('f-urgencies', self.urgencies) rset('f-types', self.types) rset('f-fromname', self.fromname) rset('f-email', self.email) def incrementUsageCount(self): self.usage_count = self.getUsageCount() + 1 def updateModDate(self): self.mod_date = DateTime() def getUsageCount(self): """ return how many times this has been used """ return getattr(self, 'usage_count', 0) ## ## Indexing ## def index_object(self): """A common method to allow Findables to index themselves.""" idxs = ['meta_type','acl_adder','adder_fromname','adder_email', 'key','mod_date','path','title'] path = '/'.join(self.getPhysicalPath()) catalog = self.getFilterValuerCatalog() catalog.catalog_object(self, path, idxs=idxs) def unindex_object(self): catalog = self.getFilterValuerCatalog() catalog.uncatalog_object('/'.join(self.getPhysicalPath())) #---------------------------------------------------------------------------- class ReportsContainer(ZopeOrderedFolder): """ A simple class that is more or less like the Folder class. This is the home where we put all the reports and information about them. """ meta_type = REPORTS_CONTAINER_METATYPE icon = '%s/issuereportscontainer.gif' % ICON_LOCATION security = ClassSecurityInfo() def __init__(self, id, title=''): self.id = id self.title = title def _getAllScripts(self): """ return all ReportScript objects plainly """ return self.objectValues(REPORTSCRIPT_METATYPE) def _getAllScriptIds(self): """ return all ReportScript objects plainly """ return self.objectIds(REPORTSCRIPT_METATYPE) def _getAllScriptItems(self): """ return all ReportScript objects plainly """ return self.objectItems(REPORTSCRIPT_METATYPE) def getScripts(self, sort=False, reverse=False): """ return all report scripts this user can see """ checked = [] for script in self._getAllScripts(): checked.append(script) if sort: if isinstance(sort, bool): # use default sort key sort = 'bobobase_modification_time' checked = sequence.sort(checked, ((sort,),)) if reverse: checked.reverse() return checked def script_log(self, summary, text=''): """ print the summary and text to the event log (idea taken from Plone's plone_log() """ LOG('Report Script', INFO, summary, text) def __before_publishing_traverse__(self, object, request=None): """ sort things out before publising object """ self.get_environ() def get_environ(self): """ Populate REQUEST as appropriate """ request = self.REQUEST stack = request['TraversalRequestNameStack'] # look in the stack to see if we have getId()+'.py' # and if so, replace that with Download2FS if len(stack)==1: if stack[0].endswith('.py'): script_id = stack[0][:-3] if script_id in self._getAllScriptIds(): stack = ['Download2FS', script_id] request.set('TraversalRequestNameStack', stack) zpts = ({'f':'zpt/Reports', 'n':'index_html'}, ) addTemplates2Class(ReportsContainer, zpts, extension='zpt') InitializeClass(ReportsContainer) IssueTrackerProduct/Email.py0000644000175000017500000002546611012074373016275 0ustar peterbepeterbe# IssueTrackerProduct # # Peter Bengtsson # License: ZPL # """ Email is for accepting inbound emails into the issue tracker """ # python import sys, os, re # Zope from OFS import SimpleItem from AccessControl import ClassSecurityInfo from Globals import InitializeClass # Product import IssueTracker from Permissions import * from Constants import * from Utils import ss class POP3Account(IssueTracker.IssueTrackerFolderBase): """ POP3Account class """ meta_type = POP3ACCOUNT_METATYPE icon = '%s/issuetracker_pop3account.gif'%ICON_LOCATION _properties = ({'id':'hostname', 'type': 'string', 'mode':'w'}, {'id':'portnr', 'type': 'int', 'mode':'w'}, {'id':'username', 'type': 'string', 'mode':'w'}, {'id':'delete_after', 'type': 'boolean', 'mode':'w'}, {'id':'ssl', 'type': 'boolean', 'mode':'w'}, ) # for legacy reasons delete_after = True ssl = False security = ClassSecurityInfo() # default portnr = 110 def __init__(self, id, hostname, username, password, portnr=110, delete_after=False, ssl=False): """ init """ self.id = str(id) self.hostname = str(hostname) self.portnr = int(portnr) self.username = str(username) self._password = str(password) self.delete_after = bool(delete_after) self.ssl = bool(ssl) def getTitle(self): """ return hostname for title """ return self.hostname def getId(self): """ return id """ return self.id def getHostname(self): """ return hostname """ return self.hostname def getPort(self): """ return portnr """ return getattr(self, 'portnr', 110) def getUsername(self): """ return username """ return self.username def doDeleteAfter(self): """ return delete_after """ return self.delete_after def doSSL(self): """ return ssl """ return self.ssl def getAcceptingEmails(self, ids=0): """ return accepting email objects here """ if ids: return self.objectIds(ACCEPTINGEMAIL_METATYPE) else: return self.objectValues(ACCEPTINGEMAIL_METATYPE) def doSendConfirmSuggestion(self): """ return true if most accepting emails herein use doSendConfirm """ total = 0 for ae in self.getAcceptingEmails(): if ae.doSendConfirm(): total += 1 else: total -= 1 # since default is True, be more lenient to total=0 return total >= 0 def getAcceptingEmailbyTo(self, to, default=None): """ which accepting email here has 'to' for email_address """ to = to.lower().strip() for object in self.getAcceptingEmails(): if object.email_address.lower().strip() == to: return object else: return default security.declareProtected(VMS, 'editAccount') def editAccount(self, hostname=None, portnr=None, username=None, password=None, delete_after=False): """ old name """ import warnings warnings.warn("editAccount() is an old name. Use manage_editAccount() instead", DeprecationWarning, 2) return self.manage_editAccount(hostname, portnr, username, password, delete_after) security.declareProtected(VMS, 'manage_editAccount') def manage_editAccount(self, hostname=None, portnr=None, username=None, password=None, delete_after=False, ssl=False): """ change the attributes """ if hostname is not None: self.hostname = hostname if portnr is not None: self.portnr = portnr if username is not None: self.username = username if password is not None: self._password = password self.delete_after = bool(delete_after) self.ssl = bool(ssl) security.declareProtected(VMS, 'createAcceptingEmail') def createAcceptingEmail(self, id, email_address, defaultsections, default_type, default_urgency, send_confirm, reveal_issue_url): """ create object and return it """ acceptingemail = AcceptingEmail(id, email_address, defaultsections, default_type, default_urgency, send_confirm, reveal_issue_url=reveal_issue_url) self._setObject(id, acceptingemail) return getattr(self, id) InitializeClass(POP3Account) class AcceptingEmail(SimpleItem.SimpleItem, IssueTracker.IssueTrackerFolderBase): """ AcceptingEmail class """ meta_type = ACCEPTINGEMAIL_METATYPE icon = '%s/issuetracker_acceptingemail.gif'%ICON_LOCATION meta_types = [] _properties=({'id':'email_address', 'type': 'string', 'mode':'w'}, {'id':'defaultsections', 'type': 'lines', 'mode':'w'}, {'id':'default_type', 'type': 'string', 'mode':'w'}, {'id':'default_urgency', 'type': 'string', 'mode':'w'}, {'id':'send_confirm', 'type': 'boolean','mode':'w'}, {'id':'whitelist_emails','type': 'lines', 'mode':'w'}, {'id':'blacklist_emails','type': 'lines', 'mode':'w'}, {'id':'reveal_issue_url','type': 'lines', 'mode':'w'}, ) security=ClassSecurityInfo() def __init__(self, id, email_address, defaultsections, default_type, default_urgency, send_confirm=True, reveal_issue_url=True): """ init """ self.id = str(id) self.email_address = str(email_address) if type(defaultsections) == type('s'): defaultsections = [defaultsections] self.defaultsections = defaultsections self.default_type = str(default_type) self.default_urgency = str(default_urgency) self.send_confirm = bool(send_confirm) self.whitelist_emails = [] self.blacklist_emails = [] self.reveal_issue_url = bool(reveal_issue_url) def getId(self): """ return id """ return self.id def getTitle(self): """ return email_address """ return self.getEmailAddress() def getEmailAddress(self): """ return email_address """ return self.email_address def doSendConfirm(self): """ return send_confirm """ return getattr(self, 'send_confirm', False) def getWhitelistEmails(self): """ return whitelist_emails """ return getattr(self, 'whitelist_emails', []) def getBlacklistEmails(self): """ return blacklist_emails """ return getattr(self, 'blacklist_emails', []) def revealIssueURL(self): """ return if the URL of the issue should be revealed in the email coming back. Since this attribute was introduced late, we have to assume that the attribute isn't always available. """ default = True return getattr(self, 'reveal_issue_url', default) def acceptOriginatorEmail(self, email, default_accept=True): """ return true if this email is either whitelisted or not blacklisted """ whitelist = self.getWhitelistEmails() blacklist = self.getBlacklistEmails() # note the order for reject, emaillist in ([False, whitelist], [True, blacklist]): for okpattern in emaillist: _okpattern = re.compile(okpattern.replace('*','\S+'), re.I) if _okpattern.findall(email): # match! if reject: return False else: return True # default is to accept all return default_accept def editDetails(self, email_address=None, defaultsections=None, default_type=None, default_urgency=None, send_confirm=None, reveal_issue_url=None, whitelist_emails=None, blacklist_emails=None): """ edit details if not None """ if email_address is not None: self.email_address = email_address if defaultsections is not None: if type(defaultsections) == type('s'): defaultsections = [defaultsections] self.defaultsections = defaultsections if default_type is not None: self.default_type = default_type if default_urgency is not None: self.default_urgency = default_urgency if send_confirm is not None: self.send_confirm = bool(send_confirm) if reveal_issue_url is not None: self.reveal_issue_url = bool(reveal_issue_url) if whitelist_emails is not None: self.whitelist_emails = self._tidyEmailList(whitelist_emails) if blacklist_emails is not None: self.blacklist_emails = self._tidyEmailList(blacklist_emails) def _tidyEmailList(self, emaillist): """ only accept either valid email addresses or those with * in them. """ emaillist = [x.strip() for x in emaillist if x.strip()] for e in emaillist: if e.find('**') > -1: m = "Email wildcard repeated excessively %s" raise ValueError, m % e elif e.find(' ') > -1: m = "Email wildcard contains whitespace %r" raise ValueError, m % e return emaillist def assertAllProperties(self): """ make sure accepting email has all properties """ props = {'email_address':'', 'defaultsections':[], 'default_type':self.default_type, 'default_urgency':self.default_urgency, 'send_confirm':1, 'whitelist_emails':[], 'blacklist_emails':[], } count = 0 for key, default in props.items(): if not self.__dict__.has_key(key): self.__dict__[key] = default count += 1 return count InitializeClass(AcceptingEmail) IssueTrackerProduct/IssueUserFolder.py0000644000175000017500000006454311012074373020330 0ustar peterbepeterbe# IssueTrackerProduct # # www.issuetrackerproduct.com # Peter Bengtsson # License: ZPL # __version__='0.0.7' # python # Zope from AccessControl import User from Globals import DTMLFile, MessageDialog, Persistent from AccessControl import ClassSecurityInfo # Product import Utils from I18N import _ from Constants import * from Permissions import IssueTrackerManagerRole, IssueTrackerUserRole, VMS #---------------------------------------------------------------------------- manage_addIssueUserFolderForm=DTMLFile('dtml/addIssueUserFolder', globals()) def manage_addIssueUserFolder(self, title='', webmaster_email='', keep_usernames=None, REQUEST=None): """ ads """ id="acl_users" webmaster_email = str(webmaster_email).strip() old_users = None if keep_usernames and id in self.objectIds('User Folder'): old_users = self.manage_getUsersToConvert(withpasswords=True) self.manage_delObjects([id]) i=IssueUserFolder(webmaster_email) self._setObject(id,i) userfolder = self._getOb(id) for role in [IssueTrackerManagerRole, IssueTrackerUserRole]: # only add these roles if they don't already exist if role not in self.valid_roles(): self.REQUEST.set('role', role) self.manage_defined_roles(submit='Add Role', REQUEST=self.REQUEST) if old_users and REQUEST is not None: old_users_dict = {} for user in old_users: old_users_dict[user['username'].replace(' ','')] = user keys = REQUEST.get('keys') for key in keys: username = REQUEST.get('username_%s'%key) if key not in keep_usernames: continue if not username: raise "IllegalValue", 'A username must be specified' password = old_users_dict[key]['__'] fullname = REQUEST.get('fullname_%s'%key) if not fullname: fullname = username email = REQUEST.get('email_%s'%key) if not Utils.ValidEmailAddress(email): raise "InvalidEmail", "Email (%r) not valid email address"%email roles = REQUEST.get('roles_%s'%key) domains = REQUEST.get('domains_%s'%key) if domains and not self.domainSpecValidate(domains): raise "IllegalValue", 'Illegal domain specification' userfolder._doAddUser(username, password, roles, domains, email=email, fullname=fullname) if REQUEST: return self.manage_main(self, REQUEST) def _uniqify(somelist): d={} for i in somelist: d[i]=1 return d.keys() def _merge_dicts_nicely(dict1, dict2): """ make all dict values into lists into one dict """ new = {} for k,v in (dict1.items()+dict2.items()): if new.has_key(k): # that we haven't seen before if type(v)==type([]): new[k].extend(v) else: new[k].append(v) else: if type(v)==type([]): new[k] = v else: new[k] = [v] for k,v in new.items(): thatlist = _uniqify(v) thatlist.sort() new[k] = thatlist return new def _find_issuetrackers(context): issuetrackers = [] for object in context.objectValues(): if object.meta_type == ISSUETRACKER_METATYPE: issuetrackers.append(object) elif object.isPrincipiaFolderish: issuetrackers.extend(_find_issuetrackers(object)) return issuetrackers def manage_getUsersToConvert(self, withpasswords=False): """ find all the users in the acl_users folder here, and try to find a suitable name and email address. """ if not 'acl_users' in self.objectIds('User Folder'): # just double checking that we have a old user folder here return [] old_user_folder = self.acl_users old_users = [] issuetrackers = _find_issuetrackers(self) if self.meta_type == ISSUETRACKER_METATYPE: if self not in issuetrackers: issuetrackers.append(self) acl_cookienames = acl_cookieemails = {} for issuetracker in issuetrackers: _cookienames = issuetracker.getACLCookieNames() if _cookienames: acl_cookienames = _merge_dicts_nicely(acl_cookienames, _cookienames) _cookieemails = issuetracker.getACLCookieEmails() if _cookieemails: acl_cookieemails = _merge_dicts_nicely(acl_cookieemails, _cookieemails) for user in old_user_folder.getUsers(): fullname = acl_cookienames.get(str(user), []) email = acl_cookieemails.get(str(user),[]) if not fullname and email: _email1 = email[0].split('@')[0] if len(_email1.split('.'))>1: fullname = [x.capitalize() \ for x in _email1.split('.')] fullname = ' '.join(fullname) elif len(_email1.split('_'))>1: fullname = [x.capitalize() \ for x in _email1.split('_')] fullname = ' '.join(fullname) else: fullname = str(user).capitalize() d = {'username':str(user), 'domains':user.domains, 'roles':user.roles, 'fullname':fullname, 'email':email} if email and email[0] and Utils.ValidEmailAddress(email[0]): d['invalid_email'] = False else: d['invalid_email'] = True if withpasswords: d['__'] = user.__ old_users.append(d) return old_users #---------------------------------------------------------------------------- class IssueUserFolder(User.UserFolder): """ user folder for managing IssueUsers """ meta_type = ISSUEUSERFOLDER_METATYPE ## these variables need to be in the new class so they are used in the ## correct context and won't be taken from the base class and consequently ## from the wrong directory _mainUser = DTMLFile('dtml/mainIssueUser', globals()) _mainUser._setName('_mainUser') manage = _mainUser manage_main = _mainUser _add_User = DTMLFile('dtml/addIssueUser', globals()) _editUser = DTMLFile('dtml/editIssueUser', globals()) _passwordReminder = DTMLFile('dtml/passwordReminder', globals()) security = ClassSecurityInfo() def __init__(self, webmaster_email=''): """ Same as inherited but a possible webmaster_email attribute """ self.webmaster_email = webmaster_email apply(User.UserFolder.__init__, (self,), {}) def _addUser(self, name, password, confirm, roles, domains, REQUEST=None): if not name: return MessageDialog( title ='Illegal value', message='A username must be specified', action ='manage_main') if not password or not confirm: if not domains: return MessageDialog( title ='Illegal value', message='Password and confirmation must be specified', action ='manage_main') if self.getUser(name) or (self._emergency_user and name == self._emergency_user.getUserName()): return MessageDialog( title ='Illegal value', message='A user with the specified name already exists', action ='manage_main') if (password or confirm) and (password != confirm): return MessageDialog( title ='Illegal value', message='Password and confirmation do not match', action ='manage_main') if not roles: roles=[] if not domains: domains=[] if domains and not self.domainSpecValidate(domains): return MessageDialog( title ='Illegal value', message='Illegal domain specification', action ='manage_main') if not Utils.ValidEmailAddress(REQUEST['email']): return MessageDialog( title ='Illegal value', message='Not a valid email address', action ='manage_main') if not REQUEST.get('fullname',''): REQUEST.set('fullname',name) self._doAddUser(name, password, roles, domains, email=REQUEST['email'], fullname=REQUEST['fullname'], must_change_password=REQUEST.get('must_change_password', False), display_format=REQUEST.get('display_format',''), ) if REQUEST: return self._mainUser(self, REQUEST) def _changeUser(self,name,password,confirm,roles,domains,REQUEST=None): if password == 'password' and confirm == 'pconfirm': # Protocol for editUser.dtml to indicate unchanged password password = confirm = None if not name: return MessageDialog( title ='Illegal value', message='A username must be specified', action ='manage_main') if password == confirm == '': if not domains: return MessageDialog( title ='Illegal value', message='Password and confirmation must be specified', action ='manage_main') if not self.getUser(name): return MessageDialog( title ='Illegal value', message='Unknown user', action ='manage_main') if (password or confirm) and (password != confirm): return MessageDialog( title ='Illegal value', message='Password and confirmation do not match', action ='manage_main') if not roles: roles=[] if not domains: domains=[] if domains and not self.domainSpecValidate(domains): return MessageDialog( title ='Illegal value', message='Illegal domain specification', action ='manage_main') if REQUEST.get('email') and not Utils.ValidEmailAddress(REQUEST['email']): return MessageDialog( title ='Illegal value', message='Not a valid email address', action ='manage_main') self._doChangeUser(name, password, roles, domains, email=REQUEST.get('email'), fullname=REQUEST.get('fullname'), must_change_password=REQUEST.get('must_change_password')) if REQUEST: return self._mainUser(self, REQUEST) def _changeUserDetails(self, name, fullname, email, REQUEST=None): """ Simple method that does what _changeUser() does but without password and roles """ fullname = fullname.strip() email = email.strip() if not fullname: raise "NoFullname", "Full name must be specified" if not email: raise "NoEmail", "Email must be specified" elif not Utils.ValidEmailAddress(email): raise "InvalidEmail", "Email not valid email address" self._doChangeUserDetails(name, fullname, email) if REQUEST: return self._mainUser(self, REQUEST) def _doChangeUserDetails(self, name, fullname, email, must_change_password=None): user = self.data[name] user.fullname = fullname user.email = email if must_change_password is not None: user.must_change_password = must_change_password self.data[name] = user def _doAddUser(self, name, password, roles, domains, **kw): """Create a new user""" email=kw['email'] fullname=kw['fullname'] must_change_password=kw.get('must_change_password',False) display_format = kw.get('display_format','') if password is not None and self.encrypt_passwords: password = self._encryptPassword(password) self.data[name]=IssueUser(name, password, roles, domains, email, fullname, must_change_password, display_format) def _doChangeUser(self, name, password, roles, domains, **kw): user = self.data[name] if password is not None: if self.encrypt_passwords: password = self._encryptPassword(password) user.__ = password user.roles = roles user.domains = domains if kw.get('email'): email=kw['email'] user.email = email if kw.get('fullname'): fullname=kw['fullname'] user.fullname = fullname if kw.get('display_format'): display_format=kw['display_format'] user.display_format = display_format user.must_change_password=kw.get('must_change_password', False) self.data[name]=user def getIssueTrackerRoot(self): """ Try to return the IssueTracker instance root or None """ try: root = self.getRoot() # from aquisition if root.meta_type == ISSUETRACKER_METATYPE: return root else: return None except: # Means it's deploy outside an issuetracker return None def getWebmasterEmail(self): """ return webmaster_email or try to find a IssueTracker instance """ # returns None if not found issuetrackerroot = self.getIssueTrackerRoot() if issuetrackerroot: wherefrom = "Issue Tracker" email = issuetrackerroot.getSitemasterEmail() else: wherefrom = "Issue User Folder" email = self.webmaster_email if not Utils.ValidEmailAddress(email): m = "Webmaster email (%s) taken from %s invalid" m = m%(email, wherefrom) raise "InvalidWebmasterEmail", m def getIssueUserFolderPath(self): """ return the absolute real path of this object parent """ return '/'.join(self.getPhysicalPath()) security.declareProtected(VMS, 'manage_sendReminder') def manage_sendReminder(self, name, email_from, email_subject, remindertext): """ actually send the password reminder """ try: user = self.getUser(name) except: return MessageDialog( title ='Illegal value', message='The specified user does not exist', action ='manage_main') issuetrackerroot = self.getIssueTrackerRoot() if not email_from: raise "NoEmailFromError", "You must specify a from email address" elif not self.webmaster_email: self.webmaster_email = email_from email_to = user.getEmail() if not email_to or email_to and not Utils.ValidEmailAddress(email_to): raise "NoEmailToError", "User does not have a valid email address" replacement_key = "" if remindertext.find(replacement_key) == -1: raise "NoPasswordReplacementError",\ "No place to put the password reminder" if self.encrypt_passwords: # generate a new password and save it password = Utils.getRandomString(length=6, loweronly=1) user.__ = password else: password = user.__ if not email_subject: email_subject = "Issue Tracker password reminder" remindertext = remindertext.replace(replacement_key, password) # send it! if issuetrackerroot: # send via the issuetracker issuetrackerroot.sendEmail(remindertext, email_to, email_from, email_subject, swallowerrors=False) else: body = '\r\n'.join(['From: %s'%email_from, 'To: %s'%email_to, 'Subject: %s'%email_subject, "", remindertext]) # Expect a mailhost object. Note that here we're outside the Issuetracker try: mailhost = self.MailHost except: try: mailhost = self.SecureMailHost except: try: mailhost = self.superValues('MailHost')[0] except IndexError: raise "NoMailHostError", "No 'MailHost' available to send from" if hasattr(mailhost, 'secureSend'): mailhost.secureSend(remindertext, email_to, email_from, email_subject) else: mailhost.send(body, email_to, email_from, email_subject) m = "Password reminder sent to %s" % email_to return self.manage_main(self, self.REQUEST, manage_tabs_message=m) security.declareProtected(VMS, 'manage_passwordReminder') def manage_passwordReminder(self, name): """ wrap up a template """ try: user = self.getUser(name) except: return MessageDialog( title ='Illegal value', message='The specified user does not exist', action ='manage_main') issuetrackerroot = self.getIssueTrackerRoot() if self.webmaster_email: from_field = self.webmaster_email elif issuetrackerroot: from_field = issuetrackerroot.getSitemasterFromField() else: from_field = "" subject = _("Password reminder") if issuetrackerroot is not None: subject = "%s: %s" % (issuetrackerroot.getTitle(), subject) msg = "Dear %(fullname)s,\n\n" msg += "This is a password reminder for you to use on %(url)s.\n" if self.encrypt_passwords: msg += "Since your previous password was encrypted we have had "\ "to recreate a new password for you.\n" msg += "Your username is still: %(username)s\nand your password is: "\ "" msg += "\n\n" msg += "PS. The administrator sending this password reminder will"\ " not able to read your password at any time." #if issuetrackerroot is not None: # msg += "Now you can go to %s and log in"%(issuetrackerroot.absolute_url() d = {'fullname':user.getFullname(), 'username':name} if issuetrackerroot is not None: d['url'] = issuetrackerroot.absolute_url() else: d['url'] = self.absolute_url().replace('/acl_users','') msg = msg % d return self._passwordReminder(self, self.REQUEST, user=user, username=name, subject=subject, message=msg, from_field=from_field) #---------------------------------------------------------------------------- class IssueUser(User.SimpleUser, Persistent): """ User with additional email property """ misc_properties = {} # backwardcompatability def __init__(self, name, password, roles, domains, email, fullname, must_change_password=False, display_format='', use_accesskeys=False, remember_savedfilter_persistently=False, show_nextactions=False): """ constructor method """ self.name = name self.__ = password self.roles = roles self.domains = domains self.email = email self.fullname = fullname self.must_change_password = must_change_password self.display_format = display_format self.use_accesskeys = use_accesskeys self.remember_savedfilter_persistently = remember_savedfilter_persistently self.show_nextactions = show_nextactions self._user_lists = None # For the User page. if None, not set #self._user_display_format = None self.misc_properties = {} def getIssueUserPath(self): """ return the absolute real path of this object parent """ return '/'.join(self.getPhysicalPath()) def getIssueUserIdentifier(self): """ return the parents physical path and username """ return self.getIssueUserPath(), self.name def getIssueUserIdentifierString(self): """ return getIssueUserIdentifier() as a comma separated string. """ return ','.join(self.getIssueUserIdentifier()) def getIssueUserIdentifierstring(self): """ return getIssueUserIdentifier() as one string """ path, name = self.getIssueUserIdentifier() return "%s,%s"%(path, name) def getEmail(self): """ returns the user's email """ return self.email def getFullname(self): """ returns the fullname """ return self.fullname def getUserLists(self): """ return _user_lists """ if not hasattr(self, '_user_lists'): self._user_lists = None return None return self._user_lists def setUserLists(self, lists): """ add these lists """ was = self.getUserLists() if was is None: was = [] new = was+lists self._user_lists = Utils.uniqify(new) def getDisplayFormat(self): """ return prefered displayformat """ if hasattr(self, 'display_format'): return getattr(self, 'display_format') else: # old bad code return getattr(self, '_user_display_format', None) def setDisplayFormat(self, displayformat): """ set prefered displayformat """ self.display_format = displayformat def useAccessKeys(self, default=False): """ return prefered displayformat """ return getattr(self, 'use_accesskeys', default) def rememberSavedfilterPersistently(self, default=False): """ return if last savedfilter id should be remembered persistently (for more info read rememberSavedfilterPersistently() in IssueTracker.py) """ return getattr(self, 'remember_savedfilter_persistently', default) def showNextActionIssues(self, default=False): """ return if 'Your next action issues' should be shown on the homepage """ return getattr(self, 'show_nextactions', default) def setRememberSavedfilterPersistently(self, toggle): """ set to saved last savedfilter id persistently or not """ self.remember_savedfilter_persistently = not not toggle def setUseNextActionIssues(self, toggle): """ set to saved last savedfilter id persistently or not """ self.show_nextactions = not not toggle def setAccessKeys(self, toggle): """ set prefered displayformat """ self.use_accesskeys = not not toggle # makes sure it's bool type def getMiscProperty(self, key, default=None): """ return from misc_properties """ return self.misc_properties.get(key, default) def hasMiscProperty(self, key): """ do we have it in misc_properties """ return self.misc_properties.has_key(key) #if not hasattr(self, 'misc_properties'): # return False #else: def setMiscProperty(self, key, value): """ set in misc_properties dict """ was = self.misc_properties was[key] = value self.misc_properties = was def debugInfo(self): """ return the misc_properties """ if DEBUG: # from Constants out = [] out.append("misc_properties:%s" % self.misc_properties) out.append("must_change_password:%s" % self.must_change_password) out.append("display_format:%s" % self.display_format) out.append("use_accesskeys:%s" % getattr(self, 'use_accesskeys', False)) out.append("remember_savedfilter_persistently:%s" % getattr(self, 'remember_savedfilter_persistently', False)) out.append("_user_lists:%s" % self._user_lists) return ', '.join(out) else: return "Not in debug mode" def mustChangePassword(self): """ return if 'self.must_change_password' is True """ if not hasattr(self, 'must_change_password'): self.must_change_password = False # default return self.must_change_password def _unmust_mustChangePassword(self): """ toggle the boolean value """ newvalue = False self.must_change_password = newvalue def sendPasswordReminder(self): """ will send the password in an email """ raise "DeprecatedError" if self.encrypt_passwords: m = "Password reminders disabled since passwords are encrypted" raise "PasswordsEncrypted", m S = "Issue User Password Reminder" M = "Dear %s,\nYour password is: "%self.getFullname() M += self.__ M += '\n' F = self.getWebmasterEmail() T = self.getEmail() # Find the nearest MailHost object mailhost = self.MailHost mailhost.send(M, T, F, S) page = self.manage_main m = "Email password reminder sent to %s"%T return page(self, self.REQUEST, manage_tabs_message=m) IssueTrackerProduct/Errors.py0000644000175000017500000000117611012074373016512 0ustar peterbepeterbeclass NotAFileError(Exception): pass class IssueInputError(Exception): pass class NoACLAdderError(Exception): """ happens when the ACL user object is not found""" pass class DeprecatedError(Exception): pass class AssigneeNotFoundError(Exception): pass class UnmatchableError(Exception): pass class DataSubmitError(Exception): """happens when the data to a functional view is passed invalid data and it's not via the web """ pass class UserSubmitError(Exception): """happens when the user incorrectly tries to do something related to her authentication that doesn't work """ passIssueTrackerProduct/Notifyables.py0000644000175000017500000004353511012074373017522 0ustar peterbepeterbe# python # Zope from Products.PageTemplates.PageTemplateFile import PageTemplateFile from OFS import SimpleItem, Folder from AccessControl import ClassSecurityInfo from Globals import MessageDialog, InitializeClass, DTMLFile # Product import Utils from Constants import * class Notifyables: # Global container # def hasGlobalContainer(self): """ Check if a global container exists """ if hasattr(self, DEFAULT_NOTIFYABLECONTAINER_ID): return true else: return false def isGlobalHere(self): """ Check if we're "standing" in a global container """ if hasattr(self, 'notifyables'): return false else: return true def _getManagementFormURL(self, msg=None): """ return the URL to redirect to for returning to the appropriate interface. """ if self.isGlobalHere(): url = self.absolute_url() url += '/manage_GlobalManagementForm' else: url = self.absolute_url() url += '/manage_ManagementNotifyables' if msg is not None: params = {'manage_tabs_message':msg} url = Utils.AddParam2URL(url, params) return url def getGlobalContainer(self): """ Return the global_notifyables object """ return getattr(self, DEFAULT_NOTIFYABLECONTAINER_ID) def getManagementForm(self): """ Return correct management form depending on container """ if self.isGlobalHere(): return self.manage_GlobalManagementForm else: return self.manage_ManagementNotifyables #return self.manage_ManagementForm # Notifyables # def hasNotifyables(self): """ see if there are any notifyables at all """ if len(self.getNotifyables()) > 0: return 1 else: return 0 def getNotifyables(self, only=None): """ Return all Notifyable objects """ if only =='': only = None if only == 'global': local_container = None else: local_container = self.getNotifyablesObjectContainer(only='local') if only == 'local': global_container = None else: global_container = self.getNotifyablesObjectContainer(only='global') meta_type = NOTIFYABLE_METATYPE if local_container is None: local_notifyables = [] else: local_notifyables = local_container.objectValues(meta_type) if global_container: global_notifyables = global_container.objectValues(meta_type) return local_notifyables + global_notifyables else: return local_notifyables def getNotifyablesByGroup(self, group, only=None): """ return a list of all notiyables belonging to this group """ checked = [] for notifyable in self.getNotifyables(only=only): if notifyable.partofGroup(group): checked.append(notifyable) return checked def getNotifyablesEmailName(self, only=None): """ wrap getNotifyables and return dictionary """ email_name = {} for nf in self.getNotifyables(only=only): email_name[nf.getEmail()] = nf.getName() return email_name def manage_addNotifyables(self, REQUEST): """ Save notifyables (only via the web) """ new_emails = REQUEST.get('new_email',[]) no_created = 0 no_attempted = 0 for c in range(len(new_emails)): email = REQUEST['new_email'][c] alias = REQUEST['new_alias'][c] groups = REQUEST.get('new_groups',[]) if email != '': no_attempted += 1 if not isinstance(groups, list): groups = [groups] if Utils.ValidEmailAddress(email): self.manage_addNotifyable(email, alias, groups) no_created += 1 if no_created == no_attempted: if len(new_emails) > 1: mtm = "Notifyables created." else: mtm = "Notifyable created." else: if len(new_emails) > 1: mtm = """%s out of %s were created. Check your input data."""%(no_created, no_attempted) else: mtm = "Notifyable not created. Check your input data" form = self.getManagementForm() return form(REQUEST, manage_tabs_message=mtm) def manage_addNotifyable(self, email, alias='', groups=[], REQUEST=None): """ Create notifyable object """ email, alias = email.strip(), alias.strip() if Utils.ValidEmailAddress(email): container = self.getNotifyablesObjectContainer() id = self.GenerateNotifyableId(email) n = IssueTrackerNotifyable(id, alias, email, groups) container._setObject(id, n) if REQUEST is not None: mtm= "%s created."%NOTIFYABLE_METATYPE return MessageDialog(title=mtm, message=mtm, action='%s/manage_main' % REQUEST['URL1']) else: raise "InvalidEmailAddress", \ "Email address used (%s) was invalid"%email def manage_delNotifyables(self, REQUEST): """ Prepare which notifyable ids to remove """ ids = REQUEST.get('del_notify_ids',[]) container = self.getNotifyablesObjectContainer() container.manage_delObjects(ids) msg = "Notifyables deleted." url = self._getManagementFormURL(msg) REQUEST.RESPONSE.redirect(url) def _filterNotifyGroups(self, groups): """ Return the list groups but filter out groups that don't exists in getNotifyableGroups() """ existing_group_ids = [] for group in self.getNotifyableGroups(): existing_group_ids.append(group.getId()) n_groups=[] for group in groups: if group.strip() in existing_group_ids and \ group.strip() not in n_groups: n_groups.append(group.strip()) return n_groups # Notify groups # def hasOldProperty(self): """ Returns how many items in the old property """ if hasattr(self, 'notify_groups') and \ type(self.notify_groups)==ListType: return len(self.notify_groups) else: return 0 def convertOldGroups2Objects(self, REQUEST=None): """ If still having the old property, recreate as objects """ all_object_groups = self.getNotifyableGroupIds() if self.hasOldProperty(): for each in self.notify_groups: try: self.manage_addNotifyableGroup(each) except: pass self.notify_groups = [] if REQUEST is not None: form = self.getManagementForm() mtm = "Old property now updated for groups." return form(REQUEST, manage_tabs_message=mtm) def getGroupsByIds(self, ids): """ Return the objects of notifyable group ids """ objects = self.getNotifyableGroups() r_objects = [] for object in objects: if object.getId() in ids: r_objects.append(object) return r_objects def getNotifyableGroups(self, only=None): """ Get all notifyable groups """ if only =='': only = None if only=='global': local_container = None else: local_container = self.getNotifyablesObjectContainer(only='local') if only=='local': global_container = None else: global_container = self.getNotifyablesObjectContainer(only='global') meta_type = NOTIFYABLEGROUP_METATYPE if local_container is None: local_notifyables = [] else: local_notifyables = local_container.objectValues(meta_type) if global_container: global_notifyables = global_container.objectValues(meta_type) return local_notifyables + global_notifyables else: return local_notifyables def getNotifyableGroupIds(self): """ return the ids of all group objects """ ids=[] for object in self.getNotifyableGroups(): ids.append(object.getId()) return ids def manage_delNotifyGroups(self, notify_groups, REQUEST=None): """ delete some groups from self.notify_groups """ container = self.getNotifyablesObjectContainer() container.manage_delObjects(notify_groups) msg = 'Notifygroups deleted.' if REQUEST is not None: url = self._getManagementFormURL(msg) REQUEST.RESPONSE.redirect(url) else: return msg def manage_saveNotifyGroup(self, notify_group, REQUEST): """ add one group (via the web only) """ self.manage_addNotifyableGroup(notify_group) msg = "%s created."%NOTIFYABLEGROUP_METATYPE url = self._getManagementFormURL(msg) REQUEST.RESPONSE.redirect(url) def GenerateNotifyableId(self, email, length=10, int_length=None): """ generate a random id for a notifyable string """ email= email.replace('@','_at_') return email def createNotifyGroupId(self, group): """ generate a random id for a notifyable string """ group = Utils.safeId(group.strip()) group = group.replace(' ','_').replace('-','_').lower() return group def manage_addNotifyableGroup(self, notify_group, REQUEST=None): """ Create a notifyable group """ dest = self.getNotifyablesObjectContainer() id = self.createNotifyGroupId(notify_group) group = IssueTrackerNotifyableGroup(id, notify_group) dest._setObject(id, group) self = dest._getOb(id) if REQUEST is not None: mtm= "%s created."%NOTIFYABLEGROUP_METATYPE return MessageDialog(title=mtm, message=mtm, action='%s/manage_main' % REQUEST['URL1']) def getNotifyablesObjectContainer(self, only=None): """ Return the container where notifyables and groups""" # Tests whether we are in an IssueTracker if only is None: if hasattr(self, 'notifyables'): return self.notifyables else: return getattr(self,DEFAULT_NOTIFYABLECONTAINER_ID) elif only=='local': if hasattr(self, 'notifyables'): return self.notifyables else: return None elif only=='global': if hasattr(self, DEFAULT_NOTIFYABLECONTAINER_ID): return getattr(self,DEFAULT_NOTIFYABLECONTAINER_ID) else: return None else: return None # Templates # dtml_file = 'dtml/NotifyableManagementPartForm' NotifyableManagementPartForm = DTMLFile(dtml_file, globals()) class IssueTrackerNotifyable(SimpleItem.SimpleItem): """ IssueTrackerNotifyable class """ meta_type = NOTIFYABLE_METATYPE icon = '%s/issuetracker_notifyable.gif'%ICON_LOCATION meta_types = [] _properties=({'id':'alias', 'type': 'string', 'mode':'w'}, {'id':'email', 'type': 'string', 'mode':'w'}, {'id':'groups', 'type': 'lines', 'mode':'w'}, ) security=ClassSecurityInfo() manage_editNotifyableForm = DTMLFile('dtml/editNotifyableForm', globals()) manage_options = ( {'label':'Properties', 'action':'manage_editNotifyableForm'}, ) def __init__(self, id, alias, email, groups=[]): """ init """ if not Utils.ValidEmailAddress(email): raise "InvalidEmailAddress",\ "The email address (%s) was incorrect"%email else: self.id = id self.alias = alias.strip() self.email = email.strip() self.groups = groups def getName(self): """ Return self.alias. This might be in the future: self.firstname + self.lastname """ return self.alias def getEmail(self): """ return the email address """ return self.email def getTitle(self): """ return alias or email address """ name = self.getName() if name: return name else: return self.getEmail() def getGroups(self): """ return groups """ return self.groups def partofGroup(self, group): """ case insensitivly check if 'group' is part of this 'self.groups' """ these = [Utils.ss(x) for x in self.getGroups()] if isinstance(group, basestring): return Utils.ss(group) in these else: return Utils.ss(group.getTitle()) in these def showGroups(self): """ return blank or comma separated with brackets """ def manage_editNotifyable(self, alias=None, email=None, groups=None, REQUEST=None): """ save changes to Notifyable """ no = self n={'id':no.id} if alias is not None: self.alias = alias.strip() if email is not None and Utils.ValidEmailAddress(email.strip()): self.email = email.strip() if groups is not None: if not isinstance(groups, list): groups = [groups] self.groups = groups msg = 'Notifyable updated.' if REQUEST is not None: url = self.absolute_url()+'/manage_editNotifyableForm' params = {'manage_tabs_message':msg} url = Utils.AddParam2URL(url, params) REQUEST.RESPONSE.redirect(url) else: return msg def getEmail(self): """ Return self.email """ return self.email def getAlias(self): """ Return self.alias """ return self.alias InitializeClass(IssueTrackerNotifyable) class IssueTrackerNotifyableGroup(SimpleItem.SimpleItem): """ IssueTrackerNotifyableGroup class """ meta_type = NOTIFYABLEGROUP_METATYPE icon = '%s/issuetracker_notifyablegroup.gif'%ICON_LOCATION meta_types = [] _properties=({'id':'title', 'type': 'string', 'mode':'w'}, ) security=ClassSecurityInfo() manage_editNotifyableGroupForm = DTMLFile('dtml/editNotifyableGroupForm', globals()) manage_options = ( {'label':'Properties', 'action':'manage_editNotifyableGroupForm'}, ) def __init__(self, id, title): """ init """ self.id = id self.title = title def getId(self): """ return id """ return self.id def getTitle(self): """ return title """ return self.title def manage_editNotifyableGroup(self, title=None, REQUEST=None): """ edit properties """ if title is not None: self.title = title if REQUEST is not None: mtm="Group changed." form = self.manage_editNotifyableGroupForm return form(REQUEST, manage_tabs_message=mtm) InitializeClass(IssueTrackerNotifyable) zpt_file = 'zpt/addNotifyableContainerForm' manage_addNotifyableContainerForm = PageTemplateFile(zpt_file, globals()) def manage_addNotifyableContainer(dispatcher, REQUEST=None): """ Create a notifyable container object """ id = DEFAULT_NOTIFYABLECONTAINER_ID title = DEFAULT_NOTIFYABLECONTAINER_TITLE dest = dispatcher.Destination() container = IssueTrackerNotifyableContainer(id, title) dest._setObject(id, container) self = dest._getOb(id) if REQUEST is not None: mtm= "%s created."%NOTIFYABLECONTAINER_METATYPE if int(REQUEST.get('goto_after',0)): page = self.manage_GlobalManagementForm return page(self, REQUEST, manage_tabs_message=mtm) else: return MessageDialog(title=mtm, message=mtm, action='%s/manage_main' % REQUEST['URL1']) class IssueTrackerNotifyableContainer(Folder.Folder, Notifyables): """ IssueTrackerNotifyableContainer class """ meta_type = NOTIFYABLECONTAINER_METATYPE icon = '%s/issuetracker_notifyablegroup.gif'%ICON_LOCATION #meta_types = [NOTIFYABLEGROUP_METATYPE] _properties=({'id':'title', 'type': 'string', 'mode':'w'}, {'id':'groups', 'type': 'lines', 'mode':'w'}, ) security=ClassSecurityInfo() dtml_file = 'dtml/GlobalManagementForm' manage_GlobalManagementForm = DTMLFile(dtml_file, globals()) manage_options = ( {'label':'Management', 'action':'manage_GlobalManagementForm'}, Folder.Folder.manage_options[0] ) def __init__(self, id, title, groups=[]): self.id = id self.title = title self.groups = groups def getRandomString(self, length=5, loweronly=0, numbersonly=0): """ return a completely random piece of string """ script = Utils.getRandomString return script(length, loweronly, numbersonly) InitializeClass(IssueTrackerNotifyableContainer) IssueTrackerProduct/Issue.py0000755000175000017500000035525511012074373016343 0ustar peterbepeterbe# IssueTrackerProduct # # Peter Bengtsson # License: ZPL # # python import re, sys, cgi, os from time import time from string import zfill try: import transaction except ImportError: # we must be in an older than 2.8 version of Zope transaction = None # Zope from Acquisition import aq_inner, aq_parent from Globals import InitializeClass from AccessControl import ClassSecurityInfo from zLOG import LOG, ERROR, INFO, PROBLEM, WARNING from DateTime import DateTime from webdav.WriteLockInterface import WriteLockInterface # Is CMF installed? try: from Products.CMFCore.utils import getToolByName as CMF_getToolByName except ImportError: CMF_getToolByName = None # Product from IssueTracker import IssueTracker, debug, safe_hasattr from TemplateAdder import addTemplates2Class import Utils from Utils import unicodify from Constants import * from Errors import * from Permissions import VMS, ChangeIssuePermission from I18N import _ #---------------------------------------------------------------------------- # Misc stuff ss = lambda s: s.strip().lower() # to save some typing space #---------------------------------------------------------------------------- class IssueTrackerIssue(IssueTracker): """ Issues class as containers """ meta_type = ISSUE_METATYPE _properties=({'id':'title', 'type': 'ustring', 'mode':'w'}, {'id':'issuedate', 'type': 'date', 'mode':'w'}, {'id':'modifydate', 'type': 'date', 'mode':'w'}, {'id':'status', 'type': 'ustring', 'mode':'w'}, {'id':'type', 'type': 'ustring', 'mode':'w'}, {'id':'urgency', 'type': 'ustring', 'mode':'w'}, {'id':'sections', 'type': 'ulines', 'mode':'w'}, {'id':'fromname', 'type': 'ustring', 'mode':'w'}, {'id':'email', 'type': 'string', 'mode':'w'}, {'id':'acl_adder', 'type': 'string', 'mode':'w'}, {'id':'url2issue', 'type': 'string', 'mode':'w'}, {'id':'confidential', 'type': 'boolean','mode':'w'}, {'id':'hide_me', 'type': 'boolean','mode':'w'}, {'id':'description', 'type': 'utext', 'mode':'w'}, {'id':'display_format','type': 'string', 'mode':'w'}, {'id':'subscribers', 'type': 'lines', 'mode':'w'}, {'id':'submission_type','type':'string', 'mode':'w'}, ) security=ClassSecurityInfo() manage_options = ( {'label':'Contents', 'action':'manage_main'}, {'label':'View', 'action':'index_html'}, {'label':'Properties', 'action':'manage_propertiesForm'} ) # backward compatability acl_adder = '' submission_type = '' def __init__(self, id, title, status, issuetype, urgency, sections, fromname, email, url2issue, confidential, hide_me, description, display_format, issuedate='', acl_adder='', submission_type='', subscribers=None, # keep this parameter (not used) for legacy (remove in a year) ): """ init an Issue object """ self.id = str(id) self.title = unicodify(title.strip()) if isinstance(issuedate, basestring): issuedate = DateTime(issuedate) self.issuedate = issuedate self.modifydate = DateTime() self.status = status self.type = issuetype self.urgency = urgency self.sections = sections self.fromname = unicodify(fromname) self.email = email self.url2issue = url2issue self.confidential = confidential self.hide_me = hide_me self.description = unicodify(description) if display_format: self.display_format = display_format else: self.display_format = self.getDefaultDisplayFormat() self.subscribers = [] self.submission_type = submission_type self.actual_time_hours = None self.estimated_time_hours = None self.email_message_id = None if acl_adder is None: acl_adder = '' self.acl_adder = acl_adder def getId(self): """ return id """ return self.id def getIssueId(self): """ return id The reason for having this method is so that one can be sure to called the correct getId() since 'getId' is a common function name here in Zope. """ return self.getId() def getGlobalIssueId(self): """ return a string that contains both the issuetrackers id and the issue id """ tmpl = '%s#%s' root_id = self.getRoot().getId() issue_id = self.getIssueId() return tmpl % (root_id, issue_id) def relative_url_path(self): """ return the url to this issue based on where you are at the moment """ return self.absolute_url().replace(self.REQUEST.URL1+'/','') def getTitle(self): """ return title """ return self.title def showTitle(self): """ return title html quoted """ t = self.getTitle() if isinstance(t, str): return self.HighlightQ( Utils.html_entity_fixer( Utils.tag_quote(t) ) ) else: return self.HighlightQ(Utils.tag_quote(t)) def getStatus(self): """ return the status of the issue """ return self.status def getURL2Issue(self): """ return url2issue """ return self.url2issue def getModifyDate(self): """ return modifydate """ if not hasattr(self, 'modifydate'): # this check is just for legacy reasons. It can probably safely # be delete at the end of 2005 self.modifydate = self.bobobase_modification_time() return self.modifydate security.declareProtected('View', 'getModifyTimestamp') def getModifyTimestamp(self): """ return the modify date as a integer timestamp """ return int(self.getModifyDate()) def _updateModifyDate(self): """ set the modify date again """ self.modifydate = DateTime() def getIssueDate(self): """ return issuedate """ return self.issuedate def getFromname(self, issueusercheck=True): """ return fromname """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # the userfolder (as it was saved) no longer exists return self.fromname if uf.meta_type == ISSUEUSERFOLDER_METATYPE: if uf.data.has_key(name): issueuserobj = uf.data[name] return issueuserobj.getFullname() or self.fromname elif CMF_getToolByName and hasattr(uf, 'portal_membership'): mtool = CMF_getToolByName(self, 'portal_membership') member = mtool.getMemberById(name) if member.getProperty('fullname'): return member.getProperty('fullname') return self.fromname def getEmail(self, issueusercheck=True): """ return email """ acl_adder = self.getACLAdder() if issueusercheck and acl_adder: ufpath, name = acl_adder.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: try: uf = self.unrestrictedTraverse(ufpath.split('/')[-1]) except KeyError: # the userfolder (as it was saved) no longer exists return self.email if uf.meta_type == ISSUEUSERFOLDER_METATYPE: if uf.data.has_key(name): issueuserobj = uf.data[name] return issueuserobj.getEmail() or self.email elif CMF_getToolByName and hasattr(uf, 'portal_membership'): mtool = CMF_getToolByName(self, 'portal_membership') member = mtool.getMemberById(name) if member.getProperty('email'): return member.getProperty('email') return self.email def getACLAdder(self): """ return acl_adder """ return self.acl_adder def _setACLAdder(self, acl_adder): """ set acl_adder """ self.acl_adder = acl_adder def isRecentlyAdded(self): """ return true if the issue was recently added. This will be true just after you press save on the Add Issue page. """ if self.REQUEST.get('HTTP_REFERER','').split('/')[-1] in ('AddIssue','QuickAddIssue'): # but perhaps you've been on the page for a while now and decide to reload it if (DateTime()-self.getIssueDate()) * 24 * 60 < 1: # not older than one minute return True elif self.REQUEST.get('NewIssue'): # the old way return True return False def isYourIssue(self): """ return true if the currently logged in user is the same user who added this issue. """ issueuser = self.getIssueUser() if issueuser: identifier = issueuser.getIssueUserIdentifier() identifier = ','.join(identifier) if identifier == self.getACLAdder(): return True else: # if you're logged in as an issue user then how could # the issue have been yours if your identifier # is not the same. # If this `return False` wasn't here a logged in user # would be able to change his email address and then see # other peoples issues. # However, as you'll see in the comment a few lines below # it's also not possible to return True here if the issue # was added by an authenticated user. return False zopeuser = self.getZopeUser() if zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_user = path+','+name if acl_user == self.getACLAdder(): return True else: return False # the last remaining chance is if the issue was added by someone # who's not logged in but has the same email address. if not self.getACLAdder(): if self.getEmail() == self.getSavedUser('email'): return True return False def getDisplayFormat(self): """ return display_format """ return self.display_format def getDescription(self): """ return description """ return self.description def getDescriptionPure(self): """ return description purified. If the description contains HTML for example, remove it.""" description = self.getDescription() if self.getDisplayFormat() =='html': # textify() coverts "Something" to "Something". Simple. description = Utils.textify(description) # a very common thing is that the description contains # these faux double linebreaks and when you run textify() # on '

 

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

 

','') return description def getEmailMessageId(self): """ if the email was submitted via email it will most likely have a message id """ return getattr(self, 'email_message_id', None) def _setEmailMessageId(self, message_id): """ set the email message id """ assert message_id.strip(), "Message_id not valid" self.email_message_id = message_id.strip() def _setEmailOriginal(self, original_email): """ set the original_email attribute """ self.original_email = original_email def hasEmailOriginal(self): """ return if we have a 'original_email' attribute set """ return hasattr(self, 'original_email') def ShowOriginalEmail(self, REQUEST): """ return the original email text """ if REQUEST: REQUEST.RESPONSE.setHeader('Content-Type','text/plain') return self.original_email def _findIssueLinks(self, formatted): """ return a dictionary where we find each issue link and it's relative URL. This method is quite slow since it does a check if the issues exist so only fire this of rarely. """ root = self.getRoot() root_id = root.getId() trackerids = [root_id] roots_parent = aq_parent(aq_inner(root)) adjacent_items = roots_parent.objectItems(ISSUETRACKER_METATYPE) for adj_issuetracker_id in [one for (one, two) in adjacent_items]: trackerids.append(adj_issuetracker_id) prefix = self.issueprefix zfill_ = self.randomid_length regex = Utils.getFindIssueLinksRegex(zfill_, trackerids, prefix) _inner_template_ = '%s#%s' _outer_template_ = '%s#%s' def process_find(match): find = match.group(1) if formatted[match.span()[1]:match.span()[1]+4] != '': # deconstruct this find to see if we can to pursue it trackerid, issueid = match.group(1).split('#') if prefix: if len(issueid.split(prefix))==2: issueid = issueid.split(prefix)[1] if issueid.isdigit() and not issueid.startswith('0'): # this can happen if someone sloppily enters #123 # instead of #0123 when '0123' is the real issue id issueid = zfill(issueid, zfill_) if not trackerid or trackerid == root_id: try: issue = root.getIssueObject(issueid) issue_url = issue.absolute_url_path() issue_title = issue.getTitle() #title_tag = "%s (%s)" % (issue_title, issue.getStatus().capitalize()) if self.ShowIdWithTitle(): title_tag = "#%s %s" % (issue.getId(), issue_title) else: title_tag = "%s" % issue_title return _inner_template_ % (issue_url, title_tag, trackerid, issueid) except AttributeError: # the issue doesn't exist in this issuetracker anymore # and there's nothing we can do about that pass elif hasattr(roots_parent, trackerid): adjacent_tracker = getattr(roots_parent, trackerid) try: # make it a link!! issue = adjacent_tracker.getIssueObject(issueid) issue_url = issue.absolute_url_path() issue_title = issue.getTitle() return _outer_template_ % (issue_url, issue_title, trackerid, issueid) except AttributeError: # the issue doesn't exist there anymore :( # nothing we can do about that pass return find return regex.sub(process_find, formatted) def _unicode_title(self): """ make the title of this issue a unicode string """ self.title = unicodify(self.title) def _unicode_description(self): """ make the description of this issue a unicode string """ self.description = unicodify(self.description) self._prerendered_description = unicodify(self._prerendered_description) def _prerender_description(self): """ Run the methods that pre-renders the description on the issue. """ description = self.getDescription() display_format = self.getDisplayFormat() formatted = self.ShowDescription(description+' ', display_format) if self.getSubmissionType()=='email': use_newline_to_br = True if display_format == 'plaintext': use_newline_to_br = False attrs = 'class="sig"' formatted = Utils.highlight_signature(formatted, attrs, use_newline_to_br=True) formatted = self._findIssueLinks(formatted) self._prerendered_description = formatted def _getFormattedDescription(self, force_refresh=False): """ return the formatted description (prerendered) or not """ if force_refresh: self._prerender_description() if getattr(self, '_prerendered_description', None): formatted = self._prerendered_description else: description = self.getDescription() display_format = self.getDisplayFormat() formatted = self.ShowDescription(description+' ', display_format) if self.getSubmissionType()=='email': attrs = 'class="sig"' formatted = Utils.highlight_signature(formatted, attrs) return formatted def showDescription(self, signature_hidden=False): """ combine ShowDescription (which is generic) with this issues display format.""" formatted = self._getFormattedDescription() highlighted = self.HighlightQ(formatted) return highlighted def getSubmissionType(self): """ return how it was submitted, empty string if not found """ return getattr(self, 'submission_type', '') def getSections(self): """ return the sections """ return self.sections def getUrgency(self): """ return urgency """ return self.urgency def getType(self): """ return type """ return self.type def countFileattachments(self): """ return how many file attachments this issue has """ return len(self.objectValues('File')) security.declareProtected(VMS, 'manage_editProperties') def manage_editProperties(self, REQUEST): """ re-prerender the description of the issue after manual change """ result = IssueTracker.manage_editProperties(self, REQUEST) try: self._prerender_description() except: if DEBUG: raise else: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Unable to _prerender_description() in manage_editProperties()", error=sys.exc_info()) return result def manage_afterAdd(self, REQUEST, RESPONSE): """ intercept so that we prerender always """ try: self._prerender_description() except: if DEBUG: raise else: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, ERROR, "Unable to _prerender_description() after add", error=sys.exc_info()) security.declareProtected(VMS, 'assertAllProperties') def assertAllProperties(self): """ make sure issue has all properties """ props = {'title':'', 'issuedate':DateTime(), 'status':self.getStatuses()[0], 'type':self.default_type, 'urgency':self.default_urgency, 'sections':self.defaultsections, 'fromname':'', 'email':'', 'url2issue':'', 'confidential':False, 'hide_me':False, 'description':'', 'display_format':self.getDefaultDisplayFormat(), 'subscribers':[], 'modifydate':self.bobobase_modification_time(), 'actual_time_hours': None, 'estimated_time_hours': None, } count = 0 for key, default in props.items(): if not self.__dict__.has_key(key): self.__dict__[key] = default count += 1 elif key=='sections' and isinstance(self.sections, tuple): self.sections = list(self.sections) count += 1 # check that self.fromname is as good as self.getFromname() attr_fromname = self.getFromname(issueusercheck=False) linked_fromname = self.getFromname(issueusercheck=True) if linked_fromname != attr_fromname: # for sanity, check that the linked fromname is ok if linked_fromname: self.fromname = linked_fromname count += 1 # check that self.email is as good as self.getFromname() attr_email = self.getEmail(issueusercheck=False) linked_email = self.getEmail(issueusercheck=True) if linked_email != attr_email: # for sanity, check that the linked email is ok if linked_email: self.email = linked_email count += 1 return count security.declareProtected('View', 'index_html') def index_html(self, REQUEST, *args, **kw): """ show the issue """ if not self.isConfidential() or self.hasManagerRole() or self.isYourIssue(): #self.RememberIssueVisit(self.getId()) self.RememberRecentIssue(self.getId(), 'viewed') if REQUEST.get('fileattachment', []): fake_fileattachments = self._getFakeFileattachments(REQUEST.get('fileattachment')) if fake_fileattachments: m = "Filename entered but no file content" SubmitError = {'fileattachment':m} self.REQUEST.set('previewissue',None) return self.ShowIssue(self, self.REQUEST, SubmitError=SubmitError, **kw) self._uploadTempFiles() return self.ShowIssue(self, self.REQUEST, **kw) else: response = self.REQUEST.RESPONSE listpage = '/%s'%self.whichList() response.redirect(self.getRootURL()+listpage) def isConfidential(self): """ check confidential property """ return getattr(self, 'confidential', False) IsConfidential = isConfidential def isHidden(self): """ return hide_me """ return self.hide_me def getEstimatedTimeHours(self): """ return estimated_time_hours """ return getattr(self, 'estimated_time_hours', None) def getActualTimeHours(self): """ return actual_time_hours """ return getattr(self, 'actual_time_hours', None) def showTimeHours(self, value, show_unit=False): """ return a string that shows the value if it's a number. """ if value: if show_unit: if value == 1: return _("1 hour") else: if int(value)==value: # eg. 1.0 but not 1.1 return _("%s hours") % int(value) else: hours, minutes = str(value).split('.') minutes = float('.%s' % minutes) minutes = int( minutes * 60) if value < 1: # show only the minutes return _("%s minutes") % minutes else: return _("%s hours %s minutes") % (hours, minutes) else: if int(value)==value: # eg. 1.0 but not 1.1 return int(value) else: return "%.2f" % value else: return "" def _parseTimeHours(self, hours): """ return a floating point number from the number of hours input. This can be in the form of a float, an int or a string containing the words 'hours' """ try: return float(hours) except ValueError: if isinstance(hours, basestring): if not hours.strip(): return 0.0 try: return float(hours.replace('hours','').replace('hour','')) except ValueError: # try to parse it minutes_ = 0 hours_ = 0 # perhaps they've written "15 minutes" minutes_regex = re.compile('((\d{1,2})\sminutes)') if minutes_regex.findall(hours): whole, number = minutes_regex.findall(hours)[0] try: minutes_ = int(number) hours = hours.replace(whole, '') except KeyError: pass hours_regex = re.compile(r'((\d{1,2})\s*(hour|hours))', re.I) if hours_regex.findall(hours): whole, number, __ = hours_regex.findall(hours)[0] try: hours_ = int(number) hours = hours.replace(whole, '') except KeyError: pass return hours_ + minutes_/60.0 # still here?! raise ValueError, "Hours not recognized, enter only a numeral (or decimal)" def getPreviewTitle(self, oldstatus, action): """ Get what the title of thread will be (via the web only) """ action = unicodify(ss(action)) statuses_and_verbs = self.getStatusesMerged(asdict=1) lowercase_values = {} statuses_and_verbs_reversed = {} for key, value in statuses_and_verbs.items(): lowercase_values[value.lower()] = value statuses_and_verbs_reversed[value] = key if lowercase_values.has_key(action): past_tense = statuses_and_verbs_reversed[lowercase_values[action]] else: action = 'addfollowup' if action == 'addfollowup': gentitle = 'Added Issue followup' else: gentitle = 'Changed status from %s to %s'%\ (oldstatus.capitalize(), past_tense.capitalize()) return gentitle def ExtensionForm(self, options): """ set some REQUEST variables to be used by form_followup. """ request = self.REQUEST # extract what we need from this caller templates options SubmitError = options.get('SubmitError') draft_followup_id = options.get('draft_followup_id', request.get('draft_followup_id')) request_action = unicodify(request.get('action','')).lower() draft_saved = options.get('draft_saved') if draft_followup_id: if not isinstance(draft_followup_id, basestring): raise ValueError, "draft_followup_id not a string (%r)" % draft_followup_id # take the action from the draft container = self.getDraftsContainer() if safe_hasattr(container, draft_followup_id): draft_object = getattr(container, draft_followup_id) if not request.get('action'): request.set('action', draft_object.action) if not request.get('comment'): request.set('comment', draft_object.getComment()) if not request.get('fromname'): request.set('fromname', draft_object.getFromname()) if not request.get('email'): request.set('email', draft_object.getEmail()) if not request.get('display_format'): request.set('display_format', draft_object.display_format) if request_action == 'delete': return self.form_delete(SubmitError=SubmitError) print "request_action", request_action if request_action == 'rejectassignment': otherTitle = "Reject issue assignment" request.set('otherActionTitle', otherTitle) request.set('otherComment', "Optional comment") request.set('otherAction', 'rejectassignment') elif request_action != 'addfollowup': title, action, comment = self._constructOtherTitles(self.status, request_action) if title: request.set('otherActionTitle', title) if action: request.set('otherAction', action) if comment: request.set('otherComment', comment) return self.form_followup(SubmitError=SubmitError, draft_followup_id=draft_followup_id, draft_saved=draft_saved) def _constructOtherTitles(self, issuestatus, action): """ return a suitable title for the action and verb """ issuestatus = issuestatus.lower() action = ss(action).replace(' ','') otherTitle = _(u"Added Issue Followup") otherAction = _(u"Add Followup") otherComment = u"" for status, verb in self.getStatusesMerged(aslist=1): if ss(verb).replace(' ','') == action: otherTitle = _(u'Change status from') + u' ' otherTitle += u'%s ' % issuestatus.capitalize() otherTitle += u'to %s' % status.capitalize() otherAction = verb.capitalize() otherComment = _(u"Optional comment") break return otherTitle, otherAction, otherComment def ManagerOptionsExtend(self): """ Determine which forms to show to Managers """ request = self.REQUEST has_key_special = self.has_key_special get_special_key = self.get_special_key do_preview = 0 if has_key_special('IssueAction') and has_key_special('previewissue'): do_preview = 1 form = '' if has_key_special('IssueAction') and not do_preview: action = get_special_key('IssueAction') return getattr(self,request['issueID']).ModifyIssue(action=action) elif has_key_special('IssueAction_AddFollowup'): request.set('no_quick_formfollowup',1) return self.form_followup() elif has_key_special('IssueAction_Delete'): return self.form_delete() else: issuestatus = self.status.lower() for item in self.getStatusesMerged(aslist=1): status, verb = item action = 'IssueAction_%s'%verb.replace(' ','').capitalize() if has_key_special(action): otherTitle = 'Change status from ' otherTitle += '%s '%issuestatus.capitalize() otherTitle += 'to %s'%status.capitalize() request.set('otherActionTitle', otherTitle) request.set('otherAction', verb.capitalize()) request.set('otherComment', 'Optional comment') request.set('whatAction', action) request.set('no_quick_formfollowup',1) return self.form_followup() else: return self.manager_options() def AnonymousOptionsExtend(self): """ Determine which forms to show to Anonymous """ request = self.REQUEST has_key_special = self.has_key_special get_special_key = self.get_special_key do_preview = 0 if has_key_special('IssueAction') and has_key_special('previewissue'): do_preview = 1 form = '' if has_key_special('IssueAction') and not do_preview: action = get_special_key('IssueAction') return getattr(self,request['issueID']).ModifyIssue(action=action) elif has_key_special('IssueAction_AddFollowup'): form = self.form_followup() request.set('no_quick_formfollowup',1) else: form = self.anonymous_options() return form def ModifyIssue(self, REQUEST, action='Addfollowup'): """ advanced change to issue properties """ request = self.REQUEST SubmitError = {} issueobject = self action = unicodify(ss(action)) oldstatus = self.status prefix = self.issueprefix comment = request.get('comment','').strip() if comment.endswith('
'): comment = comment[:-6].strip() while comment.endswith('

 

'): comment = comment[:-len('

 

')].strip() while comment.startswith('

 

'): comment = comment[len('

 

'):].strip() if not request.has_key('display_format'): saved_display_format = self.getSavedTextFormat() if saved_display_format: request.set('display_format', saved_display_format) else: request.set('display_format', self.getDefaultDisplayFormat()) req_manager = 1 # require manager by default addfollowup = 0 past_tense = None statuses_and_verbs = self.getStatusesMerged(asdict=1) lowercase_values = {} statuses_and_verbs_reversed = {} for key, value in statuses_and_verbs.items(): lowercase_values[value.lower()] = value statuses_and_verbs_reversed[value] = key if lowercase_values.has_key(action): past_tense = statuses_and_verbs_reversed[lowercase_values[action]] addfollowup = 1 elif action == 'delete': DeleteIssue(self, self.id) addfollowup = 0 redirect_url = request.URL2 elif action == 'addfollowup': addfollowup = 1 req_manager = 0 if req_manager and not self.hasManagerRole(): # the chosen action requires manager role, # but the person is not a manager self.login(self, request) if past_tense is not None and self.status != past_tense: self.status = past_tense else: if self.status == past_tense: action = 'addfollowup' REQUEST.set('action', action) if not comment: # oops! someone is accidently trying to set the status of this issue # even though it's already set and what's worse, they haven't entered # a comment. Then redirect them out like this... url = issueobject.absolute_url() count = self.countThreads() if count: url += '#i%s' % count return self.REQUEST.RESPONSE.redirect(url) # anything else means that the comment_description # cannot be empty if not Utils.SimpleTextPurifier(comment): err = "When submitting a followup you can not leave "\ "the description empty" SubmitError['comment'] = err elif self.containsSpamKeywords(comment, verbose=True): SubmitError['comment'] = _("Contains spam keywords") _invalid_name_chars = re.compile('|'.join([re.escape(x) for x in list('<>;\\')])) if _invalid_name_chars.findall(request.get('fromname','')): SubmitError['fromname'] = u'Contains not allowed characters' if _invalid_name_chars.findall(request.get('email','')): SubmitError['email'] = u'Contains not allowed characters' # check for spambots if self.useSpambotPrevention(): captcha_numbers = request.get('captcha_numbers','').strip() captchas_used = request.get('captchas') if isinstance(captchas_used, basestring): captchas_used = [captchas_used] if not captcha_numbers: m = _("Enter the numbers shown to that you are not a spambot") SubmitError['captcha_numbers'] = m else: errors = None for i, nr in enumerate(captcha_numbers): try: if int(nr) != int(self.captcha_numbers_map.get(captchas_used[i])): errors = True break except ValueError: errors = True break if errors: # use this oppurtunity to clean up what they tried to enter captcha_numbers = request.get('captcha_numbers','').strip() captcha_numbers = re.sub('[^\d]','', captcha_numbers).strip() request.set('captcha_numbers', captcha_numbers) m = _("Incorrect numbers matching") SubmitError['captcha_numbers'] = m else: self._rememberProvenNotSpambot() if REQUEST.get('fileattachment', []): fake_fileattachments = self._getFakeFileattachments(request.get('fileattachment')) if fake_fileattachments: m = "Filename entered but no file content" SubmitError['fileattachment'] = m if SubmitError: return self.ShowIssue(self, REQUEST, SubmitError=SubmitError) # most actions may perhaps add a little comment if addfollowup: randomid_length = self.randomid_length if randomid_length > 3: randomid_length = 3 genid = issueobject.generateID(randomid_length, prefix=prefix+'thread', meta_type=ISSUETHREAD_METATYPE, use_stored_counter=False) if action == 'addfollowup': gentitle = "Added Issue followup" else: gentitle = 'Changed status from %s to %s'%\ (oldstatus.capitalize(), past_tense.capitalize()) # fix variables acl_adder = None issueuser = self.getIssueUser() cmfuser = self.getCMFUser() zopeuser = self.getZopeUser() if issueuser: acl_adder = ','.join(issueuser.getIssueUserIdentifier()) elif zopeuser: path = '/'.join(zopeuser.getPhysicalPath()) name = zopeuser.getUserName() acl_adder = ','.join([path, name]) fromname = request.get('fromname','').strip() ckey = self.getCookiekey('name') if issueuser and issueuser.getFullname(): fromname = issueuser.getFullname() elif cmfuser and cmfuser.getProperty('fullname'): fromname = cmfuser.getProperty('fullname') elif not request.get('fromname') and self.has_cookie(ckey): fromname = self.get_cookie(ckey) elif request.get('fromname'): self.set_cookie(ckey, fromname) email = request.get('email','').strip() ckey = self.getCookiekey('email') if issueuser and issueuser.getEmail(): email = issueuser.getEmail() elif cmfuser and cmfuser.getProperty('email'): email = cmfuser.getProperty('email') elif not request.get('email') and self.has_cookie(ckey): email = self.get_cookie(ckey) elif request.get('email'): self.set_cookie(ckey, email) if request.get('display_format'): display_format = request.get('display_format') if issueuser: issueuser.setDisplayFormat(display_format) else: display_format = self.getDefaultDisplayFormat() if issueuser: if issueuser.getDisplayFormat(): display_format = issueuser.getDisplayFormat() # update Thread object title = gentitle threaddate = DateTime() # # Before we save the thread object, just make a duplication # check and if somehting found, redirect there. # duplicate_thread = self._check4Duplicate(title, comment) if duplicate_thread: url = issueobject.absolute_url() url += '#i%s' % self.countThreads() return self.REQUEST.RESPONSE.redirect(url) # create an Thread object create_method = issueobject._createThreadObject followupobject = create_method(genid, title, comment, threaddate, fromname, email, display_format, acl_adder) followupobject.index_object(idxs=['id','comment']) # update the parent issue self._updateModifyDate() # Also upload the fileattachments self._moveTempfiles(followupobject) # upload new file attachments if request.get('fileattachment', []): self._uploadFileattachments(followupobject, request.get('fileattachment')) self.nullifyTempfolderREQUEST() if request.form.get('draft_followup_id'): self._dropDraftThread(request.form.get('draft_followup_id')) if self.SaveDrafts(): # make sure there aren't any drafts that match # this recently added followupobject self._dropMatchingDraftThreads(followupobject) if not self.doDispatchOnSubmit() and followupobject.getEmail(): # Notifications aren't sent out immediately, that means that # there's a chance of a notification already exists inside # this issue that is designated to the submitter of this followup. self._removeUnsentNotifications(followupobject.getEmail()) if request.has_key('notify'): # now, create and email-alert-queue object # using filtered email address # get who to notify email_addresses = self.Others2Notify(do='email', emailtoskip=email) if len(email_addresses) > 0: self.sendFollowupNotifications(followupobject, email_addresses, gentitle) objectIds = issueobject.objectIds(ISSUETHREAD_METATYPE) redirect_url = '%s#i%s'%(issueobject.absolute_url(), len(objectIds)) # catalog issueobject.unindex_object() issueobject.index_object() # ready ! redirect! request.RESPONSE.redirect(redirect_url) def _removeUnsentNotifications(self, to_email): """ for all the unsent notification inside this issue, those that are designated to @to_email can be discarded. When doing this, if by removing this email from the notificaiton's list of emails, the list becomes empty then remove the notification object. The reason for doing this is that it's assumed that this @to_email has already participated in the issue and therefore don't need to be notified. """ def filter_function(notification, email): if not notification.isDispatched(): if email.lower() in [x.lower() for x in notification.getEmails()]: return True return False notifications = [x for x in self.getCreatedNotifications() if filter_function(x, to_email.lower())] del_notification_ids = [] for notification in notifications: new_emails_list = [x for x in notification.getEmails() if x.lower() != to_email.lower()] if new_emails_list: notification._setEmails(new_emails_list) else: del_notification_ids.append(notification.getId()) if del_notification_ids: self.manage_delObjects(del_notification_ids) security.declarePrivate('sendFollowupNotifications') def sendFollowupNotifications(self, followupobject, email_addresses, change): prefix = self.issueprefix # create id for notification mtype = NOTIFICATION_META_TYPE notifyid = self.generateID(4, prefix+"notification", meta_type=mtype, use_stored_counter=0) title = self.title issueID = self.id anchorname = len(self.objectIds(ISSUETHREAD_METATYPE)) emails = email_addresses date = DateTime() assert self.hasIssue(issueID), "This notification has no issue" notification_comment = followupobject.getCommentPure() notification = IssueTrackerNotification( notifyid, title, issueID, emails, followupobject.fromname, comment=notification_comment, anchorname=anchorname, change=change) self._setObject(notifyid, notification) notifyobject = getattr(self, notifyid) # use the dispatcher to try to send # this notification right now. # there is no big deal if the dispatcher crashes here # because the notification is saved and the dispatcher # can be invoked some other time manually if self.doDispatchOnSubmit(): try: self.dispatcher() except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, PROBLEM, 'Email could not be sent', error=sys.exc_info()) def _check4Duplicate(self, title, comment, fromname=None, email=None, email_message_id=None ): """ check if there is an exact replica of this issue """ # A duplication # is only a duplication if the last added thread is exactly the # same. Suppose some does this: # from Open -> Completed (comment fixed!) # from Completed -> Open (comment still doesn't work!) # from Open -> Completed (comment fixed!) # then the first and last threads are identical but the flow # is perfectly valid. # allthreads = self.ListThreads() for thread in allthreads: if thread.getTitle() == title and thread.getComment() == comment: # # looking like it's a duplicate. # # check for match on email_message_id if email_message_id and thread.getEmailMessageId(): if ss(email_message_id) == ss(thread.getEmailMessageId()): return thread # Before we return this # thread object, do a few pessimistic tests if fromname and ss(fromname) != ss(thread.getFromname()): continue if email and ss(email) != ss(thread.getEmail()): continue return thread # not a duplicate return None def _createThreadObject(self, id, title, comment, threaddate, fromname, email, display_format, acl_adder='', submission_type='', email_message_id=None): """ Crudely create thread object. No checking. """ thread = IssueTrackerIssueThread(id, title, comment, threaddate, fromname, email, display_format, acl_adder=acl_adder, submission_type=submission_type) self._setObject(id, thread) # get that object threadobject = getattr(self, id) if email_message_id: threadobject._setEmailMessageId(email_message_id) return threadobject security.declareProtected(ChangeIssuePermission, 'editIssueDetails') def editIssueDetails(self, sections=None, type=None, urgency=None, confidential=False, url2issue=None, estimated_time_hours=None, actual_time_hours=None, REQUEST=None): """ used post submission to change some of the smaller details. """ assert self.AllowIssueAttributeChange(), "Issue attribute change not enabled" #assert self.hasManagerRole() or self.isCreator(), "Not allowed to change this issue" # Section must be a list and might only allow for recognized values if sections is not None: if not isinstance(sections, list): sections = [sections] if not self.CanAddNewSections(): # check that all sections are recognized _options = self.getSectionOptions() for section in sections: assert section in _options # set it self.sections = sections # Type must be recognized if type is not None: ## I hate this variable name! assert type in self.getTypeOptions(), "Unrecognized issue type" # set it self.type = type # urgency must be recognized if urgency is not None: assert urgency in self.getUrgencyOptions(), "Unrecognized issue urgency" # set it self.urgency = urgency # because of the way forms work, the confidential boolean is never # present as False, so we have to assume that it is default. # That means that an issue being confidential is always set in # this method. # niceboolean() asserts we're returning a True or False self.confidential = Utils.niceboolean(confidential) if url2issue is not None: self.url2issue = url2issue.strip() if self.UseEstimatedTime() and estimated_time_hours is not None: hours = self._parseTimeHours(estimated_time_hours) assert isinstance(hours, float) self.estimated_time_hours = hours if self.UseActualTime() and actual_time_hours is not None: hours = self._parseTimeHours(actual_time_hours) assert isinstance(hours, float) self.actual_time_hours = hours self._updateModifyDate() # things have changed, so update its index self.reindex_object() if REQUEST is not None: # go back to the issue itself REQUEST.RESPONSE.redirect(self.absolute_url()) def isCreator(self): """ return true if the current user is logged in as the same user who created this issue. """ issueuser = self.getIssueUser() if issueuser: identifier = issueuser.getIssueUserIdentifier() identifier = ','.join(identifier) acl_adder = self.getACLAdder() return identifier == acl_adder return False def getCameFromSearchURL(self): """ if the previous page was a search, return the URL back to the same """ referer = self.REQUEST.get('HTTP_REFERER') if not referer: return None if referer.find(self.getRootURL()) == -1: return None if referer.find('?') > -1 and referer.split('?')[1].find('q') > -1: querystring = referer.split('?')[1] qs = cgi.parse_qs(querystring) if qs.has_key('q'): #q = qs.get('q')[0] return referer return None def getCameFromReportURL(self): """ if the previous page the user was on was a report, return the URL to that report. If it wasn't or the report can't be found return None. This is used on the ShowIssue page so that we can link back to the report they came from. """ referer = self.REQUEST.get('HTTP_REFERER') if not referer: return None if referer.find('/report-') > -1 and referer.startswith(self.getRootURL()): regex = r'/report-(.*?)$' found = re.findall(regex, referer) if found: reportid = found[0] container = self.getReportsContainer() if hasattr(container, reportid): root_url = self.getRoot().absolute_url_path() if root_url == '/': root_url = '' return '%s/%s/report-%s' % (root_url, self.whichList(), reportid) return None ## ## Drafts for followups ## def getMyThreadDrafts(self, issueid=None): """ return a list of issuethread draft objects """ if not self.SaveDrafts(): # don't even bother return [] ids = self._getDraftThreadIds() if not ids: return [] container = self.getDraftsContainer() objects = [] for id in ids: if hasattr(container, id): object = getattr(container, id) if object.meta_type == ISSUETHREAD_DRAFT_METATYPE: if issueid is None or object.issueid == issueid: objects.append(object) return objects def _getDraftThreadIds(self, separate=False): """ return the possible draft ids we have """ c_key = self.getCookiekey('draft_followup_ids') c_key = self.defineInstanceCookieKey(c_key) ids_cookie = self.get_cookie(c_key, '') ids_cookie = [x.strip() for x in ids_cookie.split('|') if x.strip()] issueuser = self.getIssueUser() ids_user = [] if issueuser: container = self.getDraftsContainer() all_draftobjects = container.objectValues(ISSUETHREAD_DRAFT_METATYPE) acl_adder = ','.join(issueuser.getIssueUserIdentifier()) for draft in all_draftobjects: if draft.getACLAdder()==acl_adder: ids_user.append(draft.getId()) # has the user a chosen one? chosen_id = self.REQUEST.get('draft_followup_id') if chosen_id: if chosen_id in ids_cookie: ids_cookie.remove(chosen_id) if chosen_id in ids_user: ids_user.remove(chosen_id) if separate: return Utils.uniqify(ids_cookie), Utils.uniqify(ids_user) else: return Utils.uniqify(ids_cookie+ids_user) security.declareProtected('View', 'DeleteDraftIssue') def DeleteDraftThread(self, id, REQUEST=None): """ delete this id from issue user or cookies and delete the draft issue object. """ ids_cookie, ids_user = self._getDraftThreadIds(separate=True) matched = False if id in ids_cookie: matched = True ids_cookie.remove(id) # save this c_key = self.getCookiekey('draft_followup_ids') c_key = self.defineInstanceCookieKey(c_key) all_draft_ids = '|'.join(ids_cookie) self.set_cookie(c_key, all_draft_ids, days=14) issueuser = self.getIssueUser() if id in ids_user and issueuser: matched = True if matched: # mark the draft issue as obsolete container = self.getDraftsContainer() container.manage_delObjects([id]) if REQUEST is not None: url = self.absolute_url() REQUEST.RESPONSE.redirect(url) def _dropDraftThread(self, id): """ remove this draft issuethread object if it exists """ container = self.getDraftsContainer() # remove potential client cookie ids_cookie, ids_user = self._getDraftThreadIds(separate=True) issueuser = self.getIssueUser() if id in ids_cookie: ids_cookie.remove(id) all_draft_ids = '|'.join(ids_cookie) c_key = self.getCookiekey('draft_followup_ids') c_key = self.defineInstanceCookieKey(c_key) self.set_cookie(c_key, all_draft_ids, days=14) # remove draft object if hasattr(container, id): container.manage_delObjects([id]) def _dropMatchingDraftThreads(self, thread): """ delete (if any) all thread drafts that match this particular followup thread object. """ title = thread.getTitle() comment = thread.getComment() container = self.getDraftsContainer() # the requirement for matching what to delete is if a draft matches # either: # - exactly on title and comment # - exactly on title, starts on comment for draft in container.objectValues(ISSUETHREAD_DRAFT_METATYPE): if not draft.getTitle() or not draft.getComment(): # odd draft! continue draft_title = unicodify(draft.getTitle()) draft_comment = unicodify(draft.getComment()) if draft_title == title and draft_comment == comment: self._dropDraftThread(draft.getId()) elif comment.startswith(draft_comment) and draft_title == title: self._dropDraftThread(draft.getId()) security.declareProtected('View', 'SaveDraftThread') def SaveDraftThread(self, REQUEST, draft_followup_id=None, prevent_preview=True, *args, **kw): """ basically just show AddIssue again except that we save a draft on the side. """ if prevent_preview: REQUEST.set('previewissue', False) _saver = self._saveDraftThread if self.SaveDrafts() and \ (\ (draft_followup_id is None and self._reason2saveDraft(REQUEST)) \ or \ draft_followup_id is not None \ ): action = REQUEST.get('action','') issueid = self.getId() draft_followup_id = _saver(issueid, action, REQUEST, draft_followup_id) kw['draft_followup_id'] = draft_followup_id kw['draft_saved'] = True return self.index_html(REQUEST, *args, **kw) security.declareProtected('View', 'AutoSaveDraftThread') def AutoSaveDraftThread(self, REQUEST, draft_followup_id=None, *args, **kw): """ Called by the Ajax script. Return the draft_followup_id that we create if so.""" _saver = self._saveDraftThread if self.SaveDrafts() and \ (\ (not draft_followup_id and self._reason2saveDraft(REQUEST)) \ or \ draft_followup_id \ ): action = REQUEST.get('action','') issueid = self.getId() draft_followup_id = _saver(issueid, action, REQUEST, draft_followup_id, is_autosave=True) return draft_followup_id else: return "" def getRecentOtherDraftThreadAuthor(self, only_fromname=False, max_age_seconds=20, min_timestamp=None, REQUEST=None): """ return the name of the author of the most recent draft followup that is not written by the current user. """ you_fromname = self.getSavedUser('fromname') you_email = self.getSavedUser('email') now = int(DateTime()) if REQUEST is not None: ct = 'text/html; charset=%s' % UNICODE_ENCODING REQUEST.RESPONSE.setHeader('Content-Type', ct) self.StopCache() container = self.getDraftsContainer() draftobjects = [x for x in container.objectValues(ISSUETHREAD_DRAFT_METATYPE) if x.getIssueId()==self.getIssueId()] if min_timestamp: # the @min_timestamp is possibly an integer timestamp of how old # the drafts have to be to be considered "recent". # Usually min_timestamp is set by the page template code which the # javascript picks up and sticks to. Then this timestamp is effectively # when the page was generate and thus the time the user came to it. draftobjects = [x for x in draftobjects if int(x.getModifyDate()) > int(min_timestamp)] draftobjects.sort(lambda x,y: cmp(y.getModifyDate(), x.getModifyDate())) fmt_followup = u"%s is working on a followup" for draft in draftobjects: if now - int(draft.getModifyDate()) > max_age_seconds: return None if draft.getFromname() != you_fromname and draft.getEmail() != you_email: if only_fromname and draft.getFromname(): return fmt_followup % draft.getFromname() elif only_fromname and draft.getEmail(): return fmt_followup % draft.getEmail() elif draft.getFromname() or draft.getEmail(): if draft.getFromname() and draft.getEmail(): name = self.ShowNameEmail(draft.getFromname(), draft.getEmail()) elif draft.getFromname(): name = draft.getFromname() else: name = draft.getEmail() return fmt_followup % name return None def getLatestDraftThreadAuthor(self, only_if_not_you=False): """ return the fullname of the latest draft thread author. If @only_if_not_you is true, then don't bother if the current user *is* the latest draft thread author. """ latest_draft = self._getLatestThreadDraft() if only_if_not_you: you_fromname = self.getSavedUser('fromname') you_email = self.getSavedUser('email') if latest_draft is not None: fromname = latest_draft.getFromname() email = latest_draft.getEmail() if only_if_not_you: if you_fromname == fromname and you_email == email: return '' else: return self.ShowNameEmail(fromname, email) else: return self.ShowNameEmail(fromname, email) return '' def _reason2saveDraft(self, request): """ no draft has been created. Inspect this 'request' see if there is reason enough to save a draft. """ enough_request_data = False for key in ('comment',): if Utils.SimpleTextPurifier(request.get(key,'')): enough_request_data = True break if enough_request_data: # check that a draft like this doesn't exist already _finder = self._findMatchingThreadDraft draft = _finder(request.get('comment','')) if draft: return False return enough_request_data def _findMatchingThreadDraft(self, comment): """ return drafts that match exactly. Return None if nothing found """ container = self.getDraftsContainer() draftobjects = container.objectValues(ISSUETHREAD_DRAFT_METATYPE) for draft in draftobjects: if draft.comment == comment: return draft return None def _getLatestThreadDraft(self): """ return the thread draft object that is the latest """ container = self.getDraftsContainer() draftobjects = list(container.objectValues(ISSUETHREAD_DRAFT_METATYPE)) draftobjects.sort(lambda x,y: cmp(y.getModifyDate(), x.getModifyDate())) try: return draftobjects[0] except IndexError: return None def _saveDraftThread(self, issueid, action, REQUEST, draft_followup_id=None, is_autosave=False): """ return the id this created """ draftscontainer = self.getDraftsContainer() if draft_followup_id: if not hasattr(draftscontainer, draft_followup_id): # you're lying! draft_followup_id = None if not draft_followup_id: # need to create a draft issuethread object _prefix = 'thread-%s-'%issueid # use the issue we're in as prefix id = self.generateID(5, prefix=_prefix, meta_type=ISSUETHREAD_DRAFT_METATYPE, incontainer=draftscontainer ) # create a draft issue draftthread = self._createDraftThread(id, issueid, action) draft_followup_id = id else: draftthread = getattr(draftscontainer, draft_followup_id) issueuser = self.getIssueUser() acl_adder = None if issueuser: acl_adder = ','.join(issueuser.getIssueUserIdentifier()) # now, populate this draftissue with as much data as # we can find modifier = draftthread.ModifyThread rget = REQUEST.get # only modify the thread draft if comment, fromname or email has changed if draftthread.comment==rget('comment') and draftthread.fromname == rget('fromname') \ and draftthread.email == rget('email'): # the thread hasn't changed enough, no need to call ModifyThread() # again which would be a waste of ZODB transactions and would also # mean that the getModifyDate() of the draft thread would be # set again. return draft_followup_id modifier(title=rget('title'), comment=rget('comment'), fromname=rget('fromname'), email=rget('email'), acl_adder=acl_adder, display_format=rget('display_format', self.getSavedTextFormat()), is_autosave=is_autosave, ) # remember this issueuser = self.getIssueUser() if not issueuser: # stick this in a cookie c_key = self.getCookiekey('draft_followup_ids') c_key = self.defineInstanceCookieKey(c_key) all_draft_ids = self._getDraftThreadIds() if draft_followup_id not in all_draft_ids: all_draft_ids.append(draft_followup_id) all_draft_ids = '|'.join(all_draft_ids) self.set_cookie(c_key, all_draft_ids, days=14) # also save, the name if we didn't already have it if rget('fromname') and not self.getSavedUser('fromname', use_request=False): self.set_cookie(self.getCookiekey('name'), rget('fromname')) if rget('email') and not self.getSavedUser('email', use_request=False): self.set_cookie(self.getCookiekey('email'), rget('email')) return draft_followup_id def _createDraftThread(self, id, issueid, action): """ create a draftissuethread and return it """ root = self.getDraftsContainer() title = None if action == 'AddFollowup': title = _("Followup") else: title = action inst = IssueTrackerDraftIssueThread(id, issueid, action, title=title) root._setObject(id, inst) object = root._getOb(id) return object ## Changing the issue in mid-air def Others2Notify(self, format='email', emailtoskip=None, requireemail=False, do=None # legacy parameter ): """ Returns a list of names and emails of people to notify if this issue changes or gets a followup. Take the hide_me and confidential factor into consideration. if format == email: return [foo@bar.com, bar@foo.com,...] elif format == name: return [Foo, Bar, ...] elif format == both: return ['Foo ', ...] elif format == merged: return [self.ShowNameEmail(Foo, foo@bar.com),...] """ if emailtoskip is None: # caller was lazy not to specify this # we do it here in the code issueuser = self.getIssueUser() if issueuser: emailtoskip = issueuser.getEmail() elif self.REQUEST.get('email'): emailtoskip = self.REQUEST.get('email') elif self.has_cookie(self.getCookiekey('email')): emailtoskip = self.get_cookie(self.getCookiekey('email')) all = [] nameemailshower = lambda n,e: self.ShowNameEmail(n, e, highlight=0) names_and_emails = self._getOthers(emailtoskip) for _name, _email in names_and_emails: add = '' if emailtoskip is not None and ss(_email) == ss(emailtoskip): continue # skip it! if requireemail and not Utils.ValidEmailAddress(_email): continue # skip it! if format == 'email': add = _email or _name elif format == 'name': add = _name or _email else: if _name and _email: if format == 'both': add = "%s <%s>"%(_name, _email) else: add = nameemailshower(_name, _email) elif _name: if format == 'both': add = _name else: add = _name elif _email: if format == 'both': add = _email else: add = nameemailshower(_email, _email) if add and add not in all: all.append(add) return all def _getOthers(self, avoidemail=None): """ return a 2D list of names and emails that _should_ be notified """ all = [] # what we will return allemails = {} # avoid duplicates # 1. The issue at hand. # proceed only if Manager or open-name on the issue if self.hasManagerRole() or not self.hide_me: # self.email is the email of the thread # emailtoskip comes from the param/cookie if applicable issue_email = self.getEmail() if issue_email and issue_email != avoidemail: issue_fromname = self.getFromname() item = [issue_fromname, issue_email] all.append(item) allemails[ss(issue_email)] = 1 # 2. All authors of followups/threads for thread in self.objectValues(ISSUETHREAD_METATYPE): thread_email = thread.getEmail() if thread_email and thread_email != avoidemail: ss_thread_email = ss(thread_email) if not allemails.has_key(ss_thread_email): thread_fromname = thread.getFromname() item = [thread_fromname, thread_email] all.append(item) allemails[ss_thread_email] = 1 # 3. Get all subscribers notifyables = self.getNotifyablesEmailName() for subscriber in self.getSubscribers(): if notifyables.has_key(subscriber): # Name Email item = [notifyables[subscriber], subscriber] ss_subscriber = ss(subscriber) if not allemails.has_key(ss_subscriber): all.append(item) allemails[ss_subscriber] = 1 elif len(subscriber.split(','))==2: # quite possibly an acl user ufpath, name = subscriber.split(',') try: uf = self.unrestrictedTraverse(ufpath) except KeyError: continue if uf.meta_type == ISSUEUSERFOLDER_METATYPE: issueuserobj = uf.data[name] ss_email = ss(issueuserobj.getEmail()) if not allemails.has_key(ss_email): item = [issueuserobj.getFullname(), issueuserobj.getEmail()] all.append(item) allemails[ss_email] = 1 elif Utils.ValidEmailAddress(subscriber): ss_subscriber = ss(subscriber) if not allemails.has_key(ss_subscriber): all.append(['', subscriber]) allemails[ss_subscriber] = 1 del allemails return all def getOptionButtons(self): """ Return list of dicts of actions and verbs """ res=[] def url_quote_unicodeaware(s): # if the string is a unicode string, # first convert it to a non-unicode string # then apply url_quote. if isinstance(s, unicode): return Utils.url_quote(s.encode(UNICODE_ENCODING)) else: return Utils.url_quote(s) issuestatus = self.status.lower() for item in self.getStatusesMerged(aslist=1): status, verb = item if issuestatus != status.lower(): action = verb.replace(' ','').capitalize() action_quoted = url_quote_unicodeaware(action) #res.append([action, verb.capitalize()]) res.append({'action':action, 'verb':verb.capitalize(), 'action_quoted':action_quoted}) res.append({'action':u'Delete', 'verb':u'Delete', 'action_quoted':url_quote_unicodeaware('Delete')}) return res def StatusByWhom(self): """ return who was responsible for the latest status this issue has """ threads = self.ListThreads() if threads: last_thread = threads[-1] fromname = last_thread.getFromname() if not fromname: fromname = last_thread.getEmail() status = self.status.capitalize() return "%s by %s" % (status, fromname) else: fromname = self.getFromname() if not fromname: fromname = self.getEmail() return "Added by %s" % fromname def getBriefedDescription(self, length=100): """ return some of the text """ hq = self.HighlightQ # shortcut description = self.getDescriptionPure() if len(description) <= length: d = description elif description.strip().find(' ') == -1: d = description[:length].strip() else: try: description = description[:length].strip() while description[-1] not in [' ','\n']: description = description[:-1] d = description + '...' except: d = description[:length] if isinstance(d, str): return self.HighlightQ(Utils.html_entity_fixer(Utils.safe_html_quote(d))) else: return self.HighlightQ(Utils.safe_html_quote(d)) def showAdditionalInformation(self, usebrackets=0, timedifference=None): """ returns a string of information about the issue. Zeroth (if timedifference is not None) the time difference First isConfidential, Second # files Third # comments """ info = [] if timedifference is not None: if timedifference: info.append(str(timedifference)) if self.isConfidential(): info.append("confidential") count_files = self.countFileattachments() if count_files: if count_files == 1: info.append('%s file'%count_files) else: info.append('%s files'%count_files) count_comments = self.countThreads() if count_comments: if count_comments == 1: info.append('%s comment'%count_comments) else: info.append('%s comments'%count_comments) assignments = self.getAssignments() if assignments: try: if assignments[-1].getState() == 1: info.append('assigned') elif assignments[-1].getState() == 0: info.append('reassigned') except AttributeError: pass if info: if usebrackets: return "(%s)"%(', '.join(info)) else: return ', '.join(info) else: return "" def hasThreads(self): """ return true if there are issuethreads within """ return not not self.ListThreads() def countThreads(self): """ return the number of threads within """ return len(self.ListThreads()) def getThreadObjects(self): """ Return the followup objects. The threads. """ return self.objectValues(ISSUETHREAD_METATYPE) listThreads = ListThreads = getThreadObjects # better name def getLastThread(self): """ Return the last thread in an issue or None """ allthreads = self.ListThreads() if allthreads: return allthreads[-1] else: return None def countFileattachments(self): """ return [no files in issue, no files in threads] """ return len(self.ZopeFind(self, obj_metatypes=['File'], search_sub=1)) def filenames(self): """ return all the filenames of this issue splitted """ files = self.objectValues('File') all = [] for file in files: all.extend(Utils.filenameSplitter(file.getId())) return Utils.uniqify([x.lower() for x in all]) def manage_beforeDelete(self, REQUEST=None, RESPONSE=None): """ uncatalog yourself """ for thread in self.objectValues(ISSUETHREAD_METATYPE): thread.unindex_object() self.unindex_object() def index_object(self, idxs=['id','title','description', 'fromname','email','url2issue', 'meta_type']): """A common method to allow Findables to index themselves.""" path = '/'.join(self.getPhysicalPath()) catalog = self.getCatalog() # because the ZCatalog might not yet have the # 'filenames' KeywordIndex we can't catalog this object # with that index. # Performing the following check every time takes # time so by 2007 this whole if statement below can probably # be removed because by then, must people will have updated # their issuetrackers to enable the new 'filenames' # KeywordIndex indexes = catalog._catalog.indexes # NB. This rather odd if statement magic is due to some obscure # but filed by someone called pradeep on # http://real.issuetrackerproduct.com/0269 # I don't know how 'filenames' can get into idxs if when this # index_object() function is called from reindex_object if 'filenames' in idxs: if not indexes.has_key('filenames'): idxs.remove('filenames') msg = "'filenames' KeywordIndex missing "\ "but added as parameter. "\ "Press Update Everything button." logger.info(msg) else: if indexes.has_key('filenames'): idxs.append('filenames') else: msg = "'filenames' KeywordIndex missing. "\ "Press Update Everything button" logger.info(msg) catalog.catalog_object(self, path, idxs=idxs) def getTitle_idx(self): return self.getTitle() def getFromname_idx(self): return self.getFromname() def getDescription_idx(self): return self.getDescription() def unindex_object(self): """A common method to allow Findables to unindex themselves.""" path = '/'.join(self.getPhysicalPath()) self.getCatalog().uncatalog_object(path) def isImplicitlySubscribing(self, email): """ return if they're already involved in this issue such that there is no point for them becoming a subscriber """ # was it you who added this issue? issue_email = self.getEmail() if email and issue_email == email: return True # have you made any kind of followup? for thread in self.ListThreads(): if email and thread.getEmail() == email: return True return False # fallback def isSubscribing(self, email): """ check if in subscribers list """ subscribers = self.getSubscribers() email = ss(str(email)) return email in [ss(x) for x in subscribers] def getSubscribers(self): """ return subscribers list """ return getattr(self, 'subscribers', []) def _addSubscriber(self, name_or_email): """ add subscriber to subscribers list """ subscribers = self.getSubscribers() email = None name_or_email = name_or_email.strip() if Utils.ValidEmailAddress(name_or_email): email = name_or_email elif len(name_or_email.split(','))==2: # it's a acl user email = name_or_email else: # what we're adding is not an email, # expect it to be the name of a notifyable ss_name_or_email = ss(name_or_email) for notifyable in self.getNotifyables(): if ss_name_or_email == ss(notifyable.getName()): email = notifyable.getEmail() break if email is not None: if not self.isSubscribing(email): if isinstance(subscribers, tuple): subscribers = list(subscribers) subscribers.append(email) self.subscribers = subscribers return True return False def _delSubscriber(self, email): """ remove item from subscribers list """ n_subscribers = [] ss_email = ss(email) for subscriber in self.getSubscribers(): if ss_email != ss(subscriber): n_subscribers.append(subscriber) self.subscribers = n_subscribers def Subscribe(self, subscriber, unsubscribe=0, REQUEST=None): """ analyse subscriber and add accordingly """ if subscriber == 'issueuser': issueuser = self.getIssueUser() assert issueuser, "Not logged in as Issue User" identifier = issueuser.getIssueUserIdentifierString() if unsubscribe: self._delSubscriber(identifier) else: self._addSubscriber(identifier) else: emails = self.preParseEmailString(subscriber, aslist=1) for email in emails: if unsubscribe: self._delSubscriber(email) else: self._addSubscriber(email) # Exceptional case. If the user doesn't already have an email # address, but has asked to subscribe with only one email # address, then we can assume that that is the email address he wants # to use all the time. if not self.getSavedUser('email') and len(emails)==1: if Utils.ValidEmailAddress(emails[0]): # add this self.set_cookie(self.getCookiekey('email'), emails[0]) if REQUEST is not None: url = self.absolute_url() + '/' REQUEST.RESPONSE.redirect(url) def getFriends(self, maxlength=13): """ find other people who use this issuetracker and return a list of dicts with keys 'name', 'email', 'show', 'notifyable'. Avoid assignment blacklisted people. Search current issue. Search local acl_folders if Issue User Folder. Search Adjacent issues and their threads. """ friends = [] friends_emails = [] try: maxlength = abs(int(maxlength)) except ValueError: maxlength = 13 # 0. Who are you? issueuser = self.getIssueUser() if issueuser: _email_ss = Utils.ss(issueuser.getEmail()) else: _email_ss = Utils.ss(self.getSavedUserEmail()) if _email_ss: friends_emails.append(_email_ss) # 1. Search current issue if self.meta_type == ISSUE_METATYPE: _email = self.getEmail() if _email and Utils.ss(_email) not in friends_emails and Utils.ValidEmailAddress(_email): _email_ss = Utils.ss(_email) _name = self.getFromname() _show = self.ShowNameEmail(_name, _email, highlight=False) item = {'name':_name, 'email':_email, 'show':_show} friends.append(item) friends_emails.append(_email_ss) for thread in self.ListThreads(): _email = thread.getEmail() _email_ss = Utils.ss(_email) if _email_ss not in friends_emails and Utils.ValidEmailAddress(_email): _name = thread.getFromname() _show = self.ShowNameEmail(_name, _email, highlight=False) item = {'name':_name, 'email':_email, 'show':_show} friends.append(item) friends_emails.append(_email_ss) # 2. list of acl identifiers # filter=true removes all assignment blacklisted people all_users = self.getAllIssueUsers(filter=1) for each in all_users: user = each['user'] _email = user.getEmail() _name = user.getFullname() _email_ss = Utils.ss(_email) _show = self.ShowNameEmail(_name, _email, highlight=False) if _email_ss not in friends_emails and Utils.ValidEmailAddress(_email): item = {'name':_name, 'email':_email, 'show':_show } friends.append(item) friends_emails.append(_email_ss) # 3. all issues, all threads if len(friends) >= maxlength: issues = [] else: issues = self.ListIssuesFiltered(skip_filter=True, keep_sortorder=False, sortorder='modifydate', reverse=False) for issue in issues: _email = issue.getEmail() _email_ss = Utils.ss(_email) if _email_ss not in friends_emails and Utils.ValidEmailAddress(_email): _name = issue.getFromname() _show = self.ShowNameEmail(_name, _email, highlight=False) item = {'name':_name, 'email':_email, 'show':_show} friends.append(item) friends_emails.append(_email_ss) for thread in self.ListThreads(): _email = thread.getEmail() _email_ss = Utils.ss(_email) if _email_ss not in friends_emails: _name = thread.getFromname() _show = self.ShowNameEmail(_name, _email, highlight=False) item = {'name':_name, 'email':_email, 'show':_show} friends.append(item) friends_emails.append(_email_ss) if len(friends) > maxlength: break friends = friends[:maxlength] # which email address can be replaced with a single name? all_notifyables = self.getNotifyables() emails2names = {} for noti in all_notifyables: emails2names[noti.getEmail()] = noti.getName() checked = [] for friend in friends: if emails2names.has_key(friend['email']): #friend['show'] = emails2names.get(friend['email']) friend['notifyable'] = True else: friend['notifyable'] = False checked.append(friend) friends = checked return friends security.declareProtected('View', 'TellAFriend') def TellAFriend(self, email_string, friends=[], ignoreword='', added=False, send=True, message_sender='', cancel=False): """ Allows people to send notifications to other people """ if not self.UseTellAFriend(): return "Tell a friend feature disabled" if cancel: self.REQUEST.RESPONSE.redirect(self.absolute_url()) return email_string = email_string.strip() if (not email_string or email_string == ignoreword) and send: if not friends: self.REQUEST.RESPONSE.redirect(self.absolute_url()) issueuser = self.getIssueUser() if issueuser: fromname = issueuser.getFullname() email = issueuser.getEmail() else: # look in cookies fromname = self.getSavedUserName() email = self.getSavedUserEmail() if fromname and Utils.ValidEmailAddress(email): fr = "%s <%s>"%(fromname, email) elif Utils.ValidEmailAddress(email): fr = email else: _f, _e = self.getSitemasterName(), self.getSitemasterEmail() fr = "%s <%s>"%(_f, _e) _roottitle = self.getRoot().getTitle() subject = "%s: An issue to your attention"%_roottitle if message_sender: msg = message_sender else: msg = self._constructTellAFriendMessage(fromname) if send: # first we need to figure out who to send to to = self.preParseEmailString(email_string, aslist=True) params = {} if self.REQUEST.get('NewIssue'): params['NewIssue'] = self.REQUEST.get('NewIssue') if to or friends: to_strings = [x.strip() for x in to] both = to_strings+friends both = ', '.join(both) #body = '\r\n'.join(['From: %s'%fr, 'To: %s'%both, # 'Subject: %s'%subject, "", msg]) try: self.sendEmail(msg, both, fr, subject, swallowerrors=False) params['tellafriend'] = "Success. Message sent!" params['good'] = 1 except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, PROBLEM, "Message could not be sent", error=sys.exc_info()) params['tellafriend'] = "Failure. Message could not be sent" params['bad'] = 1 try: params.pop('NewIssue') except AttributeError: try: params = Utils.dict_popper(params, 'NewIssue')[1] except KeyError: pass except KeyError: pass else: params['tellafriend'] = "Failed because no one to send to" params['bad'] = 1 try: params.pop('NewIssue') except AttributeError: try: params = Utils.dict_popper(params, 'NewIssue')[1] except KeyError: pass except KeyError: pass url = self.absolute_url() if message_sender and len(message_sender) < 5000: params['message_sender'] = message_sender #params['MoreEmailOptions'] = 1 url = Utils.AddParam2URL(url, params) + '#details' self.REQUEST.RESPONSE.redirect(url) else: return msg def _constructTellAFriendMessage(self, fromname): """ create a suggested TellAFriendMessage """ _roottitle = self.getRoot().getTitle() br = '\r\n' if fromname: msg = fromname + " has asked you to have a look at an issue in %s " \ "with the following title:"%_roottitle + br else: msg = "You have been asked to have a look at an issue in %s "\ "with the following title:"%_roottitle + br _title = self.getTitle() if _title.find('"') > -1: msg += " '%s' "%_title else: msg += ' "%s" '%_title msg += 2*br + "The issue can be found at"+br msg += self.absolute_url() msg += 3*br # signature signature = self.showSignature() if signature: msg += "--" + br +signature return msg def getDefaultTellAFriendMessage(self, added=False): """ wrap TellAFriend() to be able to extract only the message """ msg = self.TellAFriend('', added=added, send=False) return msg ## Issue Assignment security.declareProtected('View', 'changeAssignment') def changeAssignment(self, assignee, send_email=False): """ add a new assignee to the issue object only if the current assignee is the user who calls this. """ if isinstance(send_email, basestring): send_email = Utils.niceboolean(send_email) assignments = self.getAssignments() lastone = assignments[-1] if lastone.isYou() or self.hasManagerRole(): if not lastone.getACLAssignee() == assignee: self.createAssignment(assignee, state=0, send_email=send_email) url = self.absolute_url() self.REQUEST.RESPONSE.redirect(url) security.declareProtected('View', 'setAssignment') def setAssignment(self, assignee, send_email=False): """ add a new assignee to the issue object only if the current assignee is the user who calls this. """ if isinstance(send_email, basestring): send_email = Utils.niceboolean(send_email) assignments = self.getAssignments() if assignments: state = 0 else: state = 1 #lastone = assignments[-1] if self.hasManagerRole(): self.createAssignment(assignee, state=state, send_email=send_email) url = self.absolute_url() self.REQUEST.RESPONSE.redirect(url) def _getAssignments(self): """ return the issues assignment objects unsorted """ return self.objectValues(ISSUEASSIGNMENT_METATYPE) def getAssignments(self, sort=True): """ return the issue assignment objects sorted """ objects = self._getAssignments() if sort: objects = self.sortSequence(objects, (('assignmentdate',),)) return objects def getFirstAssignment(self): """ return the first assignment or None """ try: return self.getAssignments()[0] except IndexError: return None def createAssignment(self, assignee_identifier, state=1, send_email=False): """ create an Issue Assignment """ request = self.REQUEST prefix = self.issueprefix + 'assignment' mtype = ISSUEASSIGNMENT_METATYPE id = self.generateID(4, prefix=prefix, meta_type=mtype, use_stored_counter=False) # check that the assignee_identifier exits try: userfolderpath, name = assignee_identifier.split(',') except ValueError: m = "Invalid assignee identifier (%s)" raise AssigneeNotFoundError, m % assignee_identifier userfolder = self.unrestrictedTraverse(userfolderpath) if name in userfolder.user_names(): user = self.getIssueUserObject(assignee_identifier) else: m = "Invalid assignee identifier (%s)" raise AssigneeNotFoundError, m % assignee_identifier acl_adder = '' issueuser = self.getIssueUser() if issueuser: acl_adder = ','.join(issueuser.getIssueUserIdentifier()) email_cookiekey = self.getCookiekey('email') name_cookiekey = self.getCookiekey('name') if issueuser and issueuser.getEmail(): email = issueuser.getEmail() elif not request.get('email') and self.has_cookie(email_cookiekey): email = self.get_cookie(email_cookiekey) else: email = request.get('email','') if issueuser and issueuser.getFullname(): fromname = issueuser.getFullname() elif not request.get('fromname') and self.has_cookie(name_cookiekey): fromname = unicodify(self.get_cookie(name_cookiekey)) else: fromname = unicodify(request.get('fromname','')) # Add it adder = self._createAssignmentObject assignment = adder(id, assignee_identifier, state, fromname, email, acl_adder) if send_email and Utils.ValidEmailAddress(user.getEmail()) \ and not Utils.ss(user.getEmail())==Utils.ss(email): self._sendAssignmentNotification(assignment, user.getEmail()) #self._sendAssignementEmail(user.getFullname(), user.getEmail(), # fromname, email) #assignment._setEmailSent() def _createAssignmentObject(self, id, identifier, state, fromname, email, acl_adder=None): """ create the actual Issue Assignment object """ instance = IssueTrackerIssueAssignment(id, identifier, state, fromname, email, acl_adder) self._setObject(id, instance) assignment = self._getOb(id) return assignment def _sendAssignementEmail(self, to_name, to_email, from_name, from_email): """ Send a simple email to he who was assigned this issue """ raise DeprecatedError, "We're using _sendAssignmentNotification() instead" def _sendAssignmentNotification(self, assignment, to_email): """ create a notification object about this new assignment object """ # the person who "created" the assignment fromname = assignment.getFromname() email = assignment.getEmail() issue = aq_parent(aq_inner(assignment)) notifyid = self.generateID(5, self.issueprefix+"notification", meta_type=NOTIFICATION_META_TYPE, use_stored_counter=False, incontainer=issue) title = issue.getTitle() issueID = issue.getId() date = DateTime() notification = IssueTrackerNotification(notifyid, title, issue.getId(), [to_email], assignment=assignment.getId() ) issue._setObject(notifyid, notification) notifyobject = getattr(issue, notifyid) if self.doDispatchOnSubmit(): if 1: #try: self.dispatcher([notifyobject]) else: #except: try: err_log = self.error_log err_log.raising(sys.exc_info()) except: pass LOG(self.__class__.__name__, PROBLEM, 'Email could not be sent', error=sys.exc_info()) ## ## The issue's notifications ## def getCreatedNotifications(self, sort=False): objects = list(self._getCreatedNotifications()) if sort: objects.sort(lambda x, y: cmp(x.date, y.date)) return objects def _getCreatedNotifications(self): return self.objectValues(NOTIFICATION_META_TYPE) ## ## Misc. ## def getNotifiedUsersByNotificationsAndAssignment(self): """ return a list of of strings of people who will be notified when this issue was submitted. """ strings = [] _all_emails = [] notifications = self.getCreatedNotifications() first_assignment = self.getFirstAssignment() if first_assignment is not None: assignee_name = first_assignment.getAssigneeFullname() assignee_email = first_assignment.getAssigneeEmail() add = self.ShowNameEmail(assignee_name, assignee_email, highlight=False) strings.append(add) _all_emails.append(assignee_email.lower()) always = self.getAlwaysNotify() checked = [self._checkAlwaysNotify(x, format='list') for x in always] # checked is a list of tuples that look like this: # [(True, ['', 'peterbe@gmail.com']), ...] # And this can be used to help us figure out the names of the emails # stored in the notification objects. emails2names = {} for valid, name_email in checked: if not valid: continue n, e = name_email if n: emails2names[e.lower()] = n.strip() for notification in notifications: for email in notification.getEmails(): name = emails2names.get(email.lower(), '') add = self.ShowNameEmail(name, email, highlight=False) if email.lower() not in _all_emails: strings.append(add) _all_emails.append(email.lower()) return strings zpts = ({'f':'zpt/ShowIssue', 'optimize':OPTIMIZE and 'xhtml'}, 'zpt/OptionButtons', 'zpt/tell_a_friend', 'zpt/form_followup', 'zpt/quick_form_followup', 'zpt/subscription', #'zpt/manager_options', #'zpt/anonymous_options', 'zpt/form_delete', 'zpt/followup_preview', ) addTemplates2Class(IssueTracker, zpts, extension='zpt') InitializeClass(IssueTrackerIssue) #---------------------------------------------------------------------------- class IssueTrackerDraftIssue(IssueTrackerIssue): """ These are used for the 'Save as draft' feature. It's like the regular Issue objects except that it doesn't get cataloged. """ __implements__ = (WriteLockInterface,) meta_type = ISSUE_DRAFT_METATYPE icon = '%s/issuedraft.gif'%ICON_LOCATION security=ClassSecurityInfo() manage_options = ( {'label':'Contents', 'action':'manage_main'}, {'label':'Properties', 'action':'manage_draftissue_properties'} ) def __init__(self, id, title=None, status=None, issuetype=None, urgency=None, sections=None, fromname=None, email=None, url2issue=None, confidential=None, hide_me=None, description=None, display_format=None, acl_adder=None, assignee_identifier=None, is_autosave=False, Tempfolder_fileattachments=None): """ init an Issue object """ self.id = str(id) self.title = unicodify(title) self.modifydate = DateTime() self.status = status self.type = issuetype self.urgency = urgency self.sections = sections self.fromname = unicodify(fromname) self.email = email self.url2issue = url2issue self.confidential = confidential self.hide_me = hide_me self.description = unicodify(description) self.display_format = display_format self.acl_adder = acl_adder self.is_autosave = not not is_autosave self.Tempfolder_fileattachments = Tempfolder_fileattachments self.assignee_identifier = assignee_identifier # legacy support is_autosave = False def index_object(self, *args, **kws): """A common method to allow Findables to index themselves.""" pass def unindex_object(self): """A common method to allow Findables to unindex themselves.""" pass def manage_afterAdd(self, REQUEST, RESPONSE): """ the base class defines this to prerender the description, something we don't want to do. """ pass def getIssueDate(self): """ does not apply to drafts, so use modify date instead """ return self.getModifyDate() def isAutosave(self): return self.is_autosave def shortDescription(self, maxlength=55, html=True): """ return a simplified description where the title is shown and then as much of the description as possible. """ title = self.getTitle() if not title.strip(): if html: title = "(No subject)" else: title = "(No subject)" desc = self.getDescriptionPure() shortened = self.lengthLimit(title, maxlength, "|...|") if shortened.endswith('|...|'): # the title was shortened shortened = shortened[:-len('|...|')] if html: return "%s..."%shortened else: return shortened+'...' else: # i.e. title==shortened # put some of the description ontop if len(shortened) + len(desc) > maxlength: desc = self.lengthLimit(desc, maxlength-len(title)) if html: return "%s, %s"%(shortened, desc) else: return "%s, %s"%(shortened, desc) def hasThreads(self): """ return true if there are issuethreads within """ return False def ListThreads(self): """ Return the followup objects. The threads. """ return [] def manage_beforeDelete(self, REQUEST=None, RESPONSE=None): """ uncatalog yourself """ pass def getAssignments(self): """ return the issue assignment objects sorted """ return [] security.declareProtected('View', 'index_html') def index_html(self, REQUEST=None): """ does not apply """ return "Drafts do not have a view part " def ModifyIssue(self, title=None, description=None, status=None, type=None, urgency=None, sections=None, fromname=None, email=None, url2issue=None, confidential=None, hide_me=None, display_format=None, acl_adder=None, assignee_identifier=None, is_autosave=False, Tempfolder_fileattachments=None, REQUEST=None): """ Since drafts cannot have threads, we can't use the default ModifyIssue() which creates thread objects. """ if title is not None: self.title = unicodify(title) if description is not None: self.description = unicodify(description) if status is not None: self.status = status if type is not None: self.type = type if urgency is not None: self.urgency = urgency if sections is not None: if not isinstance(sections, list): if isinstance(sections, tuple): sections = list(sections) else: sections = [sections] self.sections = sections if fromname is not None: self.fromname = unicodify(fromname) if email is not None: self.email = email if url2issue is not None: self.url2issue = url2issue if confidential is not None: self.confidential = confidential if hide_me is not None: self.hide_me = hide_me if display_format is not None and display_format in self.display_formats: self.display_format = display_format if acl_adder is not None: self.acl_adder = acl_adder if assignee_identifier is not None: self.assignee_identifier = assignee_identifier self.is_autosave = not not is_autosave if Tempfolder_fileattachments is not None: self.Tempfolder_fileattachments = Tempfolder_fileattachments self.modifydate = DateTime() def isAutosave(self): """ return if this was saved as an autosave or a plain draft """ return self.is_autosave def get__dict__keys(self): """ return the names of the keys we might have """ return ('title', 'description', 'status', 'type', 'urgency', 'sections', 'fromname', 'email', 'url2issue', 'confidential', 'hide_me', 'display_format', 'acl_adder', 'assignee_identifier', 'is_autosave', 'Tempfolder_fileattachments') def get__dict__nicely(self): """ same as get__dict__keys() but we wrap it nicely """ ok = [] for key in self.get__dict__keys(): if self.__dict__.get(key, None) is not None: ok.append({'key':key, 'value':self.__dict__.get(key)}) return ok def populateREQUEST(self, request): """ put all of this information in a request object (dict like) """ for key in self.get__dict__keys(): if self.__dict__.get(key): request.set(key, self.__dict__.get(key)) # # External editor # #security.declareProtected('Use External Editor', 'manage_FTPget') security.declareProtected('View', 'manage_FTPget') def manage_FTPget(self, REQUEST, RESPONSE): """ get source for FTP download for ExternalEditor """ self.REQUEST.RESPONSE.setHeader('Content-Type','text/plain') out = "[Subject]\n%s\n"%self.title out += "[Description]\n%s\n\n"%self.description issueuser = self.getIssueUser() if not issueuser: out += "[Name] %s\n"%self.fromname out += "[Email] %s\n"%self.email fmt = self.display_format fmt = fmt.replace('plaintext',"Plain as it's written") fmt = fmt.replace('structuredtext', "StructuredText") out += "[Display format] %s\n"%fmt out += "\n" sections = self.sections if sections is None: sections = [] out += "[Sections]\n%s\n\n"%("\n".join(sections)) out += "[Type] %s\n"%self.type out += "[Urgency] %s\n"%self.urgency out += "[URL] %s\n"%self.url2issue #out += "[Assign to]\n%s\n\n"% return out def get_size(self): """ Used for FTP and ZMI """ return len(self.manage_FTPget()) #security.declareProtected('Use External Editor', 'PUT') security.declareProtected('View', 'PUT') def PUT(self, REQUEST, RESPONSE): """ handle the HTTP PUT requests for ExternalEditor """ self.dav__init(REQUEST, RESPONSE) self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1) if not REQUEST.get('BODY'): if transaction is None: get_transaction.abort() else: transaction.get().commit() RESPONSE.setStatus(405) else: body = REQUEST.get('BODY') info = None try: if body: info = self._parseExternalEditBody(body) except: LOG(self.__class__.__name__, ERROR, "failed in _parseExternalEditBody", error=sys.exc_info()) if info is None: get_transaction.abort() RESPONSE.setStatus(405) else: apply(self.ModifyIssue, (), info) RESPONSE.setStatus(204) return RESPONSE def _parseExternalEditBody(self, body): """ return a dict of all the info we could find in this body """ d = {} labels = {'Subject':'title', 'Description':'description', 'Name':'fromname', 'Email':'email', 'Display format':'display_format', 'Sections':'sections', 'Type':'type', 'Urgency':'urgency', 'URL':'url2issue'} body += "\n[" for key in labels.keys(): regex = re.compile('^\[%s\]\s*(.*?)[\[$]'%key, re.I|re.MULTILINE|re.DOTALL) found = regex.findall(body) if found: if found[0]: value = found[0].strip() else: value = '' tidied = self._tidyFoundValue(labels.get(key), value) if tidied is not None: d[labels.get(key)] = tidied return d def _tidyFoundValue(self, key, value): """ massage the value based on what the key is """ if key == 'display_format': if value.find("Plain as it's written") > -1: return 'plaintext' elif value.find("StructuredText") > -1: return 'structuredtext' else: # regular expression if re.compile('structured', re.I).findall(value): return 'structuredtext' else: return 'plaintext' elif key == 'sections': sections = value if not isinstance(sections, list): sections = sections.replace(',','\n') sections = sections.split('\n') sections = Utils.uniqify(sections) while '' in sections: sections.remove('') if not self.CanAddNewSections(): # remove all that we don't recognize options = [Utils.ss(x) for x in self.sections_options] sections = [x for x in sections if Utils.ss(x) in options] return sections elif key == 'urgency': for urgencyoption in self.urgencies: # makes sure the value is of correct case if Utils.ss(value) == Utils.ss(urgencyoption): return urgencyoption return None # if the above loop didn't work the value is unrecongized elif key == 'type': for typeoption in self.types: # makes sure the value is of correct case if Utils.ss(value) == Utils.ss(typeoption): return typeoption return None # see above on urgencies # all else, nothing to complain or nag about return value dtmls = ({'f':'dtml/draftissue_properties', 'n':'manage_draftissue_properties'}, ) addTemplates2Class(IssueTrackerDraftIssue, dtmls, "dtml") InitializeClass(IssueTrackerDraftIssue) #---------------------------------------------------------------------------- from Notification import IssueTrackerNotification from Thread import IssueTrackerIssueThread, IssueTrackerDraftIssueThread from Assignment import IssueTrackerIssueAssignment IssueTrackerProduct/zpt/0000755000175000017500000000000011012074374015475 5ustar peterbepeterbeIssueTrackerProduct/zpt/quickpreviewform.zpt0000644000175000017500000000623111012074373021637 0ustar peterbepeterbe
here's a special script that converts 'section' into ['section'] if present and 'sections' is not present
Subject:
Name:
Email:
Description:
Display format: Plain as it's written Structured Text
Section(s):
      
IssueTrackerProduct/zpt/compactList.zpt0000644000175000017500000000720211012074373020516 0ustar peterbepeterbe
Date Sections From Urgency Type
Found in comment...
Title   () - Status  Name and Email type
IssueTrackerProduct/zpt/ShowIssueData.zpt0000644000175000017500000003036111012074373020761 0ustar peterbepeterbe
Status


 Issue submitted via email Issue submitted via email

The Title  Issue submitted via email ()

This is the text
Submitted by: Author



Estimated time (hours)
Estimated time
Actual time (hours)
Actual time
Confidential
Confidential
Sections: Homepage, Other
Type:
Urgency:
URL: http://url
URL:
 

: User no longer available
IssueTrackerProduct/zpt/show_outlook.zpt0000644000175000017500000000656511012074373021003 0ustar peterbepeterbe
  (Confidential) () -
by:
Submitted by:

No activity in the last
weeks
No issues added yet

IssueTrackerProduct/zpt/show_next_actions.zpt0000644000175000017500000000346211012074373021776 0ustar peterbepeterbe

Your next action issues

  -
Show
more issues >

IssueTrackerProduct/zpt/ShowIssue.zpt0000644000175000017500000002153311012074373020170 0ustar peterbepeterbe
Close

Issue added!

URL1

Close

Notification was sent to: Notification will be sent to:

Notification icon

Preview before saving

Title   -   Name and Email Name and Email

COMMENT here

No comment. Follow ups must have a comment
date
Notification will be sent to:
Show the actual issue data
Show the threads

Stop comparing issues

Show the actual issue data
Show the threads
Show the actual issue data
Show the threads
 
IssueTrackerProduct/zpt/show_submissionerror_message.zpt0000644000175000017500000000170511012074373024247 0ustar peterbepeterbe

Error with submission

There was a slight error with your submission, please try to amend the missing pieces.

The following items submission errors were found:

IssueTrackerProduct/zpt/show_drafts.zpt0000644000175000017500000000321211012074373020554 0ustar peterbepeterbe

Draft issues

Draft followups

IssueTrackerProduct/zpt/rdf.zpt0000644000175000017500000000263511012074373017014 0ustar peterbepeterbe IssueTrackerProduct en-US IssueTrackerProduct/zpt/filter_options.zpt0000644000175000017500000002423111012074373021275 0ustar peterbepeterbe
Filters are currently being used
If this template is called on its own from AJAX, we have to make sure the content-type is set.
Do not show... (switch logic to Do only show) Do only show... (switch logic to Do not show)
Statuses
Sections
Urgencies
Types
From
Name
Email
IssueTrackerProduct/zpt/recent_history_view.zpt0000644000175000017500000000407711012074373022336 0ustar peterbepeterbe
Your Recent History
Recently added issues
     
Recently viewed issues
  • by name
Recent searches
  • (
    found)
     
IssueTrackerProduct/zpt/index_html.zpt0000644000175000017500000000604011012074373020366 0ustar peterbepeterbe
  Status  
count
Only for the last 1 day, 1 week or 1 month ever


IssueTrackerProduct/zpt/Keyboard-shortcuts.zpt0000644000175000017500000000534411012074373022035 0ustar peterbepeterbe

About keyboard shortcuts

Navigating around different pages


Searching from anywhere

To search for something press s or / and the focus will be placed in the search box. Hit the Enter key when you have entered your search word(s). If you want to search for something again, just press the s or / key again and enter another search word.


Quickly go to an issue

If you know the issue number (aka. issue ID) of an issue you want to go to, press the # key (some keyboards might require a Shift press first) and a dialog box will open to ask you for the issue number. You don't have to enter all the padded zeros or the # symbol itself but it should work nomatter what you enter as long as it is a possible issue reference.

Jumping to next and previous issue link

On any page where there is a list of links to issue (where the link text is the title of the issue) you can jump to the next and previous such link with the keys n and p respectively. This merely puts the focus on the next and previous links but to go to the issue page it links to you simply hit Return since the focus is on the link.


IssueTrackerProduct/zpt/ShowIssueThreads.zpt0000644000175000017500000000507311012074373021504 0ustar peterbepeterbe
Title Followup submitted via email Followup submitted via email   -   Name and Email permanent link permanent link
COMMENT here
No comment.
date
 
IssueTrackerProduct/zpt/anonymous_options.zpt0000644000175000017500000000135611012074373022043 0ustar peterbepeterbe
Add followup Download
IssueTrackerProduct/zpt/Your-next-action-issues.zpt0000644000175000017500000000244411012074373022735 0ustar peterbepeterbe

Your next action issues

If you enable this, a new list will appear on the home page with the headling Your next action issues. This list will only be available to those users how can log in to this issuetracker. The list attempts to figure out what your next actions ought to be. The list is sorted by a score which is worked out depending on how the issuetracker finds that this issue is relevant to you. Here is a list of how it finds it relevant to you and how it scores each found issue.

  1. Issues taken by you
  2. Issues assigned to you
  3. Issues where you have not had the last word but have participated
    (NB: internally sorted by urgency)
  4. Issues you have been emailed about if you get emails about all new issues
    (NB: internally sorted by urgency, only in the last two weeks)

If you are logged in, you will see this list related to you below:

IssueTrackerProduct/zpt/AddManyIssues.zpt0000644000175000017500000001043511012074373020747 0ustar peterbepeterbe

Add Many Issues

Name:
Email:
Subject: 
Section(s):


 
IssueTrackerProduct/zpt/addIssueTrackerForm.zpt0000644000175000017500000000274011012074373022137 0ustar peterbepeterbe

Header

Add IssueTracker

Warning
It appears that you do not have a Mail Host object deploy in your Zope acquisition path. An Issue Tracker needs one to be able to send out emails when that becomes necessary. Either go back and create one first (recommended) or create one inside the Issue Tracker that you're now about to create.
The recommended name to use is: MailHost

Id
Title

It is highly recommended that you go through the Properties Wizard the first time since it will prepare many of internal properties depending on how you intend to use this IssueTracker.

Footer

IssueTrackerProduct/zpt/What-is-WYSIWYG.zpt0000644000175000017500000000255211012074373020733 0ustar peterbepeterbe

WYSIWYG = What You See Is What You Get

Rich text editing is when you enter text with formatting as part of what you are writing. You can yourself select which words should be bold or italic for example.

If you select the WYSIWYG display format a JavaScript driven widget will appear where there used to be larger input boxes such as where you enter the description when you submit a new issue. This JavaScript widget will load dynamically into your webbrowser and will therefore appear a bit slow the first time you load it on a computer you have not used it on before. Once loaded all the dynamic JavaScript files will locally cached on your computer for subsequent use.

The WYSIWYG editor that the IssueTrackerProduct uses is TinyMCE developed under LGPL by Moxiecode Systems AB. This JavaScript widget is only supported with certain browsers (most common and modern ones). If you are experiencing problems, check that your browser is OK on this Compatiblity Chart.

IssueTrackerProduct/zpt/SearchForm.zpt0000644000175000017500000000115111012074373020262 0ustar peterbepeterbe
You can use the Filter options to filter on ex. open
IssueTrackerProduct/zpt/AddIssue.zpt0000644000175000017500000003572311012074373017746 0ustar peterbepeterbe

Add Issue (draft saved)

Subject:
User:
Name:
Email:




Description:
Display format: Plain as it's written Structured Text


 
Section(s):  new? Type: Urgency:
URL:
Assign to: Send notification to assignee
 
File attachment:
Hide again hide file attachments
 
Spambot prevention:
enter these numbers
 

IssueTrackerProduct/zpt/ShowIssueData.zpt.rej0000644000175000017500000001352511012074373021543 0ustar peterbepeterbe*************** *** 166,192 ****
- -

: - User no longer available - - -
- -
-
--- 201,230 ---- + +
+ + +   + + Send notification to new assignee + + + +
+ + IssueTrackerProduct/zpt/richList.zpt0000644000175000017500000001052611012074373020020 0ustar peterbepeterbe
Date Sections From Urgency Type
Found in comment...
Title   () - Status
Additional info sections Name and Email type
date    difference by Nameandemail
IssueTrackerProduct/zpt/previewform.zpt0000644000175000017500000001354611012074373020611 0ustar peterbepeterbe
here's a special script that converts 'section' into ['section'] if present and 'sections' is not present
Subject:
Name:
Email:
 
Description:
  Display format: Plain as it's written Structured Text
Section(s): Hide me?
tick and the public won't be able to see your name or emailaddress

Confidential issue?
tick and the public can't see the issue.
Type: Urgency:
URL:
File attachment:
Status:

IssueTrackerProduct/zpt/tell_a_friend.zpt0000644000175000017500000001267111012074373021031 0ustar peterbepeterbe
Message sent! Multimessage not sent because of no valid destinations.
Tell a Friend [with more options]

Message sent! Multimessage not sent because of no valid destinations.
Tell a Friend with more options
To:
From:
Friends


IssueTrackerProduct/zpt/About.zpt0000644000175000017500000000070211012074373017304 0ustar peterbepeterbe

About the IssueTrackerProduct

Return to Issue Tracker

Version:

Change log:


IssueTrackerProduct/zpt/OptionButtons.zpt0000644000175000017500000000117711012074373021070 0ustar peterbepeterbe
IssueTrackerProduct/zpt/list_issues_top_bar.zpt0000644000175000017500000000510711012074373022312 0ustar peterbepeterbe
# Issues: number (
issues filtered out or hidden)
Display: Rich or Compact Rich or Compact or CSV
Show only the first 5 issues   <
n
  Show them all  
n >

IssueTrackerProduct/zpt/subscription.zpt0000644000175000017500000000261711012074373020765 0ustar peterbepeterbe
Subscription to changes

  You are already involved in this issue. User:

IssueTrackerProduct/zpt/latestIssues.zpt0000644000175000017500000000405111012074373020723 0ustar peterbepeterbe
# Latest Issues
Title
   by Nameandemail
None
IssueTrackerProduct/zpt/form_followup.zpt0000644000175000017500000002351711012074373021135 0ustar peterbepeterbe


(draft saved)

Other title
Display format: Plain as it's written Structured Text
User:
Name:
Email:
Spambot prevention:
enter these numbers
File attachment:
Hide again hide file attachments
 
(Peter Bengtsson <mail@peterbe.com>)

IssueTrackerProduct/zpt/standard_error_message.zpt0000644000175000017500000001217611012074373022757 0ustar peterbepeterbe

Page Not Found, 404

Oops! Page can not be found.
Please double check the web address or use the search function on this page to find what you are looking for.

If you know you have the correct web address but are encountering an error, please send a email to the administrator of this site.

Perhaps it was one of these pages you were after:


System error

The IssueTrackerProduct encountered a system error that was unexpected. The errors were:

Error type:
Error Message:
Error value:

It appears that the system has tried to access a variable or attribute called that does not exist.
If you have upgraded your instance of the IssueTrackerProduct make sure you have pressed the Update Everything button under the Management tab. You need to have management access rights to be able to do this.

It appears that the system has tried to use one value for a missmatching purpose like type casting that should not work.

It appears that the system has tried to access an attribute or variable that is not defined in either the local or the global namespaces. This is quite typically a programming bug where the code assumes the presence of a variable from that in fact has not been defined in that block of code.

How to report this error

If this error is highly unexpected you might want to contribute to the IssueTrackerProduct project how you encountered the error and its presence. To do this, you need to report it back to the IssueTrackerProduct website on the

Real Issue Tracker.

Download this file and attach it together with the bug report when you submit it:
Download this file and submit with your bug report

You can submit your bug report confidentially and you can edit the content of this error file if you want to.

IssueTrackerProduct/zpt/What-is-StructuredText.zpt0000644000175000017500000001501111012074373022554 0ustar peterbepeterbe

About Structured Text

Structured text is a text input format that allows for some simple text formatting such as bold text, links or code inclusion.

You enter...

You get...

'Sam' said: *I* did **not** do _it_ Sam said: I did not do it
Send "email":mailto:info@foo.com to
"Foo.com":http://www.foo.com
Send email to Foo.com
Line breaks
don't matter.

Only paragraph breaks
do.
Line breaks don't matter.

Only paragraph breaks do.
Pseudo code::

 while (!perl) {
    print "Python";
 }
Pseudo code:
 while (!perl) {
    print "Python";
 }
- Bulleted lists can be...

- Written using '-', '*' or 'o'...

- Or using numbers like '1.', '2.', etc.
  • Bulleted lists can be...
  • Written using '-', * or o...
  • Or using numbers like 1., 2., etc.

Alternative external links:

Structured text is text that uses indentation and simple symbology to indicate the structure of a document.
A structured string consists of a sequence of paragraphs separated by one or more blank lines. Each paragraph has a level which is defined as the minimum indentation of the paragraph. A paragraph is a sub-paragraph of another paragraph if the other paragraph is the last preceding paragraph that has a lower level.

Special symbology is used to indicate special constructs:

  • A single-line paragraph whose immediately succeeding paragraphs are lower level is treated as a header.

  • A paragraph that begins with a '-', *, or o is treated as an unordered list (bullet) element.

  • A paragraph that begins with a sequence of digits followed by a white-space character is treated as an ordered list element.

  • A paragraph that begins with a sequence of sequences, where each sequence is a sequence of digits or a sequence of letters followed by a period, is treated as an ordered list element.

  • A paragraph with a first line that contains some text, followed by some white-space and -- is treated as a descriptive list element. The leading text is treated as the element title.

  • Sub-paragraphs of a paragraph that ends in the word example or the word examples, or :: is treated as example code and is output as is:

        <table border=0>
          <tr>
            <td> Foo 
        </table>
    

  • Text enclosed single quotes (with white-space to the left of the first quote and whitespace or puctuation to the right of the second quote) is treated as example code.

    For example: &lt;dtml-var foo>.

  • Text surrounded by ' characters (with white-space to the left of the first and whitespace or puctuation to the right of the second ') is emphasized*.

  • Text surrounded by ' characters (with white-space to the left of the first and whitespace or puctuation to the right of the second ') is made strong**.

  • Text surrounded by _ underscore characters (with whitespace to the left and whitespace or punctuation to the right) is made _underlined_.

  • Text encloded by double quotes followed by a colon, a URL, and concluded by punctuation plus white space, or just white space, is treated as a hyper link.

    For example, &quot;Zope&quot;:http://www.zope.org/ is interpreted as Zope

    Note: This works for relative as well as absolute URLs.

  • Text enclosed by double quotes followed by a comma, one or more spaces, an absolute URL and concluded by punctuation plus white space, or just white space, is treated as a hyper link.

    For example: &quot;mail me&quot;, mailto:amos@digicool.com is interpreted as mail me

  • Text enclosed in brackets which consists only of letters, digits, underscores and dashes is treated as hyper links within the document.

    For example: "As demonstrated by Smith &#091;12&#093; this technique ..."

    Is interpreted as: "As demonstrated by Smith [12] this technique"

    Together with the next rule this allows easy coding of references or end notes.

  • Text enclosed in brackets which is preceded by the start of a line, two periods and a space is treated as a named link. For example:

    .. &#091;12&#093; "Effective Techniques" Smith, Joe ...

    Is interpreted as

    [12] "Effective Techniques" Smith, Joe ...

    Note: see the <A NAME="12"> in the HTML source.

    Together with the previous rule this allows easy coding of references or end notes.

IssueTrackerProduct/zpt/submitform.zpt0000644000175000017500000000473111012074373020427 0ustar peterbepeterbe
IssueTrackerProduct/zpt/shownewIssueURL.zpt0000644000175000017500000000155311012074373021325 0ustar peterbepeterbe

Issue Added!

Bookmark this URL or copy the link location

URL1

Notification was sent to:

Notified
IssueTrackerProduct/zpt/manager_options.zpt0000644000175000017500000000237411012074373021426 0ustar peterbepeterbe
Add followup Download
Open
IssueTrackerProduct/zpt/QuickAddIssue.zpt0000644000175000017500000001672111012074373020740 0ustar peterbepeterbe

Quick Add Issue

For more adding options such as urgency, type, URL, privacy and Preview-before-save use the Add Issue

here's a special script that converts 'section' into ['section'] if present and 'sections' is not present
Subject:
User:
Name:
Email:
Description:
Display format: Plain as it's written Structured Text
Spambot prevention:
enter these numbers
Section(s):
      
IssueTrackerProduct/zpt/CompleteList.zpt0000644000175000017500000000502411012074373020640 0ustar peterbepeterbe

All reports
Date Sections From Urgency Type



 



IssueTrackerProduct/zpt/User.zpt0000644000175000017500000004010311012074373017147 0ustar peterbepeterbe
Name
Email
Display format Plain as it's written
Structured Text
WYSIWYG
 
Cancel
Old
New
Confirm
 
Cancel

Display format: Plain as it's written Structured Text WYSIWYG
Change | Change password
 more info
 more info


Issues assigned to you (List these, Complete list) [hide list] [expand list]
()
You have more issues assigned to you Show them all

 
Issues you have added (List these, Complete list) [hide list] [expand list]
()
You have more issues that you have added Show them all

No issues added yet in your name


 
Issues followed up on (List these, Complete list) [hide list] [expand list]
()
You have more followups that you have added Show them all

 
Issues you have subscribed to (List these, Complete list) [hide list] [expand list]
()

 

IssueTrackerProduct/zpt/addNotifyableContainerForm.zpt0000644000175000017500000000211211012074373023463 0ustar peterbepeterbe

Header

Form Title

You already have a Issue Tracker Notifyable Container deployed.

Go to it now

Footer

IssueTrackerProduct/zpt/user_searchform.zpt0000644000175000017500000000114411012074373021422 0ustar peterbepeterbe
Name:
Email:
IssueTrackerProduct/zpt/ListIssues.zpt0000644000175000017500000000256211012074373020347 0ustar peterbepeterbe

List Issues

All reports


IssueTrackerProduct/zpt/Statistics.zpt0000644000175000017500000001365311012074373020375 0ustar peterbepeterbe

The Quick Statistics is only for Managers

Issue status     Count Percentage
Status   count
Total Overall status progress:  

 

Issues by sections Status Total
Something

 

Issues per x days period

IssueTrackerProduct/zpt/recent_history_widget.zpt0000644000175000017500000000213611012074373022641 0ustar peterbepeterbe

Recent issues

Recent report runs

IssueTrackerProduct/zpt/followup_preview.zpt0000644000175000017500000000433011012074373021643 0ustar peterbepeterbe

Preview before saving

Title   -   Name and Email
COMMENT here
No comment.
date
  File  
Notification will be sent to:
IssueTrackerProduct/zpt/form_delete.zpt0000644000175000017500000000162011012074373020517 0ustar peterbepeterbe

Deleting an Issue should not be necessary unless the issue is broken or faulty.
Use 'Complete' for most cases.

N.B. Nothing "bad" happens if you delete it.

Delete Issue?

 

IssueTrackerProduct/zpt/Reports.zpt0000644000175000017500000000366611012074373017704 0ustar peterbepeterbe

Reports

Name of script

Show with List issues | Complete List
Found

 

IssueTrackerProduct/zpt/show_user_achievements.zpt0000644000175000017500000000403411012074373023005 0ustar peterbepeterbe
# issues Opened Closed
Ever
This month
Last month
Last week
This week
Yesterday
Today
IssueTrackerProduct/zpt/preview_issue.zpt0000644000175000017500000001077011012074373021131 0ustar peterbepeterbe

Preview before saving

Status

Issues must have a subject

This is the text

Issues must have a description.

Submitted by: YES YES Author
date
Confidential Confidential
Sections: Homepage, Other
Type: missing page
Urgency: critical
URL: http://url
 
Assigned to:
Notification will be sent to:
IssueTrackerProduct/zpt/show_drafts_simple.zpt0000644000175000017500000000157611012074373022140 0ustar peterbepeterbe

Drafts

(autosave) (delete this draft)

IssueTrackerProduct/zpt/search_widget.zpt0000644000175000017500000000257611012074373021055 0ustar peterbepeterbe
Use current filters in search Use current filters in search
IssueTrackerProduct/zpt/User_must_change_password.zpt0000644000175000017500000000411211012074373023446 0ustar peterbepeterbe

It appears that you have been asked to change your password as soon as you have logged in.
If you choose to ignore this now, which you can, you will be asked again the next time you log in again.

New password
Confirm
 

 

You have already changed your password.

Go to your page or Go to the home page

IssueTrackerProduct/zpt/StandardHeader.zpt0000644000175000017500000001063111012074373021105 0ustar peterbepeterbe
 
 
 
Validate, RSS feed
SESSION

COOKIES
USER
ZOPEUSER
IssueTrackerProduct/zpt/quick_form_followup.zpt0000644000175000017500000000024611012074373022323 0ustar peterbepeterbe