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