Ticket #548: subcomponent.patch
| File subcomponent.patch, 30.8 kB (added by endre@…, 8 months ago) |
|---|
-
trac/admin/templates/admin_subcomponents.html
1 <!DOCTYPE html 2 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 3 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 4 <html xmlns="http://www.w3.org/1999/xhtml" 5 xmlns:xi="http://www.w3.org/2001/XInclude" 6 xmlns:py="http://genshi.edgewall.org/"> 7 <xi:include href="admin.html" /> 8 <head> 9 <title>Sub-Components</title> 10 </head> 11 12 <body> 13 <h2>Manage Sub-Components</h2> 14 15 <py:def function="owner_field(default_owner='')"> 16 <div class="field"> 17 <label>Owner: <br /> 18 <py:choose> 19 <select py:when="owners" size="1" id="owner" name="owner"> 20 <option py:for="owner in owners" 21 selected="${owner==default_owner or None}">$owner</option> 22 <option py:if="default_owner and default_owner not in owners" 23 selected="selected">$default_owner</option> 24 </select> 25 <input py:otherwise="" type="text" name="owner" value="$default_owner" /> 26 </py:choose> 27 </label> 28 </div> 29 </py:def> 30 31 <py:choose test="view"> 32 <form py:when="'detail'" class="mod" id="submodcomp" method="post"> 33 <fieldset> 34 <legend>Modify Sub-Component:</legend> 35 <div class="field"> 36 <label>Component:<br /><input type="text" name="parent" value="${subcomponent.parent}" disabled="disabled" /></label> 37 </div> 38 <div class="field"> 39 <label>Name:<br /><input type="text" name="name" value="${subcomponent.name}" /></label> 40 </div> 41 ${owner_field(subcomponent.owner)} 42 <div class="field"> 43 <fieldset class="iefix"> 44 <label for="description"> 45 Description (you may use 46 <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> 47 here): 48 </label> 49 <p> 50 <textarea id="description" name="description" class="wikitext" 51 rows="6" cols="60"> 52 $subcomponent.description</textarea> 53 </p> 54 </fieldset> 55 </div> 56 <div class="buttons"> 57 <input type="submit" name="cancel" value="Cancel" /> 58 <input type="submit" name="save" value="Save" /> 59 </div> 60 </fieldset> 61 </form> 62 63 <py:otherwise> 64 <form class="addnew" id="addsubcomp" method="post"> 65 <fieldset> 66 <legend>Add Subcomponent:</legend> 67 <input type="hidden" name="parent" value="${parent}" /> 68 <div class="field"> 69 <label>Name:<br /><input type="text" name="name" /></label> 70 </div> 71 ${owner_field()} 72 <div class="buttons"> 73 <input type="submit" name="add" value="Add"/> 74 </div> 75 </fieldset> 76 </form> 77 78 <py:choose> 79 <form py:when="subcomponents" method="POST"> 80 Component: <select id="parent" name="parent" onchange="this.form.submit()"> 81 <option py:for="component in components" 82 selected="${parent == component.name or None}">${component.name}</option> 83 </select> 84 <table class="listing" id="subcomplist"> 85 <thead> 86 <tr><th class="sel"> </th> 87 <th>Name</th><th>Owner</th><th>Default</th> 88 </tr> 89 </thead> 90 <tbody> 91 <tr py:for="scomp in subcomponents"> 92 <td class="sel"><input type="checkbox" name="sel" value="$scomp.name" /></td> 93 <td class="name"> 94 <a href="${panel_href(parent + '/' + scomp.name)}">$scomp.name</a> 95 </td> 96 <td class="owner">$scomp.owner</td> 97 <td class="default"> 98 <input type="radio" name="default" value="$scomp.name" 99 checked="${scomp.name==default or None}" /> 100 </td> 101 </tr> 102 </tbody> 103 </table> 104 <div class="buttons"> 105 <input type="submit" name="remove" value="Remove selected items" /> 106 <input type="submit" name="apply" value="Apply changes" /> 107 </div> 108 <p class="help"> 109 You can remove all items from this list to completely hide this 110 field from the user interface. 111 </p> 112 </form> 113 114 <p py:otherwise="" class="help"> 115 As long as you don't add any items to the list, this field 116 will remain completely hidden from the user interface. 117 </p> 118 </py:choose> 119 </py:otherwise> 120 </py:choose> 121 </body> 122 123 </html> -
trac/db_default.py
17 17 from trac.db import Table, Column, Index 18 18 19 19 # Database version identifier. Used for automatic upgrades. 20 db_version = 2 120 db_version = 22 21 21 22 22 def __mkreports(reports): 23 23 """Utility function used to create report data in same syntax as the … … 104 104 Column('time', type='int'), 105 105 Column('changetime', type='int'), 106 106 Column('component'), 107 Column('subcomponent'), 107 108 Column('severity'), 108 109 Column('priority'), 109 110 Column('owner'), … … 139 140 Column('name'), 140 141 Column('owner'), 141 142 Column('description')], 143 Table('subcomponent', key=('parent', 'name'))[ 144 Column('parent'), 145 Column('name'), 146 Column('owner'), 147 Column('description')], 142 148 Table('milestone', key='name')[ 143 149 Column('name'), 144 150 Column('due', type='int'), … … 335 341 ('name', 'owner'), 336 342 (('component1', 'somebody'), 337 343 ('component2', 'somebody'))), 338 ('milestone', 344 ('subcomponent', 345 ('parent', 'name', 'owner'), 346 (('component1', 'subcomponent1-1', 'somebody'), 347 ('component1', 'subcomponent1-2', 'somebody'), 348 ('component2', 'subcomponent2-1', 'somebody'), 349 ('component2', 'subcomponent2-2', 'somebody'))), 350 ('milestone', 339 351 ('name', 'due', 'completed'), 340 352 (('milestone1', 0, 0), 341 353 ('milestone2', 0, 0), -
trac/htdocs/js/query.js
157 157 return e; 158 158 } 159 159 160 // Convenience function for creating a <select> dependent on other <select> 161 function createDepSelect(name, options, optional) { 162 var e = document.createElement("select"); 163 if (name) e.name = name; 164 if (optional) e.options[0] = new Option(); 165 if (options) { 166 for (var i = 0; i < options.length; i++) { 167 var option; 168 if (typeof(options[i]) == "object") { 169 option = new Option(options[i].text, options[i].value); 170 } else { 171 option = new Option(options[i], options[i]); 172 } 173 e.options[e.options.length] = option; 174 } 175 } 176 return e; 177 } 160 178 var propertyName = select.options[select.selectedIndex].value; 161 179 var property = properties[propertyName]; 162 180 var table = document.getElementById("filters").getElementsByTagName("table")[0]; … … 219 237 td.className = "filter"; 220 238 if (property.type == "select") { 221 239 var element = createSelect(propertyName, property.options, true); 240 } else if (property.type == "depselect") { 241 var element = createDepSelect(propertyName, property.options, true); 222 242 } else if (property.type == "text") { 223 243 var element = document.createElement("input"); 224 244 element.type = "text"; -
trac/htdocs/js/sublist.js
1 /* 2 Based on code found at: 3 http://www.ajaxray.com/blog/2007/11/08/jquery-controlled-dependent-or-cascading-select-list-2/ 4 */ 5 function makeSublist(parent,child,isSubselectOptional,childVal) 6 { 7 $("body").append("<select style='display:none' id='" + parent + child + "'></select>"); 8 $('#'+parent+child).html($("#"+child+" option")); 9 10 var parentValue = $('#'+parent).attr('value'); 11 $('#'+child).html($("#"+parent+child+" .sub_"+parentValue).clone()); 12 13 childVal = (typeof childVal == "undefined")? "" : childVal ; 14 if(isSubselectOptional) $('#'+child).prepend("<option></option>"); 15 $("#"+child+' option[@value="'+ childVal +'"]').attr('selected','selected'); 16 $('#'+parent).change( 17 function() 18 { 19 var parentValue = $('#'+parent).attr('value'); 20 $('#'+child).html($("#"+parent+child+" .sub_"+parentValue).clone()); 21 if(isSubselectOptional) $('#'+child).prepend("<option></option>"); 22 $('#'+child+' option:first').attr('selected', 'selected'); 23 $('#'+child).trigger("change"); 24 $('#'+child).focus(); 25 } 26 ); 27 } -
trac/ticket/admin.py
12 12 # history and logs, available at http://trac.edgewall.org/. 13 13 14 14 from datetime import datetime 15 import re 15 16 16 17 from trac.admin import IAdminPanelProvider 17 18 from trac.core import * … … 119 120 return 'admin_components.html', data 120 121 121 122 123 class SubcomponentAdminPage(TicketAdminPage): 124 125 _type = 'subcomponents' 126 _label = ('Subcomponent', 'Subcomponents') 127 128 # TicketAdminPage methods 129 130 def _render_admin_panel(self, req, cat, page, subcomponent): 131 match = None 132 # Detail view? 133 if subcomponent: 134 match = re.match('([^/]+)/(.*)$', subcomponent) # Looking for pattern <component>/<subcomponent> in url 135 136 if match: # Detail view 137 subcomp = model.Subcomponent(self.env, match.group(2), match.group(1)) 138 if req.method == 'POST': 139 if req.args.get('save'): 140 subcomp.name = req.args.get('name') 141 subcomp.owner = req.args.get('owner') 142 subcomp.description = req.args.get('description') 143 subcomp.update() 144 req.redirect(req.href.admin(cat, page, match.group(1))) 145 elif req.args.get('cancel'): 146 req.redirect(req.href.admin(cat, page, match.group(1))) 147 148 add_script(req, 'common/js/wikitoolbar.js') 149 data = {'view': 'detail', 'subcomponent': subcomp} 150 151 else: 152 if req.method == 'POST': 153 # Add Component 154 if req.args.get('add') and req.args.get('name') and req.args.get('parent'): 155 subcomp = model.Subcomponent(self.env) 156 subcomp.name = req.args.get('name') 157 subcomp.parent = req.args.get('parent') 158 if req.args.get('owner'): 159 subcomp.owner = req.args.get('owner') 160 subcomp.insert() 161 req.redirect(req.href.admin(cat, page, subcomp.parent)) 162 163 # Remove components 164 elif req.args.get('remove') and req.args.get('sel'): 165 sel = req.args.get('sel') 166 sel = isinstance(sel, list) and sel or [sel] 167 parent = req.args.get('parent') 168 if not sel: 169 raise TracError(_('No subcomponent selected')) 170 db = self.env.get_db_cnx() 171 for subcompname in sel: 172 subcomp = model.Subcomponent(self.env, subcompname, parent, db=db) 173 subcomp.delete(db=db) 174 db.commit() 175 req.redirect(req.href.admin(cat, page, req.args.get('parent'))) 176 177 # Set default component 178 elif req.args.get('apply'): 179 if req.args.get('default'): 180 name = req.args.get('default') 181 self.log.info('Setting default subcomponent to %s', name) 182 # Todo: One default for each parent? 183 self.config.set('ticket', 'default_subcomponent', name) 184 self.config.save() 185 req.redirect(req.href.admin(cat, page, req.args.get('parent'))) 186 187 # Changed selected parent 188 elif req.args.get('parent'): 189 req.redirect(req.href.admin(cat, page, req.args.get('parent'))) 190 191 components = list(model.Component.select(self.env)) 192 if subcomponent: 193 parent = subcomponent # Catches redirects 194 else: 195 parent = components[0].name # Just use first in list as default 196 197 default = self.config.get('ticket', 'default_subcomponent') 198 data = {'view': 'list', 199 'components': components, 200 'subcomponents': model.Subcomponent.select(self.env, parent=parent), 201 'parent': parent, 202 'default': default} 203 204 if self.config.getbool('ticket', 'restrict_owner'): 205 perm = PermissionSystem(self.env) 206 def valid_owner(username): 207 return perm.get_user_permissions(username).get('TICKET_MODIFY') 208 data['owners'] = [username for username, name, email 209 in self.env.get_known_users() 210 if valid_owner(username)] 211 else: 212 data['owners'] = None 213 214 return 'admin_subcomponents.html', data 215 216 122 217 class MilestoneAdminPage(TicketAdminPage): 123 218 124 219 _type = 'milestones' -
trac/ticket/api.py
227 227 elif name in ('milestone', 'version'): 228 228 field['optional'] = True 229 229 fields.append(field) 230 231 # Dependent select fields 232 depselects = [('subcomponent', model.Subcomponent, 'component')] 233 for name, cls, parent in depselects: 234 options = [(val.name, val.parent) for val in cls.select(self.env, db=db)] 235 if not options: 236 continue 237 field = {'name': name, 'type': 'depselect', 'label': name.title(), 238 'value': self.config.get('ticket', 'default_' + name), 239 'options': options} 240 field['optional'] = True 241 fields.append(field) 242 243 230 244 231 245 # Advanced text fields 232 246 for name in ('keywords', 'cc', ): -
trac/ticket/model.py
31 31 from trac.util.translation import _ 32 32 33 33 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity', 34 'Component', ' Milestone', 'Version']34 'Component', 'Subcomponent', 'Milestone', 'Version'] 35 35 36 36 37 37 class Ticket(object): … … 149 149 150 150 cursor = db.cursor() 151 151 152 # The owner field defaults to the component owner152 # The owner field defaults to the sub-component owner or component owner if sub-component doesn't exist 153 153 if self.values.get('component') and not self.values.get('owner'): 154 try: 155 component = Component(self.env, self['component'], db=db) 156 if component.owner: 157 self['owner'] = component.owner 158 except TracError, e: 159 # Assume that no such component exists 160 pass 154 foundowner = False 155 # Try to get owner field from sub-component first 156 if self.values.get('subcomponent'): 157 try: 158 subcomponent = Subcomponent(self.env, self['subcomponent'], self['component'], db=db) 159 if subcomponent.owner: 160 self['owner'] = subcomponent.owner 161 foundowner = True 162 except TracError, e: 163 pass 164 # If no owner found, try component 165 if not foundowner: 166 try: 167 component = Component(self.env, self['component'], db=db) 168 if component.owner: 169 self['owner'] = component.owner 170 except TracError, e: 171 # Assume that no such component exists 172 pass 161 173 162 174 # Insert ticket record 163 175 created = to_timestamp(self.time_created) … … 519 531 cursor = db.cursor() 520 532 self.env.log.info('Deleting component %s' % self.name) 521 533 cursor.execute("DELETE FROM component WHERE name=%s", (self.name,)) 534 # Delete all subcomponents while we're at it 535 cursor.execute("DELETE FROM subcomponent WHERE parent=%s", (self.name,)) 522 536 523 537 self.name = self._old_name = None 524 538 … … 564 578 # Update tickets 565 579 cursor.execute("UPDATE ticket SET component=%s WHERE component=%s", 566 580 (self.name, self._old_name)) 581 # Update subcomponents 582 cursor.execute("UPDATE subcomponent SET parent=%s WHERE parent=%s", 583 (self.name, self._old_name)) 567 584 self._old_name = self.name 568 585 569 586 if handle_ta: … … 584 601 select = classmethod(select) 585 602 586 603 604 class Subcomponent(object): 605 606 def __init__(self, env, name=None, parent=None, db=None): 607 self.env = env 608 if name and parent: 609 if not db: 610 db = self.env.get_db_cnx() 611 cursor = db.cursor() 612 cursor.execute("SELECT owner,description FROM subcomponent " 613 "WHERE name=%s AND parent=%s", (name,parent)) 614 row = cursor.fetchone() 615 if not row: 616 raise TracError(_('Subcomponent %(name) does not exist.', 617 name=name)) 618 self.name = self._old_name = name 619 self.parent = self._old_parent = parent 620 self.owner = row[0] or None 621 self.description = row[1] or '' 622 else: 623 self.name = self._old_name = None 624 self.owner = None 625 self.description = None 626 self.parent = self._old_parent = None 627 628 exists = property(fget=lambda self: self._old_name is not None) 629 630 def delete(self, db=None): 631 assert self.exists, 'Cannot deleting non-existent component' 632 if not db: 633 db = self.env.get_db_cnx() 634 handle_ta = True 635 else: 636 handle_ta = False 637 638 cursor = db.cursor() 639 self.env.log.info('Deleting subcomponent %s' % self.name) 640 cursor.execute("DELETE FROM subcomponent WHERE name=%s AND parent=%s", (self.name,self.parent)) 641 642 self.name = self._old_name = None 643 self.parent = self._old_parent = None 644 645 if handle_ta: 646 db.commit() 647 648 def insert(self, db=None): 649 assert not self.exists, 'Cannot insert existing subcomponent' 650 self.name = simplify_whitespace(self.name) 651 self.parent = simplify_whitespace(self.parent) 652 assert self.name, 'Cannot create component with no name' 653 assert self.parent, 'Cannot create subcomponent without a parent' 654 if not db: 655 db = self.env.get_db_cnx() 656 handle_ta = True 657 else: 658 handle_ta = False 659 660 cursor = db.cursor() 661 self.env.log.debug("Creating new subcomponent '%s'" % self.name) 662 cursor.execute("INSERT INTO subcomponent (name,owner,description,parent) " 663 "VALUES (%s,%s,%s,%s)", 664 (self.name, self.owner, self.description, self.parent)) 665 666 if handle_ta: 667 db.commit() 668 669 def update(self, db=None): 670 assert self.exists, 'Cannot update non-existent subcomponent' 671 self.name = simplify_whitespace(self.name) 672 self.parent = simplify_whitespace(self.parent) 673 assert self.name, 'Cannot update subcomponent with no name' 674 assert self.parent, 'Cannot create subcomponent without a parent' 675 if not db: 676 db = self.env.get_db_cnx() 677 handle_ta = True 678 else: 679 handle_ta = False 680 681 cursor = db.cursor() 682 self.env.log.info('Updating subcomponent "%s"' % self.name) 683 cursor.execute("UPDATE subcomponent SET name=%s,owner=%s,description=%s, parent=%s " 684 "WHERE name=%s AND parent=%s", 685 (self.name, self.owner, self.description, self.parent, self._old_name, self._old_parent)) 686 if self.name != self._old_name or self.parent != self._old_parent: 687 # Update tickets 688 cursor.execute("UPDATE ticket SET component=%s, subcomponent=%s WHERE component=%s AND subcomponent=%s", 689 (self.parent, self.name, self._old_parent, self._old_name)) 690 self._old_name = self.name 691 self._old_parent = self.parent 692 693 if handle_ta: 694 db.commit() 695 696 def select(cls, env, db=None, parent=None): 697 if not db: 698 db = env.get_db_cnx() 699 cursor = db.cursor() 700 if parent: 701 cursor.execute("SELECT name,parent,owner,description FROM subcomponent " 702 "WHERE parent=%s ORDER BY name", (parent,)) 703 else: 704 cursor.execute("SELECT name,parent,owner,description FROM subcomponent " 705 "ORDER BY parent,name") 706 for name, parent, owner, description in cursor: 707 subcomponent = cls(env) 708 subcomponent.parent = parent 709 subcomponent.name = name 710 subcomponent.owner = owner or None 711 subcomponent.description = description or '' 712 yield subcomponent 713 select = classmethod(select) 714 715 587 716 class Milestone(object): 588 717 589 718 def __init__(self, env, name=None, db=None): -
trac/ticket/query.py
511 511 {'name': _("is"), 'value': ""}, 512 512 {'name': _("is not"), 'value': "!"} 513 513 ] 514 modes['depselect'] = [ 515 {'name': _("is"), 'value': ""}, 516 {'name': _("is not"), 'value': "!"} 517 ] 514 518 515 519 groups = {} 516 520 groupsequence = [] -
trac/ticket/templates/query.html
75 75 </select> 76 76 </py:when> 77 77 78 <py:when test="'depselect'"> 79 <select name="${constraint_name}"> 80 <option></option> 81 <option py:for="option, parent in field.options" 82 class="sub_$parent" 83 selected="${option == constraint_value and 'selected' or None}">$option 84 </option> 85 </select> 86 </py:when> 87 78 88 <py:when test="'radio'"> 79 89 <py:for each="option in field.options"> 80 90 <input type="checkbox" id="${field_name}_$option" name="${field_name}" … … 150 160 <select name="group" id="group"> 151 161 <option></option> 152 162 <option py:for="field_name, field in fields.items()" 153 py:if="field.type in ('select', ' radio') or field_name == 'owner'"163 py:if="field.type in ('select', 'depselect', 'radio') or field_name == 'owner'" 154 164 selected="${field_name == query.group or None}" 155 165 value="${field_name}">${field.label}</option> 156 166 </select> … … 183 193 <py:for each="(field_name, field), sep in separated(fields.iteritems())"> 184 194 $field_name: { type: "$field.type", label: "$field.label" 185 195 <py:if test="'options' in field">, options: [ 186 <py:for each="option, sep in separated(field.options)">"$option"$sep 187 </py:for>] 196 <py:choose test="field.type"> 197 <py:when test="'depselect'"> 198 <py:for each="option, sep in separated([o] for o, p in field.options)">"$option"$sep 199 </py:for>] 200 </py:when> 201 <py:otherwise> 202 <py:for each="option, sep in separated(field.options)">"$option"$sep 203 </py:for>] 204 </py:otherwise> 205 </py:choose> 188 206 </py:if>}$sep 189 207 </py:for> 190 208 }; -
trac/ticket/templates/ticket.html
19 19 </py:choose> 20 20 </title> 21 21 <script type="text/javascript" src="${chrome.htdocs_location}js/wikitoolbar.js"></script> 22 <script type="text/javascript" py:choose=""> 22 <script type="text/javascript" src="${chrome.htdocs_location}js/sublist.js"></script> 23 <script type="text/javascript" py:choose=""> 23 24 $(document).ready(function() { 25 makeSublist('field-component', 'field-subcomponent', true, '${ticket["subcomponent"]}'); 24 26 $("div.description").find("h1,h2,h3,h4,h5,h6").addAnchor("Link to this section"); 25 27 <py:when test="ticket.exists"> 26 28 $("#changelog h3.change").addAnchor("Link to this change"); … … 39 41 updateActionFields(); 40 42 </py:when> 41 43 <py:otherwise> 42 $(document).ready(function() { $("#field-summary").get(0).focus()});44 $(document).ready(function() {makeSublist('parent', 'child', false, '1'); $("#field-summary").get(0).focus();}); 43 45 </py:otherwise> 44 46 }); 45 47 </script> … … 311 313 <option py:if="field.optional"></option> 312 314 <option py:for="option in field.options" 313 315 selected="${ticket[field.name] == option or None}" 316 value="$option" 314 317 py:content="option"></option> 315 318 <optgroup py:for="optgroup in field.optgroups" 316 319 label="${optgroup.label}"> … … 319 322 py:content="option"></option> 320 323 </optgroup> 321 324 </select> 325 <select py:when="'depselect'" id="field-${field.name}" name="field_${field.name}"> 326 <option py:for="(option, parent) in field.options" 327 class="sub_${parent}" 328 value="$option" 329 &nb
