Skip to content
This repository has been archived by the owner on Aug 14, 2023. It is now read-only.

Commit

Permalink
Convert all options to use the Options API so they can be administere…
Browse files Browse the repository at this point in the history
…d through an admin panel. Provide functionality equivalent to what is suggested in pull requests itota#14 and itota#15. Clean up some database queries
  • Loading branch information
thenor57 committed Jan 5, 2017
1 parent dc6d97f commit 8d99411
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 107 deletions.
127 changes: 70 additions & 57 deletions tracsubtickets/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@

import pkg_resources

from trac.config import Option, IntOption, ChoiceOption, ListOption
from trac.config import Option, BoolOption, IntOption, ChoiceOption, ListOption
from trac.core import *
from trac.env import IEnvironmentSetupParticipant
from trac.db import DatabaseManager
from trac.resource import ResourceNotFound
from trac.ticket.model import Ticket
from trac.ticket.model import Type as TicketType
from trac.ticket.api import ITicketChangeListener, ITicketManipulator
from trac.ticket.notification import TicketNotifyEmail

Expand All @@ -61,6 +60,18 @@ class SubTicketsSystem(Component):
ITicketChangeListener,
ITicketManipulator)

opt_no_modif_w_p_c = BoolOption \
('subtickets', 'no_modif_when_parent_closed', default='false',
doc = _("""
If `True`, any modification of a child whose parent is `closed`
will be blocked. If `False`, status changes will be blocked as
controlled by the setting of `skip_closure_validation`.
For compatibility with plugin versions prior to 0.5 that blocked
any modification unconditionally.
""")
)

def __init__(self):
self._version = None
self.ui = None
Expand Down Expand Up @@ -193,62 +204,64 @@ def prepare_ticket(self, req, ticket, fields, actions):
pass

def validate_ticket(self, req, ticket):
with self.env.db_query as db:
cursor = db.cursor()

try:
invalid_ids = set()
_ids = set(NUMBERS_RE.findall(ticket['parents'] or ''))
myid = str(ticket.id)
for id in _ids:
if id == myid:
try:
invalid_ids = set()
_ids = set(NUMBERS_RE.findall(ticket['parents'] or ''))
myid = str(ticket.id)
for id in _ids:
if id == myid:
invalid_ids.add(id)
yield 'parents', _("A ticket cannot be a parent of itself")
else:
# check if the id exists
tkt_id = self.env.db_query("""
SELECT id FROM ticket WHERE id=%s
""", (id, ))
if not tkt_id:
invalid_ids.add(id)
yield 'parents', _("A ticket cannot be a parent of itself")
yield 'parents', _("Ticket #%(id)s does not exist",
id=id)

# circularity check function
def _check_parents(id, all_parents):
all_parents = all_parents + [id]
errors = []
parents = self.env.db_query("""
SELECT parent FROM subtickets WHERE child=%s
""", (id, ))
for x in [int(x[0]) for x in parents]:
if x in all_parents:
invalid_ids.add(x)
error = ' > '.join(['#%s' % n for n in all_parents+[x]])
errors.append(('parents', _('Circularity error: %(e)s',
e=error)))
else:
# check if the id exists
cursor.execute("""
SELECT id FROM ticket WHERE id=%s
""", (id, ))
row = cursor.fetchone()
if row is None:
invalid_ids.add(id)
yield 'parents', _("Ticket #%(id)s does not exist", id=id)

# circularity check function
def _check_parents(id, all_parents):
all_parents = all_parents + [id]
errors = []
cursor.execute("""
SELECT parent FROM subtickets WHERE child=%s
""", (id, ))
for x in [int(x[0]) for x in cursor]:
if x in all_parents:
invalid_ids.add(x)
error = ' > '.join(['#%s' % n for n in all_parents + [x]])
errors.append(('parents', _('Circularity error: %(e)s', e=error)))
else:
errors += _check_parents(x, all_parents)
return errors

for x in [i for i in _ids if i not in invalid_ids]:
# Refuse modification if parent closed or if parentship is circular
try:
parent = Ticket(self.env, x)
if parent and parent['status'] == 'closed' :
invalid_ids.add(x)
yield None, _("Cannot modify ticket because parent ticket #%(id)s is closed. Comments allowed, though.", id=x)
else:
# check circularity
all_parents = ticket.id and [ticket.id] or []
for error in _check_parents(int(x), all_parents):
yield error
except ResourceNotFound, e:
errors += _check_parents(x, all_parents)
return errors

for x in [i for i in _ids if i not in invalid_ids]:
# Refuse modification if parent closed
# or if parentship is to be made circular
try:
parent = Ticket(self.env, x)
if parent and parent['status'] == 'closed' \
and self.opt_no_modif_w_p_c:
invalid_ids.add(x)

valid_ids = _ids.difference(invalid_ids)
ticket['parents'] = valid_ids and ', '.join(sorted(valid_ids, key=lambda x: int(x))) or ''
except Exception, e:
import traceback
self.log.error(traceback.format_exc())
yield 'parents', _('Not a valid list of ticket IDs.')
yield None, _("""Cannot modify ticket because
parent ticket #%(id)s is closed.
Comments allowed, though.""",
id=x)
# check circularity
all_parents = ticket.id and [ticket.id] or []
for error in _check_parents(int(x), all_parents):
yield error
except ResourceNotFound, e:
invalid_ids.add(x)

valid_ids = _ids.difference(invalid_ids)
ticket['parents'] = valid_ids and ', '.join(sorted(valid_ids, key=lambda x: int(x))) or ''
except Exception, e:
import traceback
self.log.error(traceback.format_exc())
yield 'parents', _('Not a valid list of ticket IDs.')

121 changes: 71 additions & 50 deletions tracsubtickets/web_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,36 @@ class SubTicketsModule(Component):

### Simple Options

opt_recursion_depth = IntOption('subtickets', 'recursion_depth', default=-1,
doc = _("""
Limit the number of recursive levels when listing
subtickets. Default is infinity, represented by
`-1`. The value zero (0) limits the listing to
immediate children.
""")
)
opt_add_style = ChoiceOption('subtickets', 'add_style', ['button', 'link'],
doc = _("""
Choose whether to make `Add` look like a button (default)
or a link
""")
)
opt_skip_validation = ListOption \
('subtickets', 'skip_closure_validation',
default=[],
doc = _("""
Normally, reopening a child with a `closed` parent will be
refused and closing a parent with non-`closed` children will also
be refused. Adding either of `reopen` or `resolve` to this option will
make Subtickets skip this validation for the respective action.
Separate by comma if both actions are listed.
Caveat: This functionality will be made workflow-independent in a
future release of !SubTicketsPlugin.
""")
)

opt_recursion_depth = IntOption \
('subtickets', 'recursion_depth', default=-1,
doc = _("""
Limit the number of recursive levels when listing subtickets.
Default is infinity, represented by`-1`. The value zero (0)
limits the listing to immediate children.
""")
)
opt_add_style = ChoiceOption \
('subtickets', 'add_style', ['button', 'link'],
doc = _("""
Choose whether to make `Add` look like a button (default) or a link
""")
)

opt_owner_url = Option('subtickets', 'owner_url',
doc = _("""
Currently undocumented.
Expand All @@ -77,22 +93,26 @@ class SubTicketsModule(Component):
###

def __init__(self):
# The following initialisations must happen inside init() in order to access self.env
# The following initialisations must happen inside init()
# in order to be able to access self.env
for tt in TicketType.select(self.env):
self.opt_inherit_fields[tt.name] = ListOption('subtickets','%s.child_inherits' % tt.name,
default='', doc = _("""
Comma-separated list of ticket fields whose values are
to be copied from a parent ticket into a newly created
child ticket
""")
)
self.opt_columns[tt.name] = ListOption('subtickets', '%s.table_columns' % tt.name,
default='status,owner',
doc = _("""
Comma-separated list of ticket fields whose values are to be
shown for each child ticket in the subtickets list
""")
)
self.opt_inherit_fields[tt.name] = ListOption \
('subtickets','%s.child_inherits' % tt.name,
default='',
doc = _("""
Comma-separated list of ticket fields whose values are
to be copied from a parent ticket into a newly created
child ticket
""")
)
self.opt_columns[tt.name] = ListOption \
('subtickets', '%s.table_columns' % tt.name,
default='status,owner',
doc = _("""
Comma-separated list of ticket fields whose values are to be
shown for each child ticket in the subtickets list
""")
)



Expand Down Expand Up @@ -150,39 +170,41 @@ def prepare_ticket(self, req, ticket, fields, actions):
pass

def get_children(self, parent_id):
with self.env.db_query as db:
children = {}
cursor = db.cursor()
cursor.execute("""
SELECT parent, child FROM subtickets WHERE parent=%s
""", (parent_id, ))
children = {}

for parent, child in cursor:
children[child] = None
for parent, child in self.env.db_query("""
SELECT parent, child FROM subtickets WHERE parent=%s
""", (parent_id, )):
children[child] = None

for id in children:
children[id] = self.get_children(id)
for id in children:
children[id] = self.get_children(id)

return children
return children

def validate_ticket(self, req, ticket):
action = req.args.get('action')

if action in self.opt_skip_validation:
return

if action == 'resolve':
with self.env.db_query as db:
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, _("Cannot close/resolve because child ticket #%(child)s is still open", child=child)
for parent, child in self.env.db_query("""
SELECT parent, child FROM subtickets WHERE parent=%s
""", (ticket.id, )):
if Ticket(self.env, child)['status'] != 'closed':
yield None, _("""Cannot close/resolve because child
ticket #%(child)s is still open""",
child=child)

elif action == 'reopen':
ids = set(NUMBERS_RE.findall(ticket['parents'] or ''))
for id in ids:
if Ticket(self.env, id)['status'] == 'closed':
yield None, _("Cannot reopen because parent ticket #%(id)s is closed", id=id)
yield None,
_("Cannot reopen because parent ticket #%(id)s is closed",
id=id)

# ITemplateStreamFilter method
def filter_stream(self, req, method, filename, stream, data):
Expand Down Expand Up @@ -229,7 +251,6 @@ def filter_stream(self, req, method, filename, stream, data):
div.append(tag.table(tbody, class_='subtickets'))
# tickets
def _func(children, depth=0):
print '_func', depth, children
for id in sorted(children, key=lambda x: int(x)):
ticket = Ticket(self.env, id)

Expand Down

0 comments on commit 8d99411

Please sign in to comment.