Title: [95238] trunk/Tools
Revision
95238
Author
e...@webkit.org
Date
2011-09-15 15:10:50 -0700 (Thu, 15 Sep 2011)

Log Message

Reviewed by Adam Barth.

webkit-patch should be able to find users and add them to bugzilla groups
https://bugs.webkit.org/show_bug.cgi?id=63351

These are both very basic commands.  But it's now possible to find
all users matching a regexp, as well as add all users matching a regexp
to a set of groups.

bugzilla.py already knew how to find users (for validate-committer-lists)
but now it has the ability to modify the user records.

I split some of the logic out into a new EditUsersParser class
to try and reduce the amount of code in Bugzilla/BugzillaQueries.

* Scripts/webkitpy/common/net/bugzilla/bugzilla.py:
* Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py:
* Scripts/webkitpy/tool/commands/__init__.py:
* Scripts/webkitpy/tool/commands/adduserstogroups.py: Added.
* Scripts/webkitpy/tool/commands/findusers.py: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/Tools/ChangeLog (95237 => 95238)


--- trunk/Tools/ChangeLog	2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/ChangeLog	2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,5 +1,28 @@
 2011-09-15  Eric Seidel  <e...@webkit.org>
 
+        Reviewed by Adam Barth.
+
+        webkit-patch should be able to find users and add them to bugzilla groups
+        https://bugs.webkit.org/show_bug.cgi?id=63351
+
+        These are both very basic commands.  But it's now possible to find
+        all users matching a regexp, as well as add all users matching a regexp
+        to a set of groups.
+
+        bugzilla.py already knew how to find users (for validate-committer-lists)
+        but now it has the ability to modify the user records.
+
+        I split some of the logic out into a new EditUsersParser class
+        to try and reduce the amount of code in Bugzilla/BugzillaQueries.
+
+        * Scripts/webkitpy/common/net/bugzilla/bugzilla.py:
+        * Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py:
+        * Scripts/webkitpy/tool/commands/__init__.py:
+        * Scripts/webkitpy/tool/commands/adduserstogroups.py: Added.
+        * Scripts/webkitpy/tool/commands/findusers.py: Added.
+
+2011-09-15  Eric Seidel  <e...@webkit.org>
+
         Remove ENABLE(SVG_AS_IMAGE) since all major ports have it on by default
         https://bugs.webkit.org/show_bug.cgi?id=68182
 

Modified: trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py (95237 => 95238)


--- trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py	2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py	2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,4 +1,4 @@
-# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2011 Google Inc. All rights reserved.
 # Copyright (c) 2009 Apple Inc. All rights reserved.
 # Copyright (c) 2010 Research In Motion Limited. All rights reserved.
 #
@@ -49,6 +49,75 @@
 from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, SoupStrainer
 
 
+class EditUsersParser(object):
+    def __init__(self):
+        self._group_name_to_group_string_cache = {}
+
+    def _login_and_uid_from_row(self, row):
+        first_cell = row.find("td")
+        # The first row is just headers, we skip it.
+        if not first_cell:
+            return None
+        # When there were no results, we have a fake "<none>" entry in the table.
+        if first_cell.find(text="<none>"):
+            return None
+        # Otherwise the <td> contains a single <a> which contains the login name or a single <i> with the string "<none>".
+        anchor_tag = first_cell.find("a")
+        login = unicode(anchor_tag.string).strip()
+        user_id = int(re.search(r"userid=(\d+)", str(anchor_tag['href'])).group(1))
+        return (login, user_id)
+
+    def login_userid_pairs_from_edit_user_results(self, results_page):
+        soup = BeautifulSoup(results_page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
+        results_table = soup.find(id="admin_table")
+        login_userid_pairs = [self._login_and_uid_from_row(row) for row in results_table('tr')]
+        # Filter out None from the logins.
+        return filter(lambda pair: bool(pair), login_userid_pairs)
+
+    def _group_name_and_string_from_row(self, row):
+        label_element = row.find('label')
+        group_string = unicode(label_element['for'])
+        group_name = unicode(label_element.find('strong').string).rstrip(':')
+        return (group_name, group_string)
+
+    def user_dict_from_edit_user_page(self, page):
+        soup = BeautifulSoup(page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
+        user_table = soup.find("table", {'class': 'main'})
+        user_dict = {}
+        for row in user_table('tr'):
+            label_element = row.find('label')
+            if not label_element:
+                continue  # This must not be a row we know how to parse.
+            if row.find('table'):
+                continue  # Skip the <tr> holding the groups table.
+
+            key = label_element['for']
+            if "group" in key:
+                key = "groups"
+                value = user_dict.get('groups', set())
+                # We must be parsing a "tr" inside the inner group table.
+                (group_name, _) = self._group_name_and_string_from_row(row)
+                if row.find('input', {'type': 'checkbox', 'checked': 'checked'}):
+                    value.add(group_name)
+            else:
+                value = unicode(row.find('td').string).strip()
+            user_dict[key] = value
+        return user_dict
+
+    def _group_rows_from_edit_user_page(self, edit_user_page):
+        soup = BeautifulSoup(edit_user_page, convertEntities=BeautifulSoup.HTML_ENTITIES)
+        return soup('td', {'class': 'groupname'})
+
+    def group_string_from_name(self, edit_user_page, group_name):
+        # Bugzilla uses "group_NUMBER" strings, which may be different per install
+        # so we just look them up once and cache them.
+        if not self._group_name_to_group_string_cache:
+            rows = self._group_rows_from_edit_user_page(edit_user_page)
+            name_string_pairs = map(self._group_name_and_string_from_row, rows)
+            self._group_name_to_group_string_cache = dict(name_string_pairs)
+        return self._group_name_to_group_string_cache[group_name]
+
+
 def timestamp():
     return datetime.now().strftime("%Y%m%d%H%M%S")
 
@@ -174,50 +243,51 @@
         review_queue_url = "request.cgi?action=""
         return self._fetch_attachment_ids_request_query(review_queue_url)
 
-    def _login_from_row(self, row):
-        first_cell = row.find("td")
-        # The first row is just headers, we skip it.
-        if not first_cell:
-            return None
-        # When there were no results, we have a fake "<none>" entry in the table.
-        if first_cell.find(text="<none>"):
-            return None
-        # Otherwise the <td> contains a single <a> which contains the login name or a single <i> with the string "<none>".
-        return str(first_cell.find("a").string).strip()
-
-    def _parse_logins_from_editusers_results(self, results_page):
-        soup = BeautifulSoup(results_page, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)
-        results_table = soup.find(id="admin_table")
-        logins = [self._login_from_row(row) for row in results_table('tr')]
-        # Filter out None from the logins.
-        return filter(lambda login: bool(login), logins)
-
     # This only works if your account has edituser privileges.
     # We could easily parse https://bugs.webkit.org/userprefs.cgi?tab=permissions to
     # check permissions, but bugzilla will just return an error if we don't have them.
-    def fetch_logins_matching_substring(self, search_string):
+    def fetch_login_userid_pairs_matching_substring(self, search_string):
         review_queue_url = "editusers.cgi?action="" % urllib.quote(search_string)
         results_page = self._load_query(review_queue_url)
-        return self._parse_logins_from_editusers_results(results_page)
+        # We could pull the EditUsersParser off Bugzilla if needed.
+        return EditUsersParser().login_userid_pairs_from_edit_user_results(results_page)
 
+    # FIXME: We should consider adding a BugzillaUser class.
+    def fetch_logins_matching_substring(self, search_string):
+        pairs = self.fetch_login_userid_pairs_matching_substring(search_string)
+        return map(lambda pair: pair[0], pairs)
 
+
 class Bugzilla(object):
-
     def __init__(self, dryrun=False, committers=committers.CommitterList()):
         self.dryrun = dryrun
         self.authenticated = False
         self.queries = BugzillaQueries(self)
         self.committers = committers
         self.cached_quips = []
+        self.edit_user_parser = EditUsersParser()
 
         # FIXME: We should use some sort of Browser mock object when in dryrun
         # mode (to prevent any mistakes).
         from webkitpy.thirdparty.autoinstalled.mechanize import Browser
         self.browser = Browser()
-        # Ignore bugs.webkit.org/robots.txt until we fix it to allow this
-        # script.
+        # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script.
         self.browser.set_handle_robots(False)
 
+    def fetch_user(self, user_id):
+        self.authenticate()
+        edit_user_page = self.browser.open(self.edit_user_url_for_id(user_id))
+        return self.edit_user_parser.user_dict_from_edit_user_page(edit_user_page)
+
+    def add_user_to_groups(self, user_id, group_names):
+        self.authenticate()
+        user_edit_page = self.browser.open(self.edit_user_url_for_id(user_id))
+        self.browser.select_form(nr=1)
+        for group_name in group_names:
+            group_string = self.edit_user_parser.group_string_from_name(user_edit_page, group_name)
+            self.browser.find_control(group_string).items[0].selected = True
+        self.browser.submit()
+
     def quips(self):
         # We only fetch and parse the list of quips once per instantiation
         # so that we do not burden bugs.webkit.org.
@@ -249,6 +319,9 @@
                                              attachment_id,
                                              action_param)
 
+    def edit_user_url_for_id(self, user_id):
+        return "%seditusers.cgi?action="" % (config_urls.bug_server_url, user_id)
+
     def _parse_attachment_flag(self,
                                element,
                                flag_name,

Modified: trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py (95237 => 95238)


--- trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py	2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py	2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,4 +1,4 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2011 Google Inc. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are
@@ -30,7 +30,7 @@
 import datetime
 import StringIO
 
-from .bugzilla import Bugzilla, BugzillaQueries
+from .bugzilla import Bugzilla, BugzillaQueries, EditUsersParser
 
 from webkitpy.common.checkout.changelog import parse_bug_id
 from webkitpy.common.system.outputcapture import OutputCapture
@@ -392,35 +392,37 @@
         queries = BugzillaQueries(Mock())
         queries._load_query("request.cgi?action=""
 
+
+class EditUsersParserTest(unittest.TestCase):
     _example_user_results = """
-    <div id="bugzilla-body">
-    <p>1 user found.</p>
-    <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
-      <tr bgcolor="#6666FF">
-          <th align="left">Edit user...
-          </th>
-          <th align="left">Real name
-          </th>
-          <th align="left">Account History
-          </th>
-      </tr>
-      <tr>
-          <td >
-              <a href=""
-            abarth&#64;webkit.org
-              </a>
-          </td>
-          <td >
-            Adam Barth
-          </td>
-          <td >
-              <a href=""
-            View
-              </a>
-          </td>
-      </tr>
-    </table>
-"""
+        <div id="bugzilla-body">
+        <p>1 user found.</p>
+        <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
+          <tr bgcolor="#6666FF">
+              <th align="left">Edit user...
+              </th>
+              <th align="left">Real name
+              </th>
+              <th align="left">Account History
+              </th>
+          </tr>
+          <tr>
+              <td >
+                  <a href=""
+                abarth&#64;webkit.org
+                  </a>
+              </td>
+              <td >
+                Adam Barth
+              </td>
+              <td >
+                  <a href=""
+                View
+                  </a>
+              </td>
+          </tr>
+        </table>
+    """
 
     _example_empty_user_results = """
     <div id="bugzilla-body">
@@ -438,11 +440,72 @@
     </table>
     """
 
-    def _assert_parsed_logins(self, results_page, expected_logins):
-        queries = BugzillaQueries(None)
-        logins = queries._parse_logins_from_editusers_results(results_page)
+    def _assert_login_userid_pairs(self, results_page, expected_logins):
+        parser = EditUsersParser()
+        logins = parser.login_userid_pairs_from_edit_user_results(results_page)
         self.assertEquals(logins, expected_logins)
 
-    def test_parse_logins_from_editusers_results(self):
-        self._assert_parsed_logins(self._example_user_results, ["aba...@webkit.org"])
-        self._assert_parsed_logins(self._example_empty_user_results, [])
+    def test_logins_from_editusers_results(self):
+        self._assert_login_userid_pairs(self._example_user_results, [("aba...@webkit.org", 1234)])
+        self._assert_login_userid_pairs(self._example_empty_user_results, [])
+
+    _example_user_page = """<table class="main"><tr>
+  <th><label for="" name:</label></th>
+  <td>eric&#64;webkit.org
+  </td>
+</tr>
+<tr>
+  <th><label for="" name:</label></th>
+  <td>Eric Seidel
+  </td>
+</tr>
+    <tr>
+      <th>Group access:</th>
+      <td>
+        <table class="groups">
+          <tr>
+          </tr>
+          <tr>
+            <th colspan="2">User is a member of these groups</th>
+          </tr>
+            <tr class="direct">
+              <td class="checkbox"><input type="checkbox"
+                           id="group_7"
+                           name="group_7"
+                           value="1" checked="checked" /></td>
+              <td class="groupname">
+                <label for=""
+                  <strong>canconfirm:</strong>
+                  Can confirm a bug.
+                </label>
+              </td>
+            </tr>
+            <tr class="direct">
+              <td class="checkbox"><input type="checkbox"
+                           id="group_6"
+                           name="group_6"
+                           value="1" /></td>
+              <td class="groupname">
+                <label for=""
+                  <strong>editbugs:</strong>
+                  Can edit all aspects of any bug.
+                /label>
+              </td>
+            </tr>
+        </table>
+      </td>
+    </tr>
+
+  <tr>
+    <th>Product responsibilities:</th>
+    <td>
+        <em>none</em>
+    </td>
+  </tr>
+</table>"""
+
+    def test_user_dict_from_edit_user_page(self):
+        parser = EditUsersParser()
+        user_dict = parser.user_dict_from_edit_user_page(self._example_user_page)
+        expected_user_dict = {u'login': u'e...@webkit.org', u'groups': set(['canconfirm']), u'name': u'Eric Seidel'}
+        self.assertEqual(expected_user_dict, user_dict)

Modified: trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py (95237 => 95238)


--- trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py	2011-09-15 22:06:31 UTC (rev 95237)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/__init__.py	2011-09-15 22:10:50 UTC (rev 95238)
@@ -1,10 +1,12 @@
 # Required for Python to search this directory for module files
 
+from webkitpy.tool.commands.adduserstogroups import AddUsersToGroups
 from webkitpy.tool.commands.bugfortest import BugForTest
 from webkitpy.tool.commands.bugsearch import BugSearch
 from webkitpy.tool.commands.download import *
 from webkitpy.tool.commands.earlywarningsystem import *
 from webkitpy.tool.commands.expectations import OptimizeExpectations
+from webkitpy.tool.commands.findusers import FindUsers
 from webkitpy.tool.commands.gardenomatic import GardenOMatic
 from webkitpy.tool.commands.openbugs import OpenBugs
 from webkitpy.tool.commands.prettydiff import PrettyDiff

Added: trunk/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py (0 => 95238)


--- trunk/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py	                        (rev 0)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py	2011-09-15 22:10:50 UTC (rev 95238)
@@ -0,0 +1,65 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class AddUsersToGroups(AbstractDeclarativeCommand):
+    name = "add-users-to-groups"
+    help_text = "Add users matching subtring to specified groups"
+
+    # This probably belongs in bugzilla.py
+    known_groups = ['canconfirm', 'editbugs']
+
+    def execute(self, options, args, tool):
+        search_string = args[0]
+        # FIXME: We could allow users to specify groups on the command line.
+        list_title = 'Add users matching "%s" which groups?' % search_string
+        # FIXME: Need a way to specify that "none" is not allowed.
+        # FIXME: We could lookup what groups the current user is able to grant from bugzilla.
+        groups = tool.user.prompt_with_list(list_title, self.known_groups, can_choose_multiple=True)
+        if not groups:
+            print "No groups specified."
+            return
+
+        login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string)
+        if not login_userid_pairs:
+            print "No users found matching '%s'" % search_string
+            return
+
+        print "Found %s users matching %s:" % (len(login_userid_pairs), search_string)
+        for (login, user_id) in login_userid_pairs:
+            print "%s (%s)" % (login, user_id)
+
+        confirm_message = "Are you sure you want add %s users to groups %s?  (This action cannot be undone using webkit-patch.)" % (len(login_userid_pairs), groups)
+        if not tool.user.confirm(confirm_message):
+            return
+
+        for (login, user_id) in login_userid_pairs:
+            print "Adding %s to %s" % (login, groups)
+            tool.bugs.add_user_to_groups(user_id, groups)

Added: trunk/Tools/Scripts/webkitpy/tool/commands/findusers.py (0 => 95238)


--- trunk/Tools/Scripts/webkitpy/tool/commands/findusers.py	                        (rev 0)
+++ trunk/Tools/Scripts/webkitpy/tool/commands/findusers.py	2011-09-15 22:10:50 UTC (rev 95238)
@@ -0,0 +1,44 @@
+# Copyright (c) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class FindUsers(AbstractDeclarativeCommand):
+    name = "find-users"
+    help_text = "Find users matching substring"
+
+    def execute(self, options, args, tool):
+        search_string = args[0]
+        login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string)
+        for (login, user_id) in login_userid_pairs:
+            user = tool.bugs.fetch_user(user_id)
+            groups_string = ", ".join(user['groups']) if user['groups'] else "none"
+            print "%s <%s> (%s) (%s)" % (user['name'], user['login'], user_id, groups_string)
+        else:
+            print "No users found matching '%s'" % search_string
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
http://lists.webkit.org/mailman/listinfo.cgi/webkit-changes

Reply via email to