Changeset 18217
- Timestamp:
- Apr 14, 2021, 11:22:34 AM (4 years ago)
- Location:
- tracrelationsplugin/trunk/tracrelations
- Files:
-
- 2 edited
- 4 copied
Legend:
- Unmodified
- Added
- Removed
-
tracrelationsplugin/trunk/tracrelations/__init__.py
r18194 r18217 1 1 from .api import * 2 from .childrelations import * 2 3 from .ticket import * -
tracrelationsplugin/trunk/tracrelations/childrelations.py
r18129 r18217 9 9 # you should have received as part of this distribution. 10 10 # 11 from pkg_resources import get_distribution, parse_version 11 from pkg_resources import get_distribution, parse_version, resource_filename 12 12 from trac.admin import IAdminPanelProvider 13 from trac.config import IntOption 14 from trac.env import IEnvironmentSetupParticipant 13 15 from trac.core import * 14 from trac.ticket.api import TicketSystem16 from trac.ticket.api import ITicketChangeListener, ITicketManipulator, TicketSystem 15 17 from trac.ticket.model import Type 16 from trac.util.text import exception_to_unicode 18 from trac.util.html import tag 19 from trac.util.text import exception_to_unicode, to_unicode 17 20 from trac.util.translation import _ 21 from trac.web.api import IRequestFilter 18 22 from 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') 23 from trac.web.chrome import add_notice, add_script, add_script_data, add_stylesheet, add_warning 24 from trac.wiki.formatter import format_to_oneliner 25 26 from tracrelations.api import RelationSystem 27 from tracrelations.jtransform import JTransformer 28 from tracrelations.model import Relation 29 25 30 26 31 def _save_config(config, req, log): … … 38 43 39 44 40 class Child TicketsAdminPanel(Component):45 class ChildRelationsAdminPanel(Component): 41 46 """Configure which ticket types allow children, inherited fields for children and more. 42 47 … … 45 50 46 51 The following global settings are available: 47 [[TracIni( childtickets)]]52 [[TracIni(relations-child)]] 48 53 49 54 You may specify features for each ticket type. In this example the configuration 50 55 is for tickets of type {{{defect}}}: 51 56 {{{#!ini 52 [ childtickets]57 [relations-child] 53 58 parent.defect.allow_child_tickets = True 54 59 parent.defect.inherit = description,milestone,summary,project,version … … 64 69 implements(IAdminPanelProvider, ITemplateProvider) 65 70 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 type70 71 We don't check for proper custom field type here.72 """73 return self.config.get('ticket-custom', 'parent', None)74 75 71 # IAdminPanelProvider methods 76 72 77 73 def get_admin_panels(self, req): 78 if 'TICKET_ADMIN' in req.perm('admin', 'child ticketsplugin/types'):79 yield ('child ticketsplugin', _('Child Tickets'), 'types',74 if 'TICKET_ADMIN' in req.perm('admin', 'childrelations/types'): 75 yield ('childrelations', _('Relations'), 'types', 80 76 _('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', data117 else:118 return 'admin_ct_basics_jinja.html', data119 77 120 78 def render_admin_panel(self, req, cat, page, parenttype): 121 79 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') 136 83 137 84 field_names = TicketSystem(self.env).get_ticket_field_labels() 85 138 86 # Detail view? 139 87 if parenttype: … … 141 89 allow_child_tickets = \ 142 90 req.args.get('allow_child_tickets') 143 self.config.set(' childtickets',91 self.config.set('relations-child', 144 92 'parent.%s.allow_child_tickets' 145 93 % parenttype, … … 147 95 148 96 headers = req.args.getlist('headers') 149 self.config.set(' childtickets',97 self.config.set('relations-child', 150 98 'parent.%s.table_headers' % parenttype, 151 99 ','.join(headers)) 152 100 153 101 restricted = req.args.getlist('restricted') 154 self.config.set(' childtickets',102 self.config.set('relations-child', 155 103 'parent.%s.restrict_child_type' % parenttype, 156 104 ','.join(restricted)) 157 105 158 106 inherited = req.args.getlist('inherited') 159 self.config.set(' childtickets',107 self.config.set('relations-child', 160 108 'parent.%s.inherit' % parenttype, 161 109 ','.join(inherited)) … … 177 125 else: 178 126 data = { 179 'custom_field': self.ticket_custom_field_exists(),180 'custom_field_label': _('Parent'),181 127 'view': 'list', 182 128 'base_href': req.href.admin(cat, page), … … 186 132 187 133 # 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 194 137 195 138 # ITemplateProvider methods 139 196 140 def get_templates_dirs(self): 197 from pkg_resources import resource_filename141 self.log.info(resource_filename(__name__, 'templates')) 198 142 return [resource_filename(__name__, 'templates')] 199 143 200 144 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'))] 203 146 204 147 # Custom methods 148 205 149 def _headers(self, ptype): 206 150 """Returns a list of valid headers for the given parent type. … … 241 185 242 186 187 class 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 243 379 class ParentType(object): 244 380 def __init__(self, config, name, field_names): … … 252 388 @property 253 389 def allow_child_tickets(self): 254 return self.config.getbool(' childtickets',390 return self.config.getbool('relations-child', 255 391 'parent.%s.allow_child_tickets' % self.name, 256 392 default=False) … … 258 394 @property 259 395 def table_headers(self): 260 hdrs = self.config.getlist(' childtickets',396 hdrs = self.config.getlist('relations-child', 261 397 'parent.%s.table_headers' % self.name, 262 398 default=['summary', 'owner']) … … 266 402 @property 267 403 def restrict_to_child_types(self): 268 return self.config.getlist(' childtickets',404 return self.config.getlist('relations-child', 269 405 'parent.%s.restrict_child_type' % self.name, 270 406 default=[]) … … 272 408 @property 273 409 def inherited_fields(self): 274 return self.config.getlist(' childtickets',410 return self.config.getlist('relations-child', 275 411 'parent.%s.inherit' % self.name, 276 412 default=[]) … … 278 414 @property 279 415 def default_child_type(self): 280 return self.config.get(' childtickets',416 return self.config.get('relations-child', 281 417 'parent.%s.default_child_type' % self.name, 282 418 default=self.config.get('ticket', -
tracrelationsplugin/trunk/tracrelations/htdocs/js/childrels_jtransform.js
r18113 r18217 37 37 38 38 /* This is from the SmpVersionRoadmap */ 39 if(typeof child tkt_filter !== 'undefined'){40 apply_transform(child tkt_filter);39 if(typeof childrels_filter !== 'undefined'){ 40 apply_transform(childrels_filter); 41 41 }; 42 42 }); -
tracrelationsplugin/trunk/tracrelations/templates/admin_childrelations.html
r18128 r18217 94 94 95 95 <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 # endif118 </fieldset>119 </form>120 </div>121 # endif122 96 123 97 <form id="childtickets_table" method="post" action=""> -
tracrelationsplugin/trunk/tracrelations/ticket.py
r18208 r18217 126 126 return [] 127 127 128 129 128 def create_manage_relations_dialog(self): 130 129 tmpl = u"""<div id="manage-rel-dialog" title="Manage Relations" style="display: none"> … … 208 207 have_links = False 209 208 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 }) 224 220 225 221 filter_lst = [] 226 222 227 # Prepare the manage dialog: 'modify' button anddialog div for jquery-ui.223 # Prepare the 'modify' button and manage dialog div for jquery-ui. 228 224 xform = JTransformer('table.properties #h_relations') 229 225 filter_lst.append(xform.prepend(self.create_relation_manage_form(tkt, have_links)))
Note: See TracChangeset
for help on using the changeset viewer.