[Tracker-discuss] [issue586] Add GitHub Pull Request URL on issue page

Maciej Szulik metatracker at psf.upfronthosting.co.za
Thu Nov 24 17:26:02 EST 2016


Maciej Szulik added the comment:

The alternate version with pr number and repository fields.

_______________________________________________________
PSF Meta Tracker <metatracker at psf.upfronthosting.co.za>
<http://psf.upfronthosting.co.za/roundup/meta/issue586>
_______________________________________________________
-------------- next part --------------
diff --git a/detectors/pull_request.py b/detectors/pull_request.py
new file mode 100644
--- /dev/null
+++ b/detectors/pull_request.py
@@ -0,0 +1,79 @@
+# Auditor for GitHub URLs
+# Check if it is a valid GitHub Pull Request URL and extract PR number
+
+import re
+
+
+just_number_re = re.compile(r'\d+')
+repo_number_re = re.compile(r'(?P<repository>\w+)?#(?P<number>\d+)')
+url_re = re.compile(r'github\.com/cpython/(?P<repository>\w+)/(?P<number>\d+)')
+
+default_repository = 'python'
+
+def validate_pull_requests(db, cl, nodeid, newvalues):
+    newprs = set(newvalues.get('pull_requests',()))
+    if not newprs:
+        return
+    oldprs = set()
+    if nodeid:
+        oldprs = set(db.issue.get(nodeid, 'pull_requests'))
+        newprs -= oldprs
+    try:
+        prid = newprs.pop()
+    except KeyError:
+        return
+    repository = db.pull_request.get(prid, 'repository')
+    number = db.pull_request.get(prid, 'number')
+    for oldpr in oldprs:
+        oldrepository = db.pull_request.get(oldpr, 'repository')
+        oldnumber = db.pull_request.get(oldpr, 'number')
+        if repository == oldrepository and number == oldnumber:
+            raise ValueError("GitHub PR already added to issue")
+
+def validate_url(db, cl, nodeid, newvalues):
+    try:
+        repository, number = parse_url(newvalues['url'])
+        if repository and number:
+            newvalues['repository'] = repository
+            newvalues['number'] = number
+            del newvalues['url']
+    except KeyError:
+        pass
+    try:
+        newvalues['title']
+    except KeyError:
+        newvalues['title'] = ''
+
+def parse_url(url):
+    """
+    Transform following forms:
+    - full url
+    - repository#number
+    - #number
+    - number
+    and returns tuple repository name and pull request number.
+    """
+    # first try just the number
+    just_number_match = just_number_re.search(url)
+    if just_number_match:
+        return default_repository, just_number_match.group()
+    # then try repository#number (repository part can be omitted here)
+    repo_number_match = repo_number_re.search(url)
+    if repo_number_match:
+        repository = repo_number_match.group('repository')
+        if repository is None:
+            repository = default_repository
+        return repository, repo_number_match.group('number')
+    # finally parse the url
+    url_match = url_re.search(url)
+    if url_match:
+        return url_match.group('repository'), url_match.group('number')
+    # if nothing else raise error
+    raise ValueError("Unknown PR format, acceptable formats are: full github URL, repository#pr_number, #pr_number, pr_number")
+
+
+def init(db):
+    db.issue.audit('create', validate_pull_requests)
+    db.issue.audit('set', validate_pull_requests)
+    db.pull_request.audit('create', validate_url)
+    db.pull_request.audit('set', validate_url)
diff --git a/extensions/pull_request.py b/extensions/pull_request.py
new file mode 100644
--- /dev/null
+++ b/extensions/pull_request.py
@@ -0,0 +1,9 @@
+
+def get_pr_url(repository, number):
+    """Transforms pull_request object into working URL."""
+    baseurl = 'https://github.com/cpython/'
+    return "%s%s/%d" % (baseurl, repository, number)
+
+def init(instance):
+    instance.registerUtil('get_pr_url', get_pr_url)
+
diff --git a/html/issue.item.html b/html/issue.item.html
--- a/html/issue.item.html
+++ b/html/issue.item.html
@@ -220,6 +220,14 @@
  </td>
 </tr>
 
+<tr tal:condition="context/is_edit_ok">
+ <th><tal:block>GitHub PR</tal:block>:</th>
+ <td colspan="3">
+  <input type="hidden" name="@link at pull_requests" value="pull_request-1">
+  <input type="text" name="pull_request-1 at url" size="50">
+ </td>
+</tr>
+
 </table>
 </fieldset>
 <table class="form">
@@ -273,6 +281,31 @@
  </tr>
 </table>
 
+<table class="files" tal:condition="context/pull_requests">
+ <tr><th class="header" colspan="4">Pull Requests</th></tr>
+ <tr>
+  <th>URL</th>
+  <th>Linked</th>
+  <th>Edit</th>
+ </tr>
+ <tr tal:repeat="pull_request python:context.pull_requests.sorted('creation')">
+  <td>
+   <a tal:attributes="href python:utils.get_pr_url(pull_request.repository, pull_request.number); title pull_request/title"
+      tal:content="python:utils.get_pr_url(pull_request.repository, pull_request.number)"  target="_blank">
+    URL
+   </a>
+  </td>
+  <td>
+   <span tal:content="pull_request/creator">creator's name</span>,
+   <span tal:content="python:pull_request.creation.pretty('%Y-%m-%d %H:%M')">creation date</span>
+  </td>
+  <td>
+    <a tal:condition="pull_request/is_edit_ok"
+       tal:attributes="href string:pull_request${pull_request/id}">edit</a>
+  </td>
+ </tr>
+</table>
+
 <p tal:condition="python: context.id and not request.user.contrib_form and
                   any(file.creator.id == request.user.id for file in context.files)"
    id="contribform">
@@ -284,7 +317,7 @@
      once it's done your name will have a * next to it.)</p>
 
 <table class="files" tal:condition="context/hgrepos">
- <tr><th class="Header" colspan="4">Repositories containing patches</th></tr>
+ <tr><th class="header" colspan="4">Repositories containing patches</th></tr>
  <tr tal:repeat="hgrepo python:context.hgrepos.sorted('creation')">
   <td>
    <a tal:attributes="href hgrepo/url"
diff --git a/html/pull_request.item.html b/html/pull_request.item.html
new file mode 100644
--- /dev/null
+++ b/html/pull_request.item.html
@@ -0,0 +1,72 @@
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title" i18n:translate="">Pull Request - <span
+ i18n:name="tracker" tal:replace="config/TRACKER_NAME" /></title>
+<span metal:fill-slot="body_title" tal:omit-tag="python:1"
+ i18n:translate="">Pull Request</span>
+
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="python:not (context.is_view_ok()
+ or request.user.hasRole('Anonymous'))" i18n:translate="">
+ You are not allowed to view this page.</p>
+
+<p tal:condition="python:not context.is_view_ok()
+ and request.user.hasRole('Anonymous')" i18n:translate="">
+ Please login with your username and password.</p>
+
+<form method="POST" onSubmit="return submit_once()"
+      enctype="multipart/form-data" tal:condition="context/is_view_ok"
+      tal:attributes="action context/designator">
+
+<table class="form">
+ <tr>
+  <th i18n:translate="">URL</th>
+  <td tal:content="structure python:utils.get_pr_url(context.repository, context.number)"></td>
+ </tr>
+ <tr>
+  <th i18n:translate="">Title</th>
+  <td tal:content="structure context/title/field"></td>
+ </tr>
+
+ <tr tal:condition="python:context.is_edit_ok()">
+  <td>
+    
+   <input type="hidden" name="@template" value="item">
+   <input type="hidden" name="@required" value="url">
+   <input type="hidden" name="@multilink"
+          tal:condition="python:request.form.has_key('@multilink')"
+          tal:attributes="value request/form/@multilink/value">
+  </td>
+  <td tal:content="structure context/submit">submit button here</td>
+ </tr>
+</table>
+</form>
+
+<p tal:condition="python:utils.sb_is_spam(context)" class="error-message">
+   File has been classified as spam.</p>
+
+<form tal:define="u_hasRole python:request.user.hasRole;
+                  issueid_action python:utils.issueid_and_action_from_class(context);
+                  issueid python:'issue%s' % issueid_action[0];
+                  action python:issueid_action[1]"
+      tal:condition="python:issueid and context.is_edit_ok()"
+      tal:attributes="action issueid" method="post">
+    <input type="hidden" name="@action" value="edit" />
+    <tal:block tal:condition="python: action == 'link'">
+        <input type="hidden" name="@remove at pull_requests" tal:attributes="value context/id" />
+        <p>This PR is linked to <a tal:attributes="href issueid" tal:content="issueid" />:
+           <input type="submit" value="Unlink" i18n:attributes="value"/></p>
+    </tal:block>
+    <tal:block tal:condition="python: action == 'unlink'">
+        <input type="hidden" name="@add at pull_requests" tal:attributes="value context/id" />
+        <p>This PR has been unlinked from <a tal:attributes="href issueid" tal:content="issueid" />:
+           <input type="submit" value="Restore" i18n:attributes="value"/></p>
+    </tal:block>
+</form>
+
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</td>
+
+</tal:block>
diff --git a/schema.py b/schema.py
--- a/schema.py
+++ b/schema.py
@@ -173,6 +173,13 @@
                patchbranch=String(),
                )
 
+pull_request = Class(db, "pull_request",
+                     url=String(),
+                     repository=String(),
+                     number=Number(),
+                     title=String(),
+                     )
+
 # IssueClass automatically gets these properties in addition to the Class ones:
 #   title = String()
 #   messages = Multilink("msg")
@@ -194,7 +201,8 @@
                    stage=Link('stage'),
                    nosy_count=Number(),
                    message_count=Number(),
-                   hgrepos=Multilink('hgrepo'))
+                   hgrepos=Multilink('hgrepo'),
+                   pull_requests=Multilink('pull_request'))
 
 #
 # TRACKER SECURITY SETTINGS
@@ -222,7 +230,7 @@
 
 for cl in ('issue_type', 'severity', 'component',
            'version', 'priority', 'stage', 'status', 'resolution',
-           'issue', 'keyword', 'hgrepo'):
+           'issue', 'keyword', 'hgrepo', 'pull_request'):
     db.security.addPermissionToRole('User', 'View', cl)
     db.security.addPermissionToRole('Anonymous', 'View', cl)
 
@@ -233,6 +241,13 @@
                               properties=['url', 'patchbranch'])
 db.security.addPermissionToRole('User', p)
 
+def may_edit_pull_request(db, userid, itemid):
+    return userid == db.pull_request.get(itemid, "creator")
+db.security.addPermissionToRole('User', 'Create', 'pull_request')
+p = db.security.addPermission(name='Edit', klass='pull_request',
+                              check=may_edit_pull_request, properties=['url', 'repository', 'number', 'title'])
+db.security.addPermissionToRole('User', p)
+
 class may_view_spam:
     def __init__(self, klassname):
         self.klassname = klassname
@@ -292,7 +307,7 @@
                               properties=('title', 'type',
                                           'components', 'versions',
                                           'severity',
-                                          'messages', 'files', 'nosy', 'hgrepos'),
+                                          'messages', 'files', 'nosy', 'hgrepos', 'pull_requests'),
                               description='User can report and discuss issues')
 db.security.addPermissionToRole('User', p)
 
@@ -300,7 +315,7 @@
                               properties=('title', 'type',
                                           'components', 'versions',
                                           'severity',
-                                          'messages', 'files', 'nosy', 'hgrepos'),
+                                          'messages', 'files', 'nosy', 'hgrepos', 'pull_requests'),
                               description='User can report and discuss issues')
 db.security.addPermissionToRole('User', p)
 
@@ -335,7 +350,7 @@
 ##########################
 for cl in ('issue_type', 'severity', 'component',
            'version', 'priority', 'stage', 'status', 'resolution', 'issue',
-           'file', 'msg', 'hgrepo'):
+           'file', 'msg', 'hgrepo', 'pull_request'):
     db.security.addPermissionToRole('Coordinator', 'View', cl)
     db.security.addPermissionToRole('Coordinator', 'Edit', cl)
     db.security.addPermissionToRole('Coordinator', 'Create', cl)


More information about the Tracker-discuss mailing list