diff -r f8e1ea02978c -r 3081679abd00 htdocs/css/ticket.css
|
a
|
b
|
|
| 46 | 46 | font-size: 100%; |
| 47 | 47 | font-weight: normal; |
| 48 | 48 | } |
| | 49 | .inlinebuttons input { |
| | 50 | float: right; |
| | 51 | font-size: 70%; |
| | 52 | border-width: 1px; |
| | 53 | margin: 0 .5em .1em 1.5em; |
| | 54 | padding: 0.1em; |
| | 55 | background-color: #f6f6f6; |
| | 56 | } |
| 49 | 57 | #changelog .changes { list-style: square; margin-left: 2em; padding: 0 } |
| 50 | 58 | #changelog .comment { margin-left: 2em } |
| 51 | 59 | |
diff -r f8e1ea02978c -r 3081679abd00 htdocs/css/trac.css
|
a
|
b
|
|
| 40 | 40 | h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited, |
| 41 | 41 | h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited { |
| 42 | 42 | color: inherit; |
| | 43 | } |
| | 44 | |
| | 45 | /* Heading anchors */ |
| | 46 | .anchor:link, .anchor:visited { |
| | 47 | border: none; |
| | 48 | color: #d7d7d7; |
| | 49 | font-size: .8em; |
| | 50 | vertical-align: text-top; |
| | 51 | visibility: hidden; |
| | 52 | } |
| | 53 | h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, |
| | 54 | h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { |
| | 55 | visibility: visible; |
| 43 | 56 | } |
| 44 | 57 | |
| 45 | 58 | @media screen { |
diff -r f8e1ea02978c -r 3081679abd00 htdocs/css/wiki.css
|
a
|
b
|
|
| 21 | 21 | #overview .multi { color: #999 } |
| 22 | 22 | #overview .ipnr { color: #999; font-size: 80% } |
| 23 | 23 | #overview .comment { padding: 1em 0 0 } |
| 24 | | |
| 25 | | /* Heading anchors */ |
| 26 | | .anchor:link, .anchor:visited { |
| 27 | | border: none; |
| 28 | | color: #d7d7d7; |
| 29 | | font-size: .8em; |
| 30 | | vertical-align: text-top; |
| 31 | | visibility: hidden; |
| 32 | | } |
| 33 | | h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, |
| 34 | | h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { |
| 35 | | visibility: visible; |
| 36 | | } |
| 37 | 24 | |
| 38 | 25 | /* Styles for the page history table |
| 39 | 26 | (extends the styles for "table.listing") */ |
diff -r f8e1ea02978c -r 3081679abd00 templates/macros.cs
|
a
|
b
|
|
| 190 | 190 | <input type="submit" value="Attach File" /> |
| 191 | 191 | </div></form><?cs |
| 192 | 192 | /if ?><?cs if:len(attachments) ?></div><?cs /if ?><?cs |
| | 193 | /def ?><?cs |
| | 194 | |
| | 195 | def:plural(base, count) ?><?cs |
| | 196 | var:base ?><?cs if:count > 1 ?>s<?cs /if ?><?cs |
| 193 | 197 | /def ?> |
diff -r f8e1ea02978c -r 3081679abd00 templates/ticket.cs
|
a
|
b
|
|
| 80 | 80 | <?cs call:list_of_attachments(ticket.attachments, ticket.attach_href) ?> |
| 81 | 81 | <?cs /if ?> |
| 82 | 82 | |
| | 83 | <?cs def:commentref(prefix, cnum) ?> |
| | 84 | <a href="#comment:<?cs var:cnum ?>"><small><?cs var:prefix ?><?cs var:cnum ?></small></a> |
| | 85 | <?cs /def ?> |
| | 86 | |
| 83 | 87 | <?cs if:len(ticket.changes) ?><h2>Change History</h2> |
| 84 | 88 | <div id="changelog"><?cs |
| 85 | 89 | each:change = ticket.changes ?> |
| 86 | | <h3 id="change_<?cs var:name(change) ?>" class="change"><?cs |
| 87 | | var:change.date ?>: Modified by <?cs var:change.author ?></h3><?cs |
| | 90 | <h3 <?cs if:change.cnum ?>id="comment:<?cs var:change.cnum ?>"<?cs /if ?> |
| | 91 | class="change"><?cs |
| | 92 | var:change.date ?>: |
| | 93 | Modification <a href="#comment:<?cs var:change.cnum ?>" |
| | 94 | title="Permalink to comment:<?cs var:change.cnum ?>"> |
| | 95 | <?cs var:change.cnum ?></a> |
| | 96 | by <?cs var:change.author ?> <?cs |
| | 97 | if:change.cnum ?><?cs |
| | 98 | set:nreplies = len(ticket.replies[change.cnum]) ?><?cs |
| | 99 | if:nreplies || change.replyto ?><span class="threading"> — <?cs |
| | 100 | if:change.replyto ?>in reply to: <?cs |
| | 101 | call:commentref('⇑', change.replyto) ?><?cs if nreplies ?> – <?cs /if ?><?cs |
| | 102 | /if ?><?cs |
| | 103 | if nreplies ?><?cs |
| | 104 | call:plural('follow-up', nreplies) ?>: <?cs |
| | 105 | each:reply = ticket.replies[change.cnum] ?><?cs |
| | 106 | call:commentref('⇓', reply) ?><?cs |
| | 107 | /each ?><?cs |
| | 108 | /if ?></span><?cs |
| | 109 | /if ?><?cs |
| | 110 | /if ?></h3><?cs |
| | 111 | if:change.cnum ?> |
| | 112 | <form method="get" action="<?cs var:ticket.href ?>#comment"><div class="inlinebuttons"> |
| | 113 | <input type="hidden" name="replyto" value="<?cs var:change.cnum ?>" /> |
| | 114 | <input type="submit" value="Reply" title="Reply to comment <?cs var:change.cnum ?>" /></div> |
| | 115 | </form><?cs |
| | 116 | /if ?><?cs |
| 88 | 117 | if:len(change.fields) ?> |
| 89 | 118 | <ul class="changes"><?cs |
| 90 | 119 | each:field = change.fields ?> |
| … |
… |
|
| 277 | 306 | |
| 278 | 307 | <div class="buttons"> |
| 279 | 308 | <input type="hidden" name="ts" value="<?cs var:ticket.ts ?>" /> |
| | 309 | <input type="hidden" name="replyto" value="<?cs var:ticket.replyto ?>" /> |
| | 310 | <input type="hidden" name="cnum" value="<?cs var:ticket.cnum ?>" /> |
| 280 | 311 | <input type="submit" name="preview" value="Preview" accesskey="r" /> |
| 281 | 312 | <input type="submit" value="Submit changes" /> |
| 282 | 313 | </div> |
diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/api.py
|
a
|
b
|
|
| 186 | 186 | |
| 187 | 187 | def get_link_resolvers(self): |
| 188 | 188 | return [('bug', self._format_link), |
| 189 | | ('ticket', self._format_link)] |
| | 189 | ('ticket', self._format_link), |
| | 190 | ('comment', self._format_comment_link)] |
| 190 | 191 | |
| 191 | 192 | def get_wiki_syntax(self): |
| 192 | 193 | yield ( |
| … |
… |
|
| 215 | 216 | return html.A(class_='missing ticket', rel='nofollow', |
| 216 | 217 | href=formatter.href.ticket(target))[label] |
| 217 | 218 | |
| | 219 | def _format_comment_link(self, formatter, ns, target, label): |
| | 220 | type, id, cnum = 'ticket', '1', 0 |
| | 221 | href = None |
| | 222 | if ':' in target: |
| | 223 | elts = target.split(':') |
| | 224 | if len(elts) == 3: |
| | 225 | type, id, cnum = elts |
| | 226 | href = formatter.href(type, id) |
| | 227 | else: |
| | 228 | # FIXME: the formatter should know which object the text being |
| | 229 | # formatted belongs to |
| | 230 | if formatter.req: |
| | 231 | path_info = formatter.req.path_info.strip('/').split('/', 2) |
| | 232 | if len(path_info) == 2: |
| | 233 | type, id = path_info[:2] |
| | 234 | href = formatter.href(type, id) |
| | 235 | cnum = target |
| | 236 | if href: |
| | 237 | return html.A(label, href="%s#comment:%s" % (href, cnum), |
| | 238 | title="Comment %s for %s:%s" % (cnum, type, id)) |
| | 239 | else: |
| | 240 | return label |
| | 241 | |
| 218 | 242 | # ISearchSource methods |
| 219 | 243 | |
| 220 | 244 | def get_search_filters(self, req): |
diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/model.py
|
a
|
b
|
|
| 180 | 180 | self._old = {} |
| 181 | 181 | return self.id |
| 182 | 182 | |
| 183 | | def save_changes(self, author, comment, when=0, db=None): |
| | 183 | def save_changes(self, author, comment, when=0, db=None, cnum=''): |
| 184 | 184 | """ |
| 185 | 185 | Store ticket changes in the database. The ticket must already exist in |
| 186 | 186 | the database. |
| … |
… |
|
| 244 | 244 | if comment: |
| 245 | 245 | cursor.execute("INSERT INTO ticket_change " |
| 246 | 246 | "(ticket,time,author,field,oldvalue,newvalue) " |
| 247 | | "VALUES (%s,%s,%s,'comment','',%s)", |
| 248 | | (self.id, when, author, comment)) |
| | 247 | "VALUES (%s,%s,%s,'comment',%s,%s)", |
| | 248 | (self.id, when, author, cnum, comment)) |
| 249 | 249 | |
| 250 | 250 | cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s", |
| 251 | 251 | (when, self.id)) |
| … |
… |
|
| 265 | 265 | db = self._get_db(db) |
| 266 | 266 | cursor = db.cursor() |
| 267 | 267 | if when: |
| 268 | | cursor.execute("SELECT time,author,field,oldvalue,newvalue " |
| | 268 | cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 " |
| 269 | 269 | "FROM ticket_change WHERE ticket=%s AND time=%s " |
| 270 | 270 | "UNION " |
| 271 | | "SELECT time,author,'attachment',null,filename " |
| | 271 | "SELECT time,author,'attachment',null,filename,0 " |
| 272 | 272 | "FROM attachment WHERE id=%s AND time=%s " |
| 273 | 273 | "UNION " |
| 274 | | "SELECT time,author,'comment',null,description " |
| | 274 | "SELECT time,author,'comment',null,description,0 " |
| 275 | 275 | "FROM attachment WHERE id=%s AND time=%s " |
| 276 | 276 | "ORDER BY time", |
| 277 | 277 | (self.id, when, str(self.id), when, self.id, when)) |
| 278 | 278 | else: |
| 279 | | cursor.execute("SELECT time,author,field,oldvalue,newvalue " |
| | 279 | cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 " |
| 280 | 280 | "FROM ticket_change WHERE ticket=%s " |
| 281 | 281 | "UNION " |
| 282 | | "SELECT time,author,'attachment',null,filename " |
| | 282 | "SELECT time,author,'attachment',null,filename,0 " |
| 283 | 283 | "FROM attachment WHERE id=%s " |
| 284 | 284 | "UNION " |
| 285 | | "SELECT time,author,'comment',null,description " |
| | 285 | "SELECT time,author,'comment',null,description,0 " |
| 286 | 286 | "FROM attachment WHERE id=%s " |
| 287 | 287 | "ORDER BY time", (self.id, str(self.id), self.id)) |
| 288 | 288 | log = [] |
| 289 | | for t, author, field, oldvalue, newvalue in cursor: |
| 290 | | log.append((int(t), author, field, oldvalue or '', newvalue or '')) |
| | 289 | for t, author, field, oldvalue, newvalue, permanent in cursor: |
| | 290 | log.append((int(t), author, field, oldvalue or '', newvalue or '', |
| | 291 | permanent)) |
| 291 | 292 | return log |
| 292 | 293 | |
| 293 | 294 | def delete(self, db=None): |
diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/notification.py
|
a
|
b
|
|
| 75 | 75 | changes = '' |
| 76 | 76 | if not self.newticket and modtime: # Ticket change |
| 77 | 77 | changelog = ticket.get_changelog(modtime) |
| 78 | | for date, author, field, old, new in changelog: |
| | 78 | for date, author, field, old, new, permanent in changelog: |
| 79 | 79 | self.hdf.set_unescaped('ticket.change.author', author) |
| 80 | 80 | pfx = 'ticket.change.%s' % field |
| 81 | 81 | newv = '' |
diff -r f8e1ea02978c -r 3081679abd00 trac/ticket/web_ui.py
|
a
|
b
|
|
| 268 | 268 | if comment: |
| 269 | 269 | req.hdf['ticket.comment'] = comment |
| 270 | 270 | # Wiki format a preview of comment |
| 271 | | req.hdf['ticket.comment_preview'] = wiki_to_html(comment, |
| 272 | | self.env, |
| 273 | | req, db) |
| | 271 | req.hdf['ticket.comment_preview'] = wiki_to_html( |
| | 272 | comment, self.env, req, db) |
| 274 | 273 | else: |
| 275 | 274 | req.hdf['ticket.reassign_owner'] = req.authname |
| 276 | 275 | # Store a timestamp in order to detect "mid air collisions" |
| … |
… |
|
| 511 | 510 | ticket['resolution'] = '' |
| 512 | 511 | |
| 513 | 512 | now = int(time.time()) |
| | 513 | cnum = req.args.get('cnum') |
| 514 | 514 | ticket.save_changes(req.args.get('author', req.authname), |
| 515 | | req.args.get('comment'), when=now, db=db) |
| | 515 | req.args.get('comment'), when=now, db=db, |
| | 516 | cnum=cnum) |
| 516 | 517 | db.commit() |
| 517 | 518 | |
| 518 | 519 | try: |
| … |
… |
|
| 522 | 523 | self.log.exception("Failure sending notification on change to " |
| 523 | 524 | "ticket #%s: %s" % (ticket.id, e)) |
| 524 | 525 | |
| 525 | | req.redirect(req.href.ticket(ticket.id)) |
| | 526 | fragment = '' |
| | 527 | if cnum: |
| | 528 | fragment = '#comment:'+cnum |
| | 529 | req.redirect(req.href.ticket(ticket.id) + fragment) |
| 526 | 530 | |
| 527 | 531 | def _insert_ticket_data(self, req, db, ticket, reporter_id): |
| 528 | 532 | """Insert ticket data into the hdf""" |
| | 533 | replyto = req.args.get('replyto') |
| | 534 | req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) |
| 529 | 535 | req.hdf['ticket'] = ticket.values |
| 530 | | req.hdf['ticket.id'] = ticket.id |
| 531 | | req.hdf['ticket.href'] = req.href.ticket(ticket.id) |
| 532 | | |
| | 536 | req.hdf['ticket'] = { |
| | 537 | 'id': ticket.id, |
| | 538 | 'href': req.href.ticket(ticket.id), |
| | 539 | 'replyto': replyto |
| | 540 | } |
| | 541 | |
| | 542 | # -- Ticket fields |
| | 543 | |
| 533 | 544 | for field in TicketSystem(self.env).get_ticket_fields(): |
| 534 | 545 | if field['type'] in ('radio', 'select'): |
| 535 | 546 | value = ticket.values.get(field['name']) |
| … |
… |
|
| 547 | 558 | req.hdf['ticket.fields.' + name] = field |
| 548 | 559 | |
| 549 | 560 | req.hdf['ticket.reporter_id'] = reporter_id |
| 550 | | req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary']) |
| 551 | | req.hdf['ticket.description.formatted'] = wiki_to_html(ticket['description'], |
| 552 | | self.env, req, db) |
| | 561 | req.hdf['ticket.description.formatted'] = wiki_to_html( |
| | 562 | ticket['description'], self.env, req, db) |
| 553 | 563 | |
| 554 | 564 | req.hdf['ticket.opened'] = format_datetime(ticket.time_created) |
| 555 | 565 | req.hdf['ticket.opened_delta'] = pretty_timedelta(ticket.time_created) |
| 556 | 566 | if ticket.time_changed != ticket.time_created: |
| 557 | | req.hdf['ticket.lastmod'] = format_datetime(ticket.time_changed) |
| 558 | | req.hdf['ticket.lastmod_delta'] = pretty_timedelta(ticket.time_changed) |
| | 567 | req.hdf['ticket'] = { |
| | 568 | 'lastmod': format_datetime(ticket.time_changed), |
| | 569 | 'lastmod_delta': pretty_timedelta(ticket.time_changed) |
| | 570 | } |
| | 571 | |
| | 572 | # -- Ticket Change History |
| 559 | 573 | |
| 560 | 574 | changelog = ticket.get_changelog(db=db) |
| 561 | 575 | curr_author = None |
| 562 | | curr_date = 0 |
| | 576 | curr_date = 0 |
| | 577 | autonum = 0 |
| | 578 | replies = {} |
| 563 | 579 | changes = [] |
| 564 | | for date, author, field, old, new in changelog: |
| | 580 | current = None |
| | 581 | for date, author, field, old, new, permanent in changelog: |
| 565 | 582 | if date != curr_date or author != curr_author: |
| 566 | | changes.append({ |
| | 583 | current = { |
| 567 | 584 | 'date': format_datetime(date), |
| 568 | 585 | 'author': author, |
| 569 | 586 | 'fields': {} |
| 570 | | }) |
| | 587 | } |
| | 588 | changes.append(current) |
| 571 | 589 | curr_date = date |
| 572 | 590 | curr_author = author |
| | 591 | if permanent: |
| | 592 | autonum += 1 |
| | 593 | current['cnum'] = autonum |
| 573 | 594 | if field == 'comment': |
| 574 | | changes[-1]['comment'] = wiki_to_html(new, self.env, req, db) |
| | 595 | # numbered comments threading |
| | 596 | if permanent: |
| | 597 | if old: |
| | 598 | current['cnum'] = old |
| | 599 | this_num = old |
| | 600 | parentsep = this_num.rfind('.') |
| | 601 | if parentsep > -1: # reply 1.2.3 -> parent 1.2 |
| | 602 | parent = this_num[:parentsep] |
| | 603 | current['replyto'] = parent |
| | 604 | replies.setdefault(parent, []).append(this_num) |
| | 605 | autonum -= 1 # autonum skip replies |
| | 606 | else: |
| | 607 | this_num = str(autonum) |
| | 608 | # prefill comment textarea with the cited parent, when replying |
| | 609 | if permanent and replyto == this_num and \ |
| | 610 | not 'comment' in req.args: |
| | 611 | comment_num = this_num |
| | 612 | req.hdf['ticket.comment'] = '\n'.join( |
| | 613 | ['Replying to [comment:%s %s:]' % (replyto, author)] + |
| | 614 | ['> %s' % line for line in new.splitlines()] + ['']) |
| | 615 | current['comment'] = wiki_to_html(new, self.env, req, db) |
| 575 | 616 | elif field == 'description': |
| 576 | | changes[-1]['fields'][field] = '' |
| | 617 | current['fields'][field] = '' |
| 577 | 618 | else: |
| 578 | | changes[-1]['fields'][field] = {'old': old, |
| 579 | | 'new': new} |
| 580 | | req.hdf['ticket.changes'] = changes |
| 581 | | |
| 582 | | # List attached files |
| | 619 | current['fields'][field] = {'old': old, 'new': new} |
| | 620 | if replyto: |
| | 621 | cnum = '%s.%d' % (replyto, len(replies.get(replyto, [])) + 1) |
| | 622 | else: |
| | 623 | cnum = autonum + 1 |
| | 624 | req.hdf['ticket'] = { |
| | 625 | 'changes': changes, |
| | 626 | 'replies': replies, |
| | 627 | 'cnum': cnum |
| | 628 | } |
| | 629 | |
| | 630 | # -- Ticket Attachments |
| | 631 | |
| 583 | 632 | req.hdf['ticket.attachments'] = attachments_to_hdf(self.env, req, db, |
| 584 | 633 | 'ticket', ticket.id) |
| 585 | 634 | if req.perm.has_permission('TICKET_APPEND'): |