| [14675] | 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| 3 | import re |
|---|
| 4 | |
|---|
| 5 | from datetime import datetime |
|---|
| 6 | |
|---|
| 7 | from trac.admin import * |
|---|
| 8 | from trac.config import ListOption |
|---|
| 9 | from trac.core import * |
|---|
| 10 | from trac.resource import Resource |
|---|
| 11 | from trac.ticket.api import ITicketManipulator |
|---|
| [14804] | 12 | from trac.util import get_reporter_id |
|---|
| [14675] | 13 | from trac.util.datefmt import format_datetime, utc |
|---|
| [17475] | 14 | from trac.util.html import tag |
|---|
| [14675] | 15 | from trac.util.presentation import Paginator |
|---|
| [16118] | 16 | from trac.web.api import IRequestFilter |
|---|
| [14675] | 17 | from trac.web.chrome import Chrome, add_link, add_script, add_stylesheet, add_notice, add_warning, web_context |
|---|
| 18 | from trac.wiki.formatter import format_to_oneliner |
|---|
| 19 | from trac.wiki.macros import WikiMacroBase |
|---|
| 20 | from trac.wiki.api import parse_args |
|---|
| 21 | |
|---|
| 22 | from pullrequests.model import PullRequest |
|---|
| 23 | |
|---|
| 24 | |
|---|
| 25 | class PullRequestsModule(Component): |
|---|
| 26 | |
|---|
| [16118] | 27 | implements(IAdminPanelProvider, IRequestFilter, ITicketManipulator) |
|---|
| [14675] | 28 | |
|---|
| 29 | create_commands = ListOption('pullrequests', 'create_commands', 'open') |
|---|
| 30 | update_commands = ListOption('pullrequests', 'update_commands', 'reviewed, closed') |
|---|
| 31 | |
|---|
| 32 | # IAdminPanelProvider methods |
|---|
| 33 | |
|---|
| 34 | def get_admin_panels(self, req): |
|---|
| 35 | if 'PULL_REQUEST' in req.perm: |
|---|
| 36 | yield ('pr', "Pull Requests", 'list', "List") |
|---|
| 37 | |
|---|
| 38 | def render_admin_panel(self, req, category, panel, path_info): |
|---|
| 39 | if panel == 'list': |
|---|
| 40 | req.perm.require('PULL_REQUEST') |
|---|
| 41 | return self.render_pr_list_panel(req, category, panel, path_info) |
|---|
| 42 | |
|---|
| 43 | def render_pr_list_panel(self, req, category, panel, path_info): |
|---|
| 44 | def format_datetime_utc(t): |
|---|
| 45 | return format_datetime(t, tzinfo=utc, locale=getattr(req, 'lc_time', None)) |
|---|
| 46 | |
|---|
| 47 | def format_wikilink(pr): |
|---|
| 48 | resource = Resource('ticket', pr.ticket) |
|---|
| 49 | context = web_context(req, resource) |
|---|
| 50 | return format_to_oneliner(self.env, context, pr.wikilink) |
|---|
| 51 | |
|---|
| 52 | # Detail view? |
|---|
| 53 | if path_info: |
|---|
| 54 | id = path_info |
|---|
| 55 | pr = PullRequest.select_by_id(self.env, id) |
|---|
| 56 | if not pr: |
|---|
| 57 | raise TracError("Pull request does not exist!") |
|---|
| 58 | if req.method == 'POST': |
|---|
| 59 | if req.args.get('save'): |
|---|
| 60 | pr.status = req.args.get('status') |
|---|
| 61 | pr.add_reviewer(req.authname) |
|---|
| 62 | |
|---|
| 63 | PullRequest.update_status_and_reviewers(self.env, pr) |
|---|
| 64 | add_notice(req, 'Your changes have been saved.') |
|---|
| 65 | req.redirect(req.href.admin(category, panel)) |
|---|
| 66 | elif req.args.get('cancel'): |
|---|
| 67 | req.redirect(req.href.admin(category, panel)) |
|---|
| 68 | |
|---|
| 69 | Chrome(self.env).add_wiki_toolbars(req) |
|---|
| 70 | data = {'view': 'detail', |
|---|
| 71 | 'pr': pr, |
|---|
| 72 | 'statuses': self.create_commands + self.update_commands, |
|---|
| 73 | 'format_datetime_utc': format_datetime_utc, |
|---|
| 74 | } |
|---|
| 75 | |
|---|
| 76 | else: |
|---|
| 77 | #Pagination |
|---|
| 78 | page = int(req.args.get('page', 1)) |
|---|
| 79 | max_per_page = int(req.args.get('max', 10)) |
|---|
| 80 | |
|---|
| 81 | prs = PullRequest.select_all_paginated(self.env, page, max_per_page) |
|---|
| 82 | total_count = PullRequest.count_all(self.env) |
|---|
| 83 | |
|---|
| 84 | paginator = Paginator(prs, page - 1, max_per_page, total_count) |
|---|
| 85 | if paginator.has_next_page: |
|---|
| 86 | next_href = req.href.admin(category, panel, max=max_per_page, page=page + 1) |
|---|
| 87 | add_link(req, 'next', next_href, 'Next Page') |
|---|
| 88 | if paginator.has_previous_page: |
|---|
| 89 | prev_href = req.href.admin(category, panel, max=max_per_page, page=page - 1) |
|---|
| 90 | add_link(req, 'prev', prev_href, 'Previous Page') |
|---|
| 91 | |
|---|
| 92 | pagedata = [] |
|---|
| 93 | shown_pages = paginator.get_shown_pages(21) |
|---|
| 94 | for page in shown_pages: |
|---|
| 95 | pagedata.append([req.href.admin(category, panel, max=max_per_page, page=page), None, |
|---|
| 96 | str(page), 'Page %d' % (page,)]) |
|---|
| 97 | paginator.shown_pages = [dict(zip(['href', 'class', 'string', 'title'], p)) for p in pagedata] |
|---|
| 98 | paginator.current_page = {'href': None, 'class': 'current', |
|---|
| 99 | 'string': str(paginator.page + 1), |
|---|
| 100 | 'title':None} |
|---|
| 101 | data = {'view': 'list', |
|---|
| 102 | 'paginator': paginator, |
|---|
| 103 | 'max_per_page': max_per_page, |
|---|
| 104 | 'prs': prs, |
|---|
| 105 | 'format_datetime_utc': format_datetime_utc, |
|---|
| 106 | 'format_wikilink': format_wikilink, |
|---|
| 107 | } |
|---|
| 108 | |
|---|
| [17475] | 109 | return 'pullrequests.html', data |
|---|
| [14675] | 110 | |
|---|
| [16118] | 111 | # IRequestFilter methods |
|---|
| 112 | |
|---|
| 113 | def pre_process_request(self, req, handler): |
|---|
| 114 | return handler |
|---|
| 115 | |
|---|
| 116 | def post_process_request(self, req, template, data, content_type): |
|---|
| 117 | path = req.path_info |
|---|
| 118 | if path.startswith('/ticket/'): |
|---|
| [16122] | 119 | if data and 'ticket' in data and 'fields' in data: |
|---|
| [16118] | 120 | self._append_pr_links(req, data) |
|---|
| 121 | return template, data, content_type |
|---|
| 122 | |
|---|
| 123 | def _append_pr_links(self, req, data): |
|---|
| [16123] | 124 | rendered = '' |
|---|
| [16118] | 125 | ticket = data['ticket'] |
|---|
| 126 | items = PullRequest.select(self.env, ticket=str(ticket.id)) |
|---|
| [16123] | 127 | if items: |
|---|
| 128 | ticket.values['PRs'] = True # Activates field |
|---|
| 129 | results = [] |
|---|
| 130 | for pr in reversed(items): |
|---|
| 131 | label = 'PR:%s (%s)' % (pr.id, pr.status) |
|---|
| 132 | href = req.href.ticket(pr.ticket) + '#comment:%s' % (pr.comment,) |
|---|
| 133 | link = tag.a(label, href=href) |
|---|
| 134 | results.append(link) |
|---|
| 135 | rendered = tag.span(*[e for pair in zip(results, [' '] * len(results)) for e in pair][:-1]) |
|---|
| [16118] | 136 | data['fields'].append({ |
|---|
| 137 | 'name': 'PRs', |
|---|
| 138 | 'rendered': rendered, |
|---|
| 139 | 'type': 'textarea', # Full row |
|---|
| 140 | }) |
|---|
| 141 | |
|---|
| [14675] | 142 | # ITicketManipulator methods |
|---|
| 143 | |
|---|
| [14804] | 144 | command_pr_url = r'(?P<command>[A-Za-z]+) PR: (?P<wikilink>\S+)' |
|---|
| [14675] | 145 | command_pr_id = r'(?P<command>[A-Za-z]+) PR:(?P<id>[0-9]+)' |
|---|
| 146 | |
|---|
| 147 | command_pr_url_re = re.compile(command_pr_url) |
|---|
| 148 | command_pr_id_re = re.compile(command_pr_id) |
|---|
| 149 | |
|---|
| 150 | def prepare_ticket(self, req, ticket, fields, actions): |
|---|
| 151 | pass |
|---|
| 152 | |
|---|
| 153 | def validate_ticket(self, req, ticket): |
|---|
| [14804] | 154 | if 'preview' in req.args: |
|---|
| 155 | # During preview: Do nothing |
|---|
| 156 | return [] |
|---|
| 157 | |
|---|
| [14675] | 158 | if 'PULL_REQUEST' in req.perm: |
|---|
| 159 | comment = req.args.get('comment') |
|---|
| [14804] | 160 | author = get_reporter_id(req, 'author') |
|---|
| [14675] | 161 | if comment: |
|---|
| 162 | req.args['comment'] = self._handle_comment(req, ticket, comment, author) |
|---|
| 163 | return [] |
|---|
| 164 | |
|---|
| 165 | def _handle_comment(self, req, ticket, comment, author): |
|---|
| 166 | def create_pr_and_inline_id(m): |
|---|
| 167 | command = m.group('command') |
|---|
| 168 | wikilink = m.group('wikilink') |
|---|
| 169 | if command in self.create_commands: |
|---|
| 170 | id = None |
|---|
| 171 | status = command |
|---|
| 172 | reviewers = '' |
|---|
| 173 | opened = modified = datetime.now(utc) |
|---|
| [14804] | 174 | comment_number = (ticket.get_comment_number(ticket['changetime']) or 0) + 1 |
|---|
| [14675] | 175 | pr = PullRequest(id, status, author, reviewers, opened, modified, ticket.id, comment_number, wikilink) |
|---|
| 176 | PullRequest.add(self.env, pr) |
|---|
| 177 | add_notice(req, 'A new pull request has been created.') |
|---|
| 178 | |
|---|
| 179 | return u'%s PR:%s %s' % (command, pr.id, wikilink) |
|---|
| 180 | else: |
|---|
| 181 | return m.group(0) |
|---|
| 182 | return u'[%s %s]' % (('/hours/%s' % ticket.id), match.group()) |
|---|
| 183 | comment = self.command_pr_url_re.sub(create_pr_and_inline_id, comment) |
|---|
| 184 | |
|---|
| 185 | for m in self.command_pr_id_re.finditer(comment): |
|---|
| 186 | command = m.group('command') |
|---|
| 187 | id = m.group('id') |
|---|
| 188 | if command in self.update_commands: |
|---|
| 189 | pr = PullRequest.select_by_id(self.env, id) |
|---|
| [17366] | 190 | if pr is None: |
|---|
| 191 | add_warning(req, 'Pull request %s was not found.' % (id,)) |
|---|
| 192 | continue |
|---|
| [14675] | 193 | pr.status = command |
|---|
| 194 | pr.add_reviewer(author) |
|---|
| 195 | PullRequest.update_status_and_reviewers(self.env, pr) |
|---|
| 196 | add_notice(req, 'Pull request %s has been updated.' % (id,)) |
|---|
| 197 | |
|---|
| 198 | return comment |
|---|
| 199 | |
|---|
| 200 | |
|---|
| 201 | class PRQueryMacro(WikiMacroBase): |
|---|
| 202 | """List all matching pull requests. |
|---|
| 203 | |
|---|
| 204 | Example: |
|---|
| 205 | {{{ |
|---|
| 206 | [[PRQuery(status=reviewed,author=joe)]] |
|---|
| 207 | }}} |
|---|
| 208 | """ |
|---|
| 209 | |
|---|
| 210 | def expand_macro(self, formatter, name, content): |
|---|
| 211 | args, kw = parse_args(content) |
|---|
| 212 | status = kw.get('status') |
|---|
| 213 | author = kw.get('author') |
|---|
| 214 | items = PullRequest.select(self.env, status=status, author=author) |
|---|
| 215 | |
|---|
| 216 | rows = [tag.tr( |
|---|
| 217 | tag.td(pr.id), |
|---|
| 218 | tag.td(pr.status), |
|---|
| 219 | tag.td(tag.a('#%s' % (pr.ticket,), href=formatter.href.ticket(pr.ticket) + '#comment:%s' % (pr.comment,))), |
|---|
| 220 | tag.td(format_to_oneliner(self.env, formatter.context, pr.wikilink)), |
|---|
| 221 | tag.td(pr.author), |
|---|
| 222 | tag.td(pr.reviewers), |
|---|
| 223 | class_='odd' if idx % 2 else 'even') |
|---|
| 224 | for idx, pr in enumerate(items)] |
|---|
| 225 | if not rows: |
|---|
| 226 | rows = [tag.tr(tag.td('No pull requests found', colspan=6, class_='even'))] |
|---|
| 227 | |
|---|
| 228 | return tag.table( |
|---|
| 229 | tag.thead( |
|---|
| 230 | tag.tr( |
|---|
| 231 | tag.th('PR'), |
|---|
| 232 | tag.th('Status'), |
|---|
| 233 | tag.th('Ticket'), |
|---|
| 234 | tag.th('Changes'), |
|---|
| 235 | tag.th('Author'), |
|---|
| 236 | tag.th('Reviewers'), |
|---|
| 237 | class_='trac-columns')), |
|---|
| 238 | tag.tbody(rows), |
|---|
| 239 | class_='listing') |
|---|