Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 192eb94c986c556ddef83fb22d32b5e78bda5b84 @itota committed
Showing with 368 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +3 −0 setup.cfg
  3. +56 −0 setup.py
  4. 0 tracsubtickets/__init__.py
  5. +169 −0 tracsubtickets/api.py
  6. +40 −0 tracsubtickets/db_default.py
  7. +96 −0 tracsubtickets/web_ui.py
4 .gitignore
@@ -0,0 +1,4 @@
+*.pyc
+*.swp
+*.egg-info
+tags
3 setup.cfg
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = .dev
+tag_date = True
56 setup.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2010, Takashi Ito
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of the authors nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from setuptools import find_packages, setup
+
+setup(
+ name = 'TracSubTicketsPlugin',
+ version = '0.1.0',
+ keywords = 'trac plugin ticket subticket',
+ author = 'Takashi Ito',
+ author_email = 'TakashiC.Ito@gmail.com',
+ url = 'http://github.com/itota/trac-subtickets-plugin',
+ description = 'Trac Sub-Tickets Plugin',
+ license = 'BSD',
+
+ install_requires = ['Trac'],
+
+ packages = find_packages(exclude=['*.tests*']),
+ package_data = {
+ 'tracsubtickets': [
+ ],
+ },
+ entry_points = {
+ 'trac.plugins': [
+ 'tracsubtickets.api = tracsubtickets.api',
+ 'tracsubtickets.web_ui = tracsubtickets.web_ui',
+ ],
+ },
+)
+
0 tracsubtickets/__init__.py
No changes.
169 tracsubtickets/api.py
@@ -0,0 +1,169 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2010, Takashi Ito
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of the authors nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+
+from trac.core import *
+from trac.env import IEnvironmentSetupParticipant
+from trac.db import DatabaseManager
+from trac.ticket.api import ITicketChangeListener, ITicketManipulator
+
+import db_default
+
+
+class SubTicketsSystem(Component):
+
+ implements(IEnvironmentSetupParticipant,
+ ITicketChangeListener,
+ ITicketManipulator)
+
+ NUMBERS_RE = re.compile(r'\d+', re.U)
+
+ # IEnvironmentSetupParticipant methods
+ def environment_created(self):
+ self.found_db_version = 0
+ self.upgrade_environment(self.env.get_db_cnx())
+
+ def environment_needs_upgrade(self, db):
+ cursor = db.cursor()
+ cursor.execute("SELECT value FROM system WHERE name=%s",
+ (db_default.name, ))
+ value = cursor.fetchone()
+ try:
+ self.found_db_version = int(value[0])
+ if self.found_db_version < db_default.version:
+ return True
+ except:
+ self.found_db_version = 0
+ return True
+
+ # check the custom field
+ if 'parents' not in self.config['ticket-custom']:
+ return True
+
+ return False
+
+ def upgrade_environment(self, db):
+ db_manager, _ = DatabaseManager(self.env)._get_connector()
+
+ # update the version
+ old_data = {} # {table.name: (cols, rows)}
+ cursor = db.cursor()
+ if not self.found_db_version:
+ cursor.execute("INSERT INTO system (name, value) VALUES (%s, %s)",
+ (db_default.name, db_default.version))
+ else:
+ cursor.execute("UPDATE system SET value=%s WHERE name=%s",
+ (db_default.version, db_default.name))
+ for table in db_default.tables:
+ cursor.execute("SELECT * FROM %s", (table.name, ))
+ cols = [x[0] for x in cursor.description],
+ rows = cursor.fetchall()
+ old_data[table.name] = (cols, rows)
+ cursor.execute("DROP TABLE %s", (table.name))
+
+ # insert the default table
+ for table in db_default.tables:
+ for sql in db_manager.to_sql(table):
+ cursor.execute(sql)
+
+ # add old data
+ if table.name in old_data:
+ cols, rows = old_data[table.name]
+ sql = 'INSERT INTO %s (%s) VALUES (%s)' % \
+ (table.name, ','.join(cols), ','.join(['%s'] * len(cols)))
+ for row in rows:
+ cursor.execute(sql, row)
+
+ # add the custom field
+ cfield = self.config['ticket-custom']
+ if 'parents' not in cfield:
+ cfield.set('parents', 'text')
+ cfield.set('parents.label', 'Parent Tickets')
+ self.config.save()
+
+ # ITicketChangeListener methods
+ def ticket_created(self, ticket):
+ self.ticket_changed(ticket, '', ticket['reporter'], {})
+
+ def ticket_changed(self, ticket, comment, author, old_values):
+ if 'parents' not in old_values:
+ return
+
+ old_parents = old_values.get('parents', '')
+ old_parents = set(self.NUMBERS_RE.findall(old_parents))
+ new_parents = set(self.NUMBERS_RE.findall(ticket['parents'] or ''))
+
+ if new_parents == old_parents:
+ return
+
+ db = self.env.get_db_cnx()
+ cursor = db.cursor()
+
+ # add new parents
+ for parent in new_parents - old_parents:
+ cursor.execute("INSERT INTO subtickets VALUES(%s, %s)",
+ (parent, ticket.id))
+ # remove old parents
+ for parent in old_parents - new_parents:
+ cursor.execute("DELETE FROM subtickets WHERE parent=%s AND child=%s",
+ (parent, ticket.id))
+ db.commit()
+
+ def ticket_deleted(self, ticket):
+ db = self.env.get_db_cnx()
+ cursor = db.cursor()
+ # TODO: check if there's any child ticket
+ cursor.execute("DELETE FROM subtickets WHERE child=%s", (ticket.id, ))
+ db.commit()
+
+ # ITicketManipulator methods
+ def prepare_ticket(self, req, ticket, fields, actions):
+ pass
+
+ def validate_ticket(self, req, ticket):
+ db = self.env.get_db_cnx()
+ cursor = db.cursor()
+
+ try:
+ ids = []
+ _ids = set(self.NUMBERS_RE.findall(ticket['parents'] or ''))
+ myid = int(ticket.id)
+ for id in _ids:
+ if int(id) == myid:
+ yield 'parents', 'A ticket cannot be a parent to itself'
+ # TODO: circularity check
+ cursor.execute("SELECT id FROM ticket WHERE id=%s", (id, ))
+ row = cursor.fetchone()
+ if row is not None:
+ ids.append(id)
+ ticket['parents'] = ', '.join(sorted(ids, key=lambda x: int(x)))
+ except Exception, e:
+ yield 'parents', 'Not a valid list of ticket IDs'
+
40 tracsubtickets/db_default.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2010, Takashi Ito
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of the authors nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from trac.db import Table, Column
+
+name = 'subtickets'
+version = 1
+tables = [
+ Table(name, key=('parent','child'))[
+ Column('parent'),
+ Column('child'),
+ ],
+]
+
96 tracsubtickets/web_ui.py
@@ -0,0 +1,96 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2010, Takashi Ito
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of the authors nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from trac.core import *
+from trac.web.api import IRequestFilter
+from trac.ticket.api import ITicketManipulator
+from trac.ticket.model import Ticket
+from genshi.builder import tag
+
+from api import SubTicketsSystem
+
+
+class SubTicketsModule(Component):
+
+ implements(IRequestFilter, ITicketManipulator)
+
+ NUMBERS_RE = SubTicketsSystem.NUMBERS_RE
+
+ # IRequestFilter methods
+ def pre_process_request(self, req, handler):
+ return handler
+
+ def post_process_request(self, req, template, data, content_type):
+ if req.path_info.startswith('/ticket/'):
+ # get parents data
+ parents = data['ticket']['parents'] or ''
+ ids = set(self.NUMBERS_RE.findall(parents))
+
+ if len(parents) > 0:
+ self._append_parent_links(req, data, ids)
+
+ return template, data, content_type
+
+ def _append_parent_links(self, req, data, ids):
+ links = []
+ for id in ids:
+ ticket = Ticket(self.env, id)
+ elem = tag.a('#%s' % id,
+ href=req.href.ticket(id),
+ class_='%s ticket' % ticket['status'],
+ title=ticket['summary'])
+ if len(links) > 0:
+ links.append(', ')
+ links.append(elem)
+ for field in data.get('fields', ''):
+ if field.get('name') == 'parents':
+ field['rendered'] = tag.span(*links)
+
+ # ITicketManipulator methods
+ def prepare_ticket(self, req, ticket, fields, actions):
+ pass
+
+ def validate_ticket(self, req, ticket):
+ if req.args.get('action') == 'resolve':
+ db = self.env.get_db_cnx()
+ cursor = db.cursor()
+
+ cursor.execute("SELECT parent, child FROM subtickets WHERE parent=%s",
+ (ticket.id, ))
+
+ for parent, child in cursor:
+ if Ticket(self.env, child)['status'] != 'closed':
+ yield None, 'Child ticket #%s has not been closed yet' % child
+
+ elif req.args.get('action') == 'reopen':
+ ids = set(self.NUMBERS_RE.findall(ticket['parents'] or ''))
+ for id in ids:
+ if Ticket(self.env, id)['status'] == 'closed':
+ yield None, 'Parent ticket #%s is closed' % id
+

0 comments on commit 192eb94

Please sign in to comment.
Something went wrong with that request. Please try again.