Changeset 18217


Ignore:
Timestamp:
Apr 14, 2021, 11:22:34 AM (4 years ago)
Author:
Cinc-th
Message:

TracRelationsPlugin: allow to create child tickets. Some code is taken from ChildTicketsPlugin. Note that children are not shown on the ticket page yet.

Location:
tracrelationsplugin/trunk/tracrelations
Files:
2 edited
4 copied

Legend:

Unmodified
Added
Removed
  • tracrelationsplugin/trunk/tracrelations/__init__.py

    r18194 r18217  
    11from .api import *
     2from .childrelations import *
    23from .ticket import *
  • tracrelationsplugin/trunk/tracrelations/childrelations.py

    r18129 r18217  
    99# you should have received as part of this distribution.
    1010#
    11 from pkg_resources import get_distribution, parse_version
     11from pkg_resources import get_distribution, parse_version, resource_filename
    1212from trac.admin import IAdminPanelProvider
     13from trac.config import IntOption
     14from trac.env import IEnvironmentSetupParticipant
    1315from trac.core import *
    14 from trac.ticket.api import TicketSystem
     16from trac.ticket.api import ITicketChangeListener, ITicketManipulator, TicketSystem
    1517from trac.ticket.model import Type
    16 from trac.util.text import exception_to_unicode
     18from trac.util.html import tag
     19from trac.util.text import exception_to_unicode, to_unicode
    1720from trac.util.translation import _
     21from trac.web.api import IRequestFilter
    1822from trac.web.chrome import ITemplateProvider
    19 from trac.web.chrome import add_notice, add_warning, add_stylesheet
    20 
    21 
    22 # Api changes regarding Genshi started after v1.2. This not only affects templates but also fragment
    23 # creation using trac.util.html.tag and friends
    24 pre_1_3 = parse_version(get_distribution("Trac").version) < parse_version('1.3')
     23from trac.web.chrome import add_notice, add_script, add_script_data, add_stylesheet, add_warning
     24from trac.wiki.formatter import format_to_oneliner
     25
     26from tracrelations.api import RelationSystem
     27from tracrelations.jtransform import JTransformer
     28from tracrelations.model import Relation
     29
    2530
    2631def _save_config(config, req, log):
     
    3843
    3944
    40 class ChildTicketsAdminPanel(Component):
     45class ChildRelationsAdminPanel(Component):
    4146    """Configure which ticket types allow children, inherited fields for children and more.
    4247
     
    4550
    4651    The following global settings are available:
    47     [[TracIni(childtickets)]]
     52    [[TracIni(relations-child)]]
    4853
    4954    You may specify features for each ticket type. In this example the configuration
    5055    is for tickets of type {{{defect}}}:
    5156    {{{#!ini
    52     [childtickets]
     57    [relations-child]
    5358    parent.defect.allow_child_tickets = True
    5459    parent.defect.inherit = description,milestone,summary,project,version
     
    6469    implements(IAdminPanelProvider, ITemplateProvider)
    6570
    66     def ticket_custom_field_exists(self):
    67         """Check if the ticket custom field 'parentt' is configured.
    68 
    69         :returns None if not configured, otherwise the field type
    70 
    71         We don't check for proper custom field type here.
    72         """
    73         return self.config.get('ticket-custom', 'parent', None)
    74 
    7571    # IAdminPanelProvider methods
    7672
    7773    def get_admin_panels(self, req):
    78         if 'TICKET_ADMIN' in req.perm('admin', 'childticketsplugin/types'):
    79             yield ('childticketsplugin', _('Child Tickets'), 'types',
     74        if 'TICKET_ADMIN' in req.perm('admin', 'childrelations/types'):
     75            yield ('childrelations', _('Relations'), 'types',
    8076                   _('Parent Types'))
    81         if 'TICKET_ADMIN' in req.perm('admin', 'childticketsplugin/basics'):
    82             excl_mark = '' if self.ticket_custom_field_exists() else ' (!)'
    83             yield ('childticketsplugin', _('Child Tickets'), 'basics',
    84                    _('Basic Settings') + excl_mark)
    85 
    86     def _render_admin_basics(self, req, cat, page, parenttype):
    87         # Only for trac admins.
    88         req.perm('admin', 'childticketsplugin/basics').require('TICKET_ADMIN')
    89 
    90         data = {
    91             'custom_field': self.ticket_custom_field_exists(),
    92             'custom_field_label': _('Parent'),
    93             'max_view_depth_val': self.config.getint('childtickets', 'max_view_depth', default=3),
    94             'recursion_warn': self.config.getint('childtickets', 'recursion_warn', default=7)
    95         }
    96 
    97         if req.method == 'POST':
    98             if req.args.get('create-ticket-custom'):
    99                 self.config.set('ticket-custom', 'parent', 'text')
    100                 self.config.set('ticket-custom', 'parent.label', req.args.get('custom-field-label-val', _('Parent')))
    101                 self.config.set('ticket-custom', 'parent.format', 'wiki')
    102                 self.config.save()
    103                 add_notice(req, _("The ticket custom field 'parent' was added to the configuration."))
    104             elif req.args.get('max-view-depth'):
    105                 self.config.set('childtickets', 'max_view_depth', req.args.get('max-view-depth-val', 3))
    106                 self.config.save()
    107                 add_notice(req, _('Your changes have been saved.'))
    108             elif req.args.get('recursion-warn'):
    109                 self.config.set('childtickets', 'recursion_warn', req.args.get('recursion-warn-val', 7))
    110                 self.config.save()
    111                 add_notice(req, _('Your changes have been saved.'))
    112 
    113             req.redirect(req.href.admin(cat, page))
    114 
    115         if pre_1_3:
    116             return 'admin_ct_basics.html', data
    117         else:
    118             return 'admin_ct_basics_jinja.html', data
    11977
    12078    def render_admin_panel(self, req, cat, page, parenttype):
    12179
    122         if page == 'basics':
    123             return self._render_admin_basics(req, cat, page, parenttype)
    124 
    125         # Only for trac admins.
    126         req.perm('admin', 'childticketsplugin/types').require('TICKET_ADMIN')
    127 
    128         if req.method == 'POST':
    129             if req.args.get('create-ticket-custom'):
    130                 self.config.set('ticket-custom', 'parent', 'text')
    131                 self.config.set('ticket-custom', 'parent.label', req.args.get('custom-field-label-val', _('Parent')))
    132                 self.config.set('ticket-custom', 'parent.format', 'wiki')
    133                 self.config.save()
    134                 add_notice(req, _("The ticket custom field 'parent' was added to the configuration."))
    135                 req.redirect(req.href.admin(cat, page))
     80        # Only for ticket admins.
     81
     82        req.perm('admin', 'childrelations/types').require('TICKET_ADMIN')
    13683
    13784        field_names = TicketSystem(self.env).get_ticket_field_labels()
     85
    13886        # Detail view?
    13987        if parenttype:
     
    14189                allow_child_tickets = \
    14290                    req.args.get('allow_child_tickets')
    143                 self.config.set('childtickets',
     91                self.config.set('relations-child',
    14492                                'parent.%s.allow_child_tickets'
    14593                                % parenttype,
     
    14795
    14896                headers = req.args.getlist('headers')
    149                 self.config.set('childtickets',
     97                self.config.set('relations-child',
    15098                                'parent.%s.table_headers' % parenttype,
    15199                                ','.join(headers))
    152100
    153101                restricted = req.args.getlist('restricted')
    154                 self.config.set('childtickets',
     102                self.config.set('relations-child',
    155103                                'parent.%s.restrict_child_type' % parenttype,
    156104                                ','.join(restricted))
    157105
    158106                inherited = req.args.getlist('inherited')
    159                 self.config.set('childtickets',
     107                self.config.set('relations-child',
    160108                                'parent.%s.inherit' % parenttype,
    161109                                ','.join(inherited))
     
    177125        else:
    178126            data = {
    179                 'custom_field': self.ticket_custom_field_exists(),
    180                 'custom_field_label': _('Parent'),
    181127                'view': 'list',
    182128                'base_href': req.href.admin(cat, page),
     
    186132
    187133        # Add our own styles for the ticket lists.
    188         add_stylesheet(req, 'ct/css/childtickets.css')
    189 
    190         if pre_1_3:
    191             return 'admin_childtickets.html', data
    192         else:
    193             return 'admin_childtickets_jinja.html', data
     134        add_stylesheet(req, 'ticketrelations/css/child_relations.css')
     135
     136        return 'admin_childrelations.html', data
    194137
    195138    # ITemplateProvider methods
     139
    196140    def get_templates_dirs(self):
    197         from pkg_resources import resource_filename
     141        self.log.info(resource_filename(__name__, 'templates'))
    198142        return [resource_filename(__name__, 'templates')]
    199143
    200144    def get_htdocs_dirs(self):
    201         from pkg_resources import resource_filename
    202         return [('ct', resource_filename(__name__, 'htdocs'))]
     145        return [('ticketrelations', resource_filename(__name__, 'htdocs'))]
    203146
    204147    # Custom methods
     148
    205149    def _headers(self, ptype):
    206150        """Returns a list of valid headers for the given parent type.
     
    241185
    242186
     187class ChildTicketsModule(Component):
     188    """Component which inserts the child ticket data into the ticket page"""
     189
     190    implements(IRequestFilter, ITemplateProvider, ITicketChangeListener,
     191               ITicketManipulator)
     192
     193    max_view_depth = IntOption('Relations-child', 'max_view_depth', default=3,
     194                               doc="Maximum depth of child ticket tree shown on the ticket page.")
     195
     196    # IRequestFilter methods
     197
     198    def pre_process_request(self, req, handler):
     199
     200        return handler
     201
     202    def post_process_request(self, req, template, data, content_type):
     203
     204        if req.path_info == '/newticket':
     205            pass
     206
     207        if data and template in ('ticket.html', 'ticket_box.html'):
     208
     209            ticket = data.get('ticket')
     210
     211            if ticket:
     212                filter_lst = []
     213                parent_id = ticket['relationdata']
     214                rendered_parent = format_to_oneliner(self.env, data['context'], '#%s' % parent_id)
     215                if 'fields' in data:
     216                    # When creating a newticket show read-only fields with an appropriate label for
     217                    # the custom field 'relationdata'.
     218                    # When showing a ticket after creation hide the custom field.
     219                    #
     220                    # The custom  field holds the parent ticket id and is needed so the
     221                    # parent id ends up in the change listener 'ticket_created()' where we can create
     222                    # the relation in the database.
     223                    field = data['fields'].by_name('relationdata')
     224                    if field:
     225                        if ticket.exists or not parent_id:
     226                            field['skip'] = True
     227                        else:
     228                            field['rendered'] = rendered_parent
     229                            field['label'] = _("Child of")
     230                            # replace the input field with hidden one and text so the user can't change the parent.
     231                            xform = JTransformer('input#field-relationdata')
     232                            filter_lst.append(xform.replace('<input type="hidden" id="field-relationdata" '
     233                                                            'name="field_relationdata" '
     234                                                            'value="{tkt}"/>'.format(tkt=parent_id) +
     235                                                            to_unicode(rendered_parent)))
     236
     237                if ticket.exists:
     238                    buttons = self.create_child_ticket_buttons(req, ticket)
     239
     240                    if buttons:
     241                        # xpath: //div[@id="ticket"]
     242                        xform = JTransformer('div#ticket')
     243                        filter_lst.append(xform.after(to_unicode(buttons)))
     244
     245                add_stylesheet(req, 'ticketrelations/css/child_relations.css')
     246                add_script_data(req, {'childrels_filter': filter_lst})
     247                add_script(req, 'ticketrelations/js/childrels_jtransform.js')
     248
     249        return template, data, content_type
     250
     251    def create_child_ticket_buttons(self, req, ticket):
     252        """Create the button div holding buttons for creating child tickets."""
     253
     254        # Are child tickets allowed?
     255        childtickets_allowed = self.config.getbool('relations-child', 'parent.%s.allow_child_tickets' % ticket['type'])
     256
     257        if childtickets_allowed and 'TICKET_CREATE' in req.perm(ticket.resource):
     258
     259            # Always pass these fields, e.g. the parent ticket id
     260            default_child_fields = (tag.input(type="hidden", name="relationdata", value=str(ticket.id)),)
     261
     262            # Pass extra fields defined in inherit parameter of parent
     263            inherited_child_fields = [
     264                tag.input(type="hidden", name="%s" % field, value=ticket[field]) for field in
     265                self.config.getlist('childtickets', 'parent.%s.inherit' % ticket['type'])
     266            ]
     267
     268            # If child types are restricted then create a set of buttons for the allowed types (This will override 'default_child_type).
     269            restrict_child_types = self.config.getlist('relations-child',
     270                                                       'parent.%s.restrict_child_type' % ticket['type'],
     271                                                       default=[])
     272
     273            if not restrict_child_types:
     274                # trac.ini : Default 'type' of child tickets?
     275                default_child_type = self.config.get('relations-child',
     276                                                     'parent.%s.default_child_type' % ticket['type'],
     277                                                     default=self.config.get('ticket', 'default_type'))
     278
     279                # ... create a default submit button
     280                if ticket['status'] == 'closed':
     281                    submit_button_fields = (
     282                        tag.input(type="submit", disabled="disabled", name="childticket",
     283                                  value="New Child Ticket", title="Create a child ticket"),
     284                        tag.input(type="hidden", name="type", value=default_child_type),)
     285                else:
     286                    submit_button_fields = (
     287                        tag.input(type="submit", name="childticket", value="New Child Ticket",
     288                                  title="Create a child ticket"),
     289                        tag.input(type="hidden", name="type", value=default_child_type),)
     290            else:
     291                if ticket['status'] == 'closed':
     292                    submit_button_fields = [
     293                        tag.input(type="submit", disabled="disabled", name="type", value="%s" % ticket_type,
     294                                  title="Create a %s child ticket" % ticket_type) for ticket_type in
     295                        restrict_child_types]
     296                else:
     297                    submit_button_fields = [tag.input(type="submit", name="type", value="%s" % ticket_type,
     298                                                      title="Create a %s child ticket" % ticket_type) for
     299                                            ticket_type in restrict_child_types]
     300
     301            buttonform = tag.form(tag.p(_("Create New Ticket")),
     302                                  tag.div(default_child_fields, inherited_child_fields, submit_button_fields),
     303                                  method="get", action=req.href.newticket(),
     304                                  class_="child-trelations-form", )
     305            return to_unicode(buttonform)
     306        return ''
     307
     308    # ITicketManipulator methods
     309
     310    def prepare_ticket(self, req, ticket, fields, actions):
     311        pass
     312
     313    def validate_ticket(self, req, ticket):
     314        """Validate ticket properties when creating or modifying.
     315
     316        Must return a list of `(field, message)` tuples, one for each problem
     317        detected. `field` can be `None` to indicate an overall problem with the
     318        ticket. Therefore, a return value of `[]` means everything is OK."""
     319
     320        # This custom field is used to transfer data from e.g. a parent ticket
     321        # to a newly created child ticket. There is no other way to get that information
     322        # from a req to the created ticket.
     323        # Clear the custom field so if we reuse it for more than one transfer we
     324        # don't get any change entries in the ticket history.
     325        # In the change listener on may use the data from the 'tracrelation' which
     326        # is not saved in the database.
     327        # if ticket['relationdata']:
     328        #     self.log.info('##### have relationdata: %s' % ticket['relationdata'])
     329        #     ticket['tracrelation'] = ticket['relationdata']
     330        #   # ticket['relationdata'] = None
     331
     332        return []
     333
     334    def validate_comment(self, req, comment):
     335        return []
     336
     337    # ITicketChangeListener methods
     338
     339    def ticket_changed(self, ticket, comment, author, old_values):
     340        """Called when a ticket is modified.
     341
     342        `old_values` is a dictionary containing the previous values of the
     343        fields that have changed.
     344        """
     345        pass
     346
     347    def ticket_created(self, ticket):
     348        """Called when a ticket is created."""
     349        # If we are a child ticket than create the relation in the database.
     350        if ticket['relationdata']:
     351            rel = Relation(self.env, 'ticket', src=ticket['relationdata'],
     352                           dest=ticket.id, type='parentchild')
     353            RelationSystem.add_relation(self.env, rel)
     354
     355    def ticket_deleted(self, ticket):
     356        pass
     357
     358    def ticket_comment_modified(ticket, cdate, author, comment, old_comment):
     359        """Called when a ticket comment is modified."""
     360        pass
     361
     362    def ticket_change_deleted(ticket, cdate, changes):
     363        """Called when a ticket change is deleted.
     364
     365        `changes` is a dictionary of tuple `(oldvalue, newvalue)`
     366        containing the ticket change of the fields that have changed."""
     367        pass
     368
     369    # ITemplateProvider methods
     370
     371    def get_templates_dirs(self):
     372        self.log.info(resource_filename(__name__, 'templates'))
     373        return [resource_filename(__name__, 'templates')]
     374
     375    def get_htdocs_dirs(self):
     376        return [('ticketrelations', resource_filename(__name__, 'htdocs'))]
     377
     378
    243379class ParentType(object):
    244380    def __init__(self, config, name, field_names):
     
    252388    @property
    253389    def allow_child_tickets(self):
    254         return self.config.getbool('childtickets',
     390        return self.config.getbool('relations-child',
    255391                                   'parent.%s.allow_child_tickets' % self.name,
    256392                                   default=False)
     
    258394    @property
    259395    def table_headers(self):
    260         hdrs = self.config.getlist('childtickets',
     396        hdrs = self.config.getlist('relations-child',
    261397                                   'parent.%s.table_headers' % self.name,
    262398                                   default=['summary', 'owner'])
     
    266402    @property
    267403    def restrict_to_child_types(self):
    268         return self.config.getlist('childtickets',
     404        return self.config.getlist('relations-child',
    269405                                   'parent.%s.restrict_child_type' % self.name,
    270406                                   default=[])
     
    272408    @property
    273409    def inherited_fields(self):
    274         return self.config.getlist('childtickets',
     410        return self.config.getlist('relations-child',
    275411                                   'parent.%s.inherit' % self.name,
    276412                                   default=[])
     
    278414    @property
    279415    def default_child_type(self):
    280         return self.config.get('childtickets',
     416        return self.config.get('relations-child',
    281417                               'parent.%s.default_child_type' % self.name,
    282418                               default=self.config.get('ticket',
  • tracrelationsplugin/trunk/tracrelations/htdocs/js/childrels_jtransform.js

    r18113 r18217  
    3737
    3838  /* This is from the SmpVersionRoadmap */
    39   if(typeof childtkt_filter !== 'undefined'){
    40       apply_transform(childtkt_filter);
     39  if(typeof childrels_filter !== 'undefined'){
     40      apply_transform(childrels_filter);
    4141  };
    4242});
  • tracrelationsplugin/trunk/tracrelations/templates/admin_childrelations.html

    r18128 r18217  
    9494
    9595        <h2>${_("Parent Types")} <span class="trac-count">(${len(ticket_types)})</span></h2>
    96         # if not custom_field:
    97         <div>
    98             <form action="" method="POST">
    99                 ${jmacros.form_token_input()}
    100                 <fieldset>
    101                     <legend>${_("Ticket custom field")}</legend>
    102                     <div class="system-message warning">${_("Ticket custom field 'parent' not configured. See installation instructions.")}</div>
    103                     <p>${_("Do you want to create the ticket custom field now?")}</p>
    104                     <div class="field">
    105                         <label>
    106                             ${_("Label for field: ")}
    107                             <input type="text" name="custom-field-label-val" value="${custom_field_label}"/>
    108                         </label>
    109                     </div>
    110                     <p class="help">${_("Without the proper ticket custom field it's not possible to link tickets to parents.")}</p>
    111                     # if 'TICKET_ADMIN' in perm:
    112                     <div>
    113                         <div class="buttons">
    114                             <input type="submit" name="create-ticket-custom" value="${_('Create')}"/>
    115                         </div>
    116                     </div>
    117                     # endif
    118                 </fieldset>
    119             </form>
    120         </div>
    121         # endif
    12296
    12397        <form id="childtickets_table" method="post" action="">
  • tracrelationsplugin/trunk/tracrelations/ticket.py

    r18208 r18217  
    126126        return []
    127127
    128 
    129128    def create_manage_relations_dialog(self):
    130129        tmpl = u"""<div id="manage-rel-dialog" title="Manage Relations" style="display: none">
     
    208207                have_links = False
    209208                if 'fields' in data:
    210                     field = data['fields'].by_name('relations')
    211                     if field:
    212                         pass
    213                     else:
    214                         tkt.values['relations'], have_links = self.create_relations_wiki(req, tkt)  # Activates field
    215                         data['fields'].append({
    216                             'name': 'relations',
    217                             'label': 'Relations',
    218                             # 'rendered': html,  # format_to_html(self.env, web_context(req, tkt.resource), '== Baz\n' + tst_wiki),
    219                             # 'editable': False,
    220                             # 'value': "",
    221                             'type': 'textarea',  # Full row
    222                             'format': 'wiki'
    223                         })
     209                    # Create a temporary field for display only
     210                    tkt.values['relations'], have_links = self.create_relations_wiki(req, tkt)  # Activates field
     211                    data['fields'].append({
     212                        'name': 'relations',
     213                        'label': 'Relations',
     214                        # 'rendered': html,  # format_to_html(self.env, web_context(req, tkt.resource), '== Baz\n' + tst_wiki),
     215                        # 'editable': False,
     216                        # 'value': "",
     217                        'type': 'textarea',  # Full row
     218                        'format': 'wiki'
     219                    })
    224220
    225221                filter_lst = []
    226222
    227                 # Prepare the manage dialog: 'modify' button and dialog div for jquery-ui.
     223                # Prepare the 'modify' button and manage dialog div for jquery-ui.
    228224                xform = JTransformer('table.properties #h_relations')
    229225                filter_lst.append(xform.prepend(self.create_relation_manage_form(tkt, have_links)))
Note: See TracChangeset for help on using the changeset viewer.