Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/tracker/attachment_helpers.py b/tracker/attachment_helpers.py
index 9ed9a7c..8086182 100644
--- a/tracker/attachment_helpers.py
+++ b/tracker/attachment_helpers.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Functions to help display attachments and compute quotas."""
 from __future__ import print_function
@@ -9,8 +8,9 @@
 from __future__ import absolute_import
 
 import base64
+import hashlib
 import hmac
-import logging
+import six
 
 from framework import urls
 from services import secrets_svc
@@ -71,9 +71,9 @@
 
 def SignAttachmentID(aid):
   """One-way hash of attachment ID to make it harder for people to scan."""
-  digester = hmac.new(secrets_svc.GetXSRFKey())
-  digester.update(str(aid))
-  return base64.urlsafe_b64encode(digester.digest())
+  digester = hmac.new(secrets_svc.GetXSRFKey(), digestmod=hashlib.md5)
+  digester.update(six.ensure_binary(str(aid)))
+  return six.ensure_str(base64.urlsafe_b64encode(digester.digest()))
 
 
 def GetDownloadURL(attachment_id):
diff --git a/tracker/component_helpers.py b/tracker/component_helpers.py
index 786ab96..a98da55 100644
--- a/tracker/component_helpers.py
+++ b/tracker/component_helpers.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Helper functions for component-related servlets."""
 from __future__ import print_function
@@ -12,7 +11,7 @@
 import logging
 import re
 
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import tracker_bizobj
 
 
@@ -33,11 +32,15 @@
   deprecated = 'deprecated' in post_data
 
   admin_usernames = [
-      uname.strip() for uname in re.split('[,;\s]+', post_data['admins'])
-      if uname.strip()]
+      uname.strip()
+      for uname in re.split(r'[,;\s]+', post_data['admins'])
+      if uname.strip()
+  ]
   cc_usernames = [
-      uname.strip() for uname in re.split('[,;\s]+', post_data['cc'])
-      if uname.strip()]
+      uname.strip()
+      for uname in re.split(r'[,;\s]+', post_data['cc'])
+      if uname.strip()
+  ]
   all_user_ids = services.user.LookupUserIDs(
       mr.cnxn, admin_usernames + cc_usernames, autocreate=True)
 
@@ -48,7 +51,7 @@
       continue
     admin_id = all_user_ids[admin_name]
     if admin_id not in admin_ids:
-     admin_ids.append(admin_id)
+      admin_ids.append(admin_id)
 
   cc_ids = []
   for cc_name in cc_usernames:
@@ -60,8 +63,10 @@
       cc_ids.append(cc_id)
 
   label_strs = [
-    lab.strip() for lab in re.split('[,;\s]+', post_data['labels'])
-    if lab.strip()]
+      lab.strip()
+      for lab in re.split(r'[,;\s]+', post_data['labels'])
+      if lab.strip()
+  ]
 
   label_ids = services.config.LookupLabelIDs(
       mr.cnxn, mr.project_id, label_strs, autocreate=True)
diff --git a/tracker/componentcreate.py b/tracker/componentcreate.py
index 9974879..afdddbf 100644
--- a/tracker/componentcreate.py
+++ b/tracker/componentcreate.py
@@ -1,20 +1,16 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A servlet for project owners to create a new component def."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import time
 
-from framework import flaskservlet
 from framework import framework_helpers
 from framework import framework_views
-from framework import jsonfeed
 from framework import permissions
 from framework import servlet
 from framework import urls
@@ -29,7 +25,7 @@
 class ComponentCreate(servlet.Servlet):
   """Servlet allowing project owners to create a component."""
 
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/component-create-page.ezt'
 
   def AssertBasePermission(self, mr):
@@ -39,7 +35,7 @@
       mr: commonly used info parsed from the request.
     """
     super(ComponentCreate, self).AssertBasePermission(mr)
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'User is not allowed to administer this project')
 
@@ -77,7 +73,7 @@
         'initial_admins': [],
         'initial_cc': [],
         'initial_labels': [],
-        }
+    }
 
   def ProcessFormData(self, mr, post_data):
     """Validate and store the contents of the issues tracker admin page.
@@ -99,7 +95,7 @@
       if not parent_def:
         self.abort(500, 'parent component not found')
       allow_parent_edit = permissions.CanEditComponentDef(
-          mr.auth.effective_ids, mr.perms, mr.project, parent_def, config)
+          mr, self.services, parent_def, config)
       if not allow_parent_edit:
         raise permissions.PermissionException(
             'User is not allowed to add a subcomponent here')
@@ -137,11 +133,11 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.ADMIN_COMPONENTS, saved=1, ts=int(time.time()))
 
-  # def GetComponentCreatePage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetComponentCreatePage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostComponentCreatePage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostComponentCreatePage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def LeafNameErrorMessage(parent_path, leaf_name, config):
diff --git a/tracker/componentdetail.py b/tracker/componentdetail.py
index 01f2469..177e548 100644
--- a/tracker/componentdetail.py
+++ b/tracker/componentdetail.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A servlet for project and component owners to view and edit components."""
 from __future__ import print_function
@@ -20,6 +19,7 @@
 from framework import servlet
 from framework import timestr
 from framework import urls
+from gae_ts_mon import flask_handlers
 from tracker import component_helpers
 from tracker import tracker_bizobj
 from tracker import tracker_constants
@@ -86,7 +86,7 @@
         mr, component_def.modifier_id, component_def.modified)
 
     allow_edit = permissions.CanEditComponentDef(
-        mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
+        mr, self.services, component_def, config)
 
     subcomponents = tracker_bizobj.FindDescendantComponents(
         config, component_def)
@@ -111,7 +111,7 @@
         'created': created,
         'modifier': modifier,
         'modified': modified,
-        }
+    }
 
   def ProcessFormData(self, mr, post_data):
     """Validate and store the contents of the issues tracker admin page.
@@ -125,7 +125,7 @@
     """
     config, component_def = self._GetComponentDef(mr)
     allow_edit = permissions.CanEditComponentDef(
-        mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
+        mr, self.services, component_def, config)
     if not allow_edit:
       raise permissions.PermissionException(
           'User is not allowed to edit or delete this component')
@@ -244,3 +244,9 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.COMPONENT_DETAIL,
         component=new_path, saved=1, ts=int(time.time()))
+
+  def GetComponentDetailPage(self, **kwargs):
+    return self.handler(**kwargs)
+
+  def PostComponentDetailPage(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/field_helpers.py b/tracker/field_helpers.py
index d15f5e0..bd05cc0 100644
--- a/tracker/field_helpers.py
+++ b/tracker/field_helpers.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Helper functions for custom field sevlets."""
 from __future__ import print_function
@@ -12,6 +11,9 @@
 import itertools
 import logging
 import re
+import settings
+
+from google.appengine.api import app_identity
 
 from features import autolink_constants
 from framework import authdata
@@ -21,7 +23,7 @@
 from framework import permissions
 from framework import timestr
 from framework import validate
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import config_svc
 from tracker import tracker_bizobj
 
@@ -39,9 +41,9 @@
 
 
 def ListApplicableFieldDefs(issues, config):
-  # type: (Sequence[proto.tracker_pb2.Issue],
-  #     proto.tracker_pb2.ProjectIssueConfig) ->
-  #     Sequence[proto.tracker_pb2.FieldDef]
+  # type: (Sequence[mrproto.tracker_pb2.Issue],
+  #     mrproto.tracker_pb2.ProjectIssueConfig) ->
+  #     Sequence[mrproto.tracker_pb2.FieldDef]
   """Return the applicable FieldDefs for the given issues. """
   issue_labels = []
   issue_approval_ids = []
@@ -280,9 +282,39 @@
   return field_values
 
 
+def ValidateLabels(cnxn, services, project_id, labels, ezt_errors=None):
+  """Validate labels to block creation of new labels for the Chromium project in
+    Monorail and return an error string or None.
+
+  Args:
+    cnxn: MonorailConnection object.
+    services: Services object referencing services that can be queried.
+    project_id: Project ID.
+    labels: List of labels to be validated.
+
+  Returns:
+    A string containing an error message if there was one.
+  """
+  if settings.unit_test_mode or project_id in settings.label_freeze_project_ids:
+    new_labels = [
+        l for l in labels if services.config.LookupLabelID(
+            cnxn, project_id, l, autocreate=False, case_sensitive=False) is None
+        and not settings.is_label_allowed(project_id, l)
+    ]
+    if len(new_labels) > 0:
+      err_msg = (
+          "The creation of new labels is blocked for the Chromium project"
+          " in Monorail. To continue with editing your issue, please"
+          " remove: {} label(s).").format(", ".join(new_labels))
+      if ezt_errors is not None:
+        ezt_errors.labels = err_msg
+      return err_msg
+  return None
+
+
 def ValidateCustomFieldValue(cnxn, project, services, field_def, field_val):
-  # type: (MonorailConnection, proto.tracker_pb2.Project, Services,
-  #     proto.tracker_pb2.FieldDef, proto.tracker_pb2.FieldValue) -> str
+  # type: (MonorailConnection, mrproto.tracker_pb2.Project, Services,
+  #     mrproto.tracker_pb2.FieldDef, mrproto.tracker_pb2.FieldValue) -> str
   """Validate one custom field value and return an error string or None.
 
   Args:
@@ -353,9 +385,9 @@
 def ValidateCustomFields(
     cnxn, services, field_values, config, project, ezt_errors=None, issue=None):
   # type: (MonorailConnection, Services,
-  #     Collection[proto.tracker_pb2.FieldValue],
-  #     proto.tracker_pb2.ProjectConfig, proto.tracker_pb2.Project,
-  #     Optional[EZTError], Optional[proto.tracker_pb2.Issue]) ->
+  #     Collection[mrproto.tracker_pb2.FieldValue],
+  #     mrproto.tracker_pb2.ProjectConfig, mrproto.tracker_pb2.Project,
+  #     Optional[EZTError], Optional[mrproto.tracker_pb2.Issue]) ->
   #     Sequence[str]
   """Validate given fields and report problems in error messages."""
   fds_by_id = {fd.field_id: fd for fd in config.field_defs}
diff --git a/tracker/fieldcreate.py b/tracker/fieldcreate.py
index b1f2316..245b697 100644
--- a/tracker/fieldcreate.py
+++ b/tracker/fieldcreate.py
@@ -1,27 +1,22 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A servlet for project owners to create a new field def."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import re
 import time
 
 import ezt
 
-from framework import exceptions
-from framework import flaskservlet
 from framework import framework_helpers
-from framework import jsonfeed
 from framework import permissions
 from framework import servlet
 from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import field_helpers
 from tracker import tracker_bizobj
 from tracker import tracker_constants
@@ -31,7 +26,7 @@
 class FieldCreate(servlet.Servlet):
   """Servlet allowing project owners to create a custom field."""
 
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/field-create-page.ezt'
 
   def AssertBasePermission(self, mr):
@@ -41,7 +36,7 @@
       mr: commonly used info parsed from the request.
     """
     super(FieldCreate, self).AssertBasePermission(mr)
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'You are not allowed to administer this project')
 
@@ -125,7 +120,8 @@
           parsed.is_restricted_field), 'Approval fields cannot be restricted.'
       if parsed.approvers_str:
         approver_ids_dict = self.services.user.LookupUserIDs(
-            mr.cnxn, re.split('[,;\s]+', parsed.approvers_str),
+            mr.cnxn,
+            re.split(r'[,;\s]+', parsed.approvers_str),
             autocreate=True)
         approver_ids = list(set(approver_ids_dict.values()))
       else:
@@ -199,11 +195,11 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.ADMIN_LABELS, saved=1, ts=int(time.time()))
 
-  # def GetFieldCreate(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetFieldCreate(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostFieldCreate(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostFieldCreate(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def FieldNameErrorMessage(field_name, config):
diff --git a/tracker/fielddetail.py b/tracker/fielddetail.py
index 76cf378..ae607f2 100644
--- a/tracker/fielddetail.py
+++ b/tracker/fielddetail.py
@@ -1,27 +1,23 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A servlet for project and component owners to view and edit field defs."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import time
 import re
 
 import ezt
 
-from framework import exceptions
-from framework import flaskservlet
 from framework import framework_helpers
 from framework import framework_views
 from framework import permissions
 from framework import servlet
 from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import field_helpers
 from tracker import tracker_bizobj
 from tracker import tracker_helpers
@@ -31,7 +27,7 @@
 class FieldDetail(servlet.Servlet):
   """Servlet allowing project owners to view and edit a custom field."""
 
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/field-detail-page.ezt'
 
   def _GetFieldDef(self, mr):
@@ -190,7 +186,8 @@
 
       if parsed.approvers_str:
         approver_ids_dict = self.services.user.LookupUserIDs(
-            mr.cnxn, re.split('[,;\s]+', parsed.approvers_str),
+            mr.cnxn,
+            re.split(r'[,;\s]+', parsed.approvers_str),
             autocreate=True)
         approver_ids = list(set(approver_ids_dict.values()))
       else:
@@ -249,8 +246,8 @@
           mr, urls.FIELD_DETAIL, field=field_def.field_name,
           saved=1, ts=int(time.time()))
 
-  # def GetFieldDetail(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetFieldDetail(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostFieldDetail(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostFieldDetail(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/fltconversion.py b/tracker/fltconversion.py
index 63a7e63..a88f841 100644
--- a/tracker/fltconversion.py
+++ b/tracker/fltconversion.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """FLT task to be manually triggered to convert launch issues."""
 from __future__ import print_function
@@ -18,7 +17,7 @@
 from framework import permissions
 from framework import exceptions
 from framework import jsonfeed
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import template_helpers
 from tracker import tracker_bizobj
 
@@ -134,7 +133,7 @@
     'phase_map, approvals_to_labels, labels_re')
 
 
-class FLTConvertTask(jsonfeed.FlaskInternalTask):
+class FLTConvertTask(jsonfeed.InternalTask):
   """FLTConvert converts current Type=Launch issues into Type=FLT-Launch."""
 
   def AssertBasePermission(self, mr):
diff --git a/tracker/issueadmin.py b/tracker/issueadmin.py
index 10fbdc8..849e59f 100644
--- a/tracker/issueadmin.py
+++ b/tracker/issueadmin.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Servlets for issue tracker configuration.
 
@@ -14,18 +13,12 @@
 from __future__ import absolute_import
 
 import collections
-import itertools
 import logging
 import time
 
-import ezt
-
 from features import filterrules_helpers
 from features import filterrules_views
 from features import savedqueries_helpers
-from framework import authdata
-from framework import flaskservlet
-from framework import framework_bizobj
 from framework import framework_constants
 from framework import framework_helpers
 from framework import framework_views
@@ -33,8 +26,7 @@
 from framework import permissions
 from framework import servlet
 from framework import urls
-from proto import tracker_pb2
-from tracker import field_helpers
+from mrproto import tracker_pb2
 from tracker import tracker_bizobj
 from tracker import tracker_constants
 from tracker import tracker_helpers
@@ -44,7 +36,7 @@
 class IssueAdminBase(servlet.Servlet):
   """Base class for servlets allowing project owners to configure tracker."""
 
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
   _PROCESS_SUBTAB = None  # specified in subclasses
 
   def GatherPageData(self, mr):
@@ -63,12 +55,19 @@
     labels_text = tracker_views.LabelDefsAsText(config)
 
     return {
-        'admin_tab_mode': self._PROCESS_SUBTAB,
-        'config': config_view,
-        'open_text': open_text,
-        'closed_text': closed_text,
-        'labels_text': labels_text,
-        }
+        'admin_tab_mode':
+            self._PROCESS_SUBTAB,
+        'config':
+            config_view,
+        'open_text':
+            open_text,
+        'closed_text':
+            closed_text,
+        'labels_text':
+            labels_text,
+        'can_edit_project':
+            permissions.CanEditProjectConfig(mr, self.services) or None,
+    }
 
   def ProcessFormData(self, mr, post_data):
     """Validate and store the contents of the issues tracker admin page.
@@ -91,7 +90,7 @@
   """Servlet allowing project owners to configure well-known statuses."""
 
   _PAGE_TEMPLATE = 'tracker/admin-statuses-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_STATUSES
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_STATUSES
 
   def ProcessSubtabForm(self, post_data, mr):
     """Process the status definition section of the admin page.
@@ -103,7 +102,7 @@
     Returns:
       The URL of the page to show after processing.
     """
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'Only project owners may edit the status definitions')
 
@@ -142,18 +141,18 @@
 
     return urls.ADMIN_STATUSES
 
-  # def GetAdminStatusesPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAdminStatusesPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostAdminStatusesPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostAdminStatusesPage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class AdminLabels(IssueAdminBase):
   """Servlet allowing project owners to labels and fields."""
 
   _PAGE_TEMPLATE = 'tracker/admin-labels-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_LABELS
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_LABELS
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -186,7 +185,7 @@
     Returns:
       The URL of the page to show after processing.
     """
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'Only project owners may edit the label definitions')
 
@@ -231,18 +230,18 @@
 
     return urls.ADMIN_LABELS
 
-  # def GetAdminLabelsPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAdminLabelsPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostAdminLabelsPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostAdminLabelsPage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class AdminTemplates(IssueAdminBase):
   """Servlet allowing project owners to configure templates."""
 
   _PAGE_TEMPLATE = 'tracker/admin-templates-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -265,7 +264,7 @@
     Returns:
       The URL of the page to show after processing.
     """
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'Only project owners may edit the default templates')
 
@@ -301,18 +300,18 @@
     return (GetSelectedTemplateID('default_template_for_developers'),
             GetSelectedTemplateID('default_template_for_users'))
 
-  # def GetAdminTemplatesPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAdminTemplatesPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostAdminTemplatesPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostAdminTemplatesPage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class AdminComponents(IssueAdminBase):
   """Servlet allowing project owners to view the list of components."""
 
   _PAGE_TEMPLATE = 'tracker/admin-components-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_COMPONENTS
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_COMPONENTS
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -389,7 +388,7 @@
 
     for component_def in component_defs:
       allow_edit = permissions.CanEditComponentDef(
-          mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
+          mr, self.services, component_def, config)
       if not allow_edit:
         perm_errors.append(component_def.path)
 
@@ -417,18 +416,18 @@
         failed_templ=','.join(templates_errors),
         deleted=','.join(deleted_components))
 
-  # def GetAdminComponentsPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAdminComponentsPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostAdminComponentsPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostAdminComponentsPage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class AdminViews(IssueAdminBase):
   """Servlet for project owners to set default columns, axes, and sorting."""
 
   _PAGE_TEMPLATE = 'tracker/admin-views-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_VIEWS
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_VIEWS
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -466,7 +465,7 @@
     Returns:
       The URL of the page to show after processing.
     """
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'Only project owners may edit the default views')
     existing_queries = savedqueries_helpers.ParseSavedQueries(
@@ -488,11 +487,11 @@
 
     return urls.ADMIN_VIEWS
 
-  # def GetAdminViewsPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAdminViewsPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostAdminViewsPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostAdminViewsPage(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def _ParseListPreferences(post_data):
@@ -540,7 +539,7 @@
   """Servlet allowing project owners to configure filter rules."""
 
   _PAGE_TEMPLATE = 'tracker/admin-rules-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_RULES
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_RULES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -549,7 +548,7 @@
       mr: commonly used info parsed from the request.
     """
     super(AdminRules, self).AssertBasePermission(mr)
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'User is not allowed to administer this project')
 
@@ -617,8 +616,8 @@
 
     return urls.ADMIN_RULES
 
-  # def GetAdminRulesPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAdminRulesPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostAdminRulesPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostAdminRulesPage(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issueadvsearch.py b/tracker/issueadvsearch.py
index f702763..7185b1f 100644
--- a/tracker/issueadvsearch.py
+++ b/tracker/issueadvsearch.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Classes that implement the advanced search feature page.
 
@@ -13,11 +12,9 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import re
 
 from features import savedqueries_helpers
-from framework import flaskservlet
 from framework import framework_helpers
 from framework import permissions
 from framework import servlet
@@ -32,7 +29,7 @@
   """IssueAdvancedSearch shows a form to enter an advanced search."""
 
   _PAGE_TEMPLATE = 'tracker/issue-advsearch-page.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   # This form *only* redirects to a GET request, and permissions are checked
   # in that handler.
@@ -123,8 +120,8 @@
       search_term = '%s%s' % (operator, ','.join(values))
       search_query.append(search_term)
 
-  # def GetIssueAdvSearchPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueAdvSearchPage(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostIssueAdvSearchPage(self, **kwargs):
-  #   return self.handler(**kwargs)
\ No newline at end of file
+  def PostIssueAdvSearchPage(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issueattachment.py b/tracker/issueattachment.py
index 26982d0..6554447 100644
--- a/tracker/issueattachment.py
+++ b/tracker/issueattachment.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Issue Tracker code to serve out issue attachments.
 
@@ -14,26 +13,16 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import base64
 import logging
-import os
-import re
-from six.moves import urllib
 
 from google.appengine.api import app_identity
-from google.appengine.api import images
 
 from framework import exceptions
-from framework import flaskservlet
 from framework import framework_constants
-from framework import framework_helpers
 from framework import gcs_helpers
-from framework import permissions
 from framework import servlet
-from framework import urls
 from tracker import attachment_helpers
 from tracker import tracker_helpers
-from tracker import tracker_views
 
 
 # This will likely appear blank or as a broken image icon in the browser.
@@ -88,8 +77,8 @@
           bucket_name, gcs_object_id, filename):
         gcs_object_id = gcs_object_id + '-download'
 
-    url = gcs_helpers.SignUrl(bucket_name, gcs_object_id)
-    self.redirect(url, abort=True)
+    redirect_url = gcs_helpers.SignUrl(bucket_name, gcs_object_id)
+    raise exceptions.RedirectException(redirect_url)
 
-  # def GetAttachmentPage(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAttachmentPage(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issueattachmenttext.py b/tracker/issueattachmenttext.py
index 40db170..c12d135 100644
--- a/tracker/issueattachmenttext.py
+++ b/tracker/issueattachmenttext.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Servlet to safely display textual issue attachments.
 
@@ -13,6 +12,7 @@
 from __future__ import absolute_import
 
 import logging
+import six
 
 import ezt
 from google.appengine.api import app_identity
@@ -20,7 +20,6 @@
 
 from features import prettify
 from framework import exceptions
-from framework import flaskservlet
 from framework import filecontent
 from framework import permissions
 from framework import servlet
@@ -34,7 +33,7 @@
   """AttachmentText displays textual attachments much like source browsing."""
 
   _PAGE_TEMPLATE = 'tracker/issue-attachment-text.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   def GatherPageData(self, mr):
     """Parse the attachment ID from the request and serve its content.
@@ -80,7 +79,7 @@
       self.abort(400, 'not a text file')
 
     u_text, is_binary, too_large = filecontent.DecodeFileContents(content)
-    lines = prettify.PrepareSourceLinesForHighlighting(u_text.encode('utf8'))
+    lines = prettify.PrepareSourceLinesForHighlighting(six.ensure_str(u_text))
 
     config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
     granted_perms = tracker_bizobj.GetGrantedPerms(
@@ -108,5 +107,5 @@
 
     return page_data
 
-  # def GetAttachmentText(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetAttachmentText(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issuebulkedit.py b/tracker/issuebulkedit.py
index 3ea4d3f..f902a86 100644
--- a/tracker/issuebulkedit.py
+++ b/tracker/issuebulkedit.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Classes that implement the issue bulk edit page and related forms.
 
@@ -24,7 +23,6 @@
 from features import filterrules_helpers
 from features import send_notifications
 from framework import exceptions
-from framework import flaskservlet
 from framework import framework_constants
 from framework import framework_views
 from framework import permissions
@@ -42,7 +40,7 @@
   """IssueBulkEdit lists multiple issues and allows an edit to all of them."""
 
   _PAGE_TEMPLATE = 'tracker/issue-bulk-edit-page.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
   _SECONDS_OVERHEAD = 4
   _SECONDS_PER_UPDATE = 0.12
   _SLOWNESS_THRESHOLD = 10
@@ -171,47 +169,45 @@
     """
     if not mr.local_id_list:
       logging.info('missing issue local IDs, probably tampered')
-      #TODO: switch when convert /p to flask
-      # self.response.status_code = http_client.BAD_REQUEST
-      self.response.status = http_client.BAD_REQUEST
+      self.response.status_code = http_client.BAD_REQUEST
       return
 
     # Check that the user is logged in; anon users cannot update issues.
     if not mr.auth.user_id:
       logging.info('user was not logged in, cannot update issue')
-      #TODO: switch when convert /p to flask
-      # self.response.status_code = http_client.BAD_REQUEST
-      self.response.status = http_client.BAD_REQUEST
+      self.response.status_code = http_client.BAD_REQUEST
       return
 
     # Check that the user has permission to add a comment, and to enter
     # metadata if they are trying to do that.
     if not self.CheckPerm(mr, permissions.ADD_ISSUE_COMMENT):
       logging.info('user has no permission to add issue comment')
-      #TODO: switch when convert /p to flask
-      # self.response.status_code = http_client.BAD_REQUEST
-      self.response.status = http_client.BAD_REQUEST
+      self.response.status_code = http_client.BAD_REQUEST
       return
 
     if not self.CheckPerm(mr, permissions.EDIT_ISSUE):
       logging.info('user has no permission to edit issue metadata')
-      #TODO: switch when convert /p to flask
-      # self.response.status_code = http_client.BAD_REQUEST
-      self.response.status = http_client.BAD_REQUEST
+      self.response.status_code = http_client.BAD_REQUEST
       return
 
     move_to = post_data.get('move_to', '').lower()
     if move_to and not self.CheckPerm(mr, permissions.DELETE_ISSUE):
       logging.info('user has no permission to move issue')
-      #TODO: switch when convert /p to flask
-      # self.response.status_code = http_client.BAD_REQUEST
-      self.response.status = http_client.BAD_REQUEST
+      self.response.status_code = http_client.BAD_REQUEST
       return
 
     config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
 
     parsed = tracker_helpers.ParseIssueRequest(
         mr.cnxn, post_data, self.services, mr.errors, mr.project_name)
+
+    field_helpers.ValidateLabels(
+        mr.cnxn,
+        self.services,
+        mr.project_id,
+        parsed.labels,
+        ezt_errors=mr.errors)
+
     bounce_labels = (
         parsed.labels[:] +
         ['-%s' % lr for lr in parsed.labels_remove])
@@ -341,6 +337,35 @@
             issue for issue in editable_issues
             if not permissions.GetRestrictions(issue)]
 
+      # Check that we can modify issues we want to block with.
+      if post_data.get('blocked_on'):
+        for issue_ref in post_data.get('blocked_on').split(','):
+          if not issue_ref:
+            continue
+          project_name, iid = tracker_bizobj.ParseIssueRef(issue_ref)
+          project_name = project_name or mr.project_name
+          project = self.services.project.GetProjectByName(
+              mr.cnxn, project_name)
+          issue = self.services.issue.GetIssueByLocalID(
+              mr.cnxn, project.project_id, iid, use_cache=False)
+          if not self._CheckEditIssuePermissions(mr, project, issue):
+            mr.errors.blocked_on = 'Target issue %s cannot be modified' % (iid)
+            break
+
+      if post_data.get('blocking'):
+        for issue_ref in post_data.get('blocking').split(','):
+          if not issue_ref:
+            continue
+          project_name, iid = tracker_bizobj.ParseIssueRef(issue_ref)
+          project_name = project_name or mr.project_name
+          project = self.services.project.GetProjectByName(
+              mr.cnxn, project_name)
+          issue = self.services.issue.GetIssueByLocalID(
+              mr.cnxn, project.project_id, iid, use_cache=False)
+          if not self._CheckEditIssuePermissions(mr, project, issue):
+            mr.errors.blocking = 'Target issue %s cannot be modified' % (iid)
+            break
+
       # If 'Duplicate' status is specified ensure there are no permission issues
       # with the issue we want to merge with.
       if post_data.get('merge_into'):
@@ -349,9 +374,10 @@
               mr.cnxn, self.services, mr.project_name, post_data, parsed.status,
               config, issue, mr.errors)
           if merge_into_issue:
-            merge_allowed = tracker_helpers.IsMergeAllowed(
-                merge_into_issue, mr, self.services)
-            if not merge_allowed:
+            project = self.services.project.GetProjectByName(
+                mr.cnxn, issue.project_name)
+            if not self._CheckEditIssuePermissions(mr, project,
+                                                   merge_into_issue):
               mr.errors.merge_into_id = 'Target issue %s cannot be modified' % (
                                             merge_into_issue.local_id)
               break
@@ -483,8 +509,15 @@
     return tracker_helpers.FormatIssueListURL(
         mr, config, saved=len(mr.local_id_list), ts=int(time.time()))
 
-  # def GetIssueBulkEdit(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueBulkEdit(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostIssueBulkEdit(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostIssueBulkEdit(self, **kwargs):
+    return self.handler(**kwargs)
+
+  def _CheckEditIssuePermissions(self, mr, project, issue):
+    config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id)
+    granted_perms = tracker_bizobj.GetGrantedPerms(
+        issue, mr.auth.effective_ids, config)
+    return tracker_helpers.CanEditProjectIssue(
+        mr, project, issue, granted_perms)
diff --git a/tracker/issuedetailezt.py b/tracker/issuedetailezt.py
index 3a10443..233ea57 100644
--- a/tracker/issuedetailezt.py
+++ b/tracker/issuedetailezt.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Classes that implement the issue detail page and related forms.
 
@@ -14,45 +13,20 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import json
-import logging
-import time
-import ezt
-
 import settings
-from api import converters
 from businesslogic import work_env
 from features import features_bizobj
-from features import send_notifications
 from features import hotlist_helpers
-from features import hotlist_views
 from framework import exceptions
-from framework import flaskservlet
-from framework import framework_bizobj
-from framework import framework_constants
 from framework import framework_helpers
-from framework import framework_views
 from framework import jsonfeed
-from framework import paginate
 from framework import permissions
 from framework import servlet
 from framework import servlet_helpers
-from framework import sorting
-from framework import sql
-from framework import template_helpers
 from framework import urls
-from framework import xsrf
-from proto import user_pb2
-from proto import tracker_pb2
 from services import features_svc
-from services import tracker_fulltext
-from tracker import field_helpers
-from tracker import tracker_bizobj
 from tracker import tracker_constants
 from tracker import tracker_helpers
-from tracker import tracker_views
-
-from google.protobuf import json_format
 
 
 def CheckMoveIssueRequest(
@@ -126,9 +100,7 @@
 
 class FlipperRedirectBase(servlet.Servlet):
 
-  # pylint: disable=arguments-differ
-  # pylint: disable=unused-argument
-  def get(self, project_name=None, viewed_username=None, hotlist_id=None):
+  def get(self):
     with work_env.WorkEnv(self.mr, self.services) as we:
       hotlist_id = self.mr.GetIntParam('hotlist_id')
       current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id,
@@ -159,24 +131,22 @@
 class FlipperNext(FlipperRedirectBase):
   next_handler = True
 
-  # def GetFlipperNextRedirectPage(self, **kwargs):
-  #   self.next_handler = True
-  #   return self.handler(**kwargs)
+  def GetFlipperNextRedirectPage(self, **kwargs):
+    self.next_handler = True
+    return self.handler(**kwargs)
 
 
 class FlipperPrev(FlipperRedirectBase):
   next_handler = False
 
-  # def GetFlipperPrevRedirectPage(self, **kwargs):
-  #   self.next_handler = False
-  #   return self.handler(**kwargs)
+  def GetFlipperPrevRedirectPage(self, **kwargs):
+    self.next_handler = False
+    return self.handler(**kwargs)
 
 
 class FlipperList(servlet.Servlet):
-  # pylint: disable=arguments-differ
-  # pylint: disable=unused-argument
-  # TODO: (monorail:6511)change to get(self) when convert to flask
-  def get(self, project_name=None, viewed_username=None, hotlist_id=None):
+
+  def get(self):
     with work_env.WorkEnv(self.mr, self.services) as we:
       hotlist_id = self.mr.GetIntParam('hotlist_id')
       current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id,
@@ -199,11 +169,10 @@
                                                hotlist, self.services)
     self.redirect(url)
 
-  # def GetFlipperList(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetFlipperList(self, **kwargs):
+    return self.handler(**kwargs)
 
 
-# TODO: (monorail:6511) change to flaskJsonFeed when convert to flask
 class FlipperIndex(jsonfeed.JsonFeed):
   """Return a JSON object of an issue's index in search.
 
@@ -274,11 +243,11 @@
       'total_count': total_count,
     }
 
-  # def GetFlipperIndex(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetFlipperIndex(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostFlipperIndex(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostFlipperIndex(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def _ShouldShowFlipper(mr, services):
diff --git a/tracker/issueentry.py b/tracker/issueentry.py
index 2ae59d8..287c638 100644
--- a/tracker/issueentry.py
+++ b/tracker/issueentry.py
@@ -1,14 +1,12 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Servlet that implements the entry of new issues."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import collections
 import difflib
 import logging
 import string
@@ -16,9 +14,7 @@
 
 from businesslogic import work_env
 from features import hotlist_helpers
-from features import send_notifications
 from framework import exceptions
-from framework import flaskservlet
 from framework import framework_bizobj
 from framework import framework_constants
 from framework import framework_helpers
@@ -34,7 +30,7 @@
 from tracker import tracker_constants
 from tracker import tracker_helpers
 from tracker import tracker_views
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 PLACEHOLDER_SUMMARY = 'Enter one-line summary'
 PHASES_WITH_MILESTONES = ['Beta', 'Stable', 'Stable-Exp', 'Stable-Full']
@@ -46,7 +42,7 @@
   """IssueEntry shows a page with a simple form to enter a new issue."""
 
   _PAGE_TEMPLATE = 'tracker/issue-entry-page.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   # The issue filing wizard is a separate app that posted back to Monorail's
   # issue entry page. To make this possible for the wizard, we need to allow
@@ -392,6 +388,12 @@
         mr, config, field_values, labels, template.field_values,
         template.labels)
 
+    # This ValidateLabels call is redundant with work already done
+    # in CreateIssue. However, this instance passes in an ezt_errors object
+    # to allow showing related errors next to the fields they happen on.
+    field_helpers.ValidateLabels(
+        mr.cnxn, self.services, mr.project_id, labels, ezt_errors=mr.errors)
+
     # This ValidateCustomFields call is redundant with work already done
     # in CreateIssue. However, this instance passes in an ezt_errors object
     # to allow showing related errors next to the fields they happen on.
@@ -466,7 +468,7 @@
         except exceptions.OverAttachmentQuota:
           mr.errors.attachments = 'Project attachment quota exceeded.'
         except exceptions.InputException as e:
-          if 'Undefined or deprecated component with id' in e.message:
+          if 'Undefined or deprecated component with id' in str(e):
             mr.errors.components = 'Undefined or deprecated component'
 
     mr.template_name = parsed.template_name
@@ -512,11 +514,11 @@
 
     return template
 
-  # def GetIssueEntry(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueEntry(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostIssueEntry(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostIssueEntry(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 def _AttachDefaultApprovers(config, approval_values):
diff --git a/tracker/issueentryafterlogin.py b/tracker/issueentryafterlogin.py
index 3008d54..f60cb71 100644
--- a/tracker/issueentryafterlogin.py
+++ b/tracker/issueentryafterlogin.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Redirect to /issues/entry or an external URL (like the wizard).
 """
@@ -11,7 +10,7 @@
 
 import logging
 
-from framework import flaskservlet
+from framework import exceptions
 from framework import servlet
 from framework import servlet_helpers
 
@@ -28,7 +27,8 @@
 
     entry_page_url = servlet_helpers.ComputeIssueEntryURL(mr)
     logging.info('Redirecting to %r', entry_page_url)
-    self.redirect(entry_page_url, abort=True)
 
-  # def GetIssueEntryAfterLogin(self, **kwargs):
-  #   return self.handler(**kwargs)
+    raise exceptions.RedirectException(entry_page_url)
+
+  def GetIssueEntryAfterLogin(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issueexport.py b/tracker/issueexport.py
index 91620c4..6fe7018 100644
--- a/tracker/issueexport.py
+++ b/tracker/issueexport.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Servlet to export a range of issues in JSON format.
 """
@@ -9,16 +8,12 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import time
 
-import ezt
-
 from businesslogic import work_env
 from features import savedqueries_helpers
-from framework import flaskservlet
-from framework import permissions
 from framework import jsonfeed
+from framework import permissions
 from framework import servlet
 from tracker import tracker_bizobj
 
@@ -27,7 +22,7 @@
   """IssueExportControls let's an admin choose how to export issues."""
 
   _PAGE_TEMPLATE = 'tracker/issue-export-page.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   def AssertBasePermission(self, mr):
     """Make sure that the logged in user has permission to view this page."""
@@ -70,11 +65,10 @@
         'saved_queries': saved_query_views,
     }
 
-  # def GetIssueExport(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueExport(self, **kwargs):
+    return self.handler(**kwargs)
 
 
-# TODO: convert to FLaskJsonFeed while conver to flask
 class IssueExportJSON(jsonfeed.JsonFeed):
   """IssueExport shows a range of issues in JSON format."""
 
@@ -154,7 +148,7 @@
     }
     return json_data
 
-  def _MakeAmendmentJSON(self, amendment, email_dict):
+  def _MakeAmendmentJSON(self, amendment, email_dict, comp_dict):
     amendment_json = {
         'field': amendment.field.name,
     }
@@ -170,6 +164,24 @@
       amendment_json.update(
           {'removed_emails': [email_dict.get(user_id)
                               for user_id in amendment.removed_user_ids]})
+    if amendment.added_component_ids:
+      amendment_json.update(
+          {
+              'added_components':
+                  [
+                      comp_dict.get(component_id).path.lower()
+                      for component_id in amendment.added_component_ids
+                  ]
+          })
+    if amendment.removed_component_ids:
+      amendment_json.update(
+          {
+              'removed_components':
+                  [
+                      comp_dict.get(component_id).path.lower()
+                      for component_id in amendment.removed_component_ids
+                  ]
+          })
     return amendment_json
 
   def _MakeAttachmentJSON(self, attachment):
@@ -183,11 +195,13 @@
     }
     return attachment_json
 
-  def _MakeCommentJSON(self, comment, email_dict):
+  def _MakeCommentJSON(self, comment, email_dict, comp_dict):
     if comment.deleted_by:
       return None
-    amendments = [self._MakeAmendmentJSON(a, email_dict)
-                  for a in comment.amendments]
+    amendments = [
+        self._MakeAmendmentJSON(a, email_dict, comp_dict)
+        for a in comment.amendments
+    ]
     attachments = [self._MakeAttachmentJSON(a)
                    for a in comment.attachments]
     comment_json = {
@@ -242,10 +256,13 @@
     descriptions = [c for c in comment_list if c.is_description]
     for i, d in enumerate(descriptions):
       d.description_num = str(i+1)
-    comments = [self._MakeCommentJSON(c, email_dict) for c in comment_list]
     phase_dict = {phase.phase_id: phase.name for phase in issue.phases}
     config = self.services.config.GetProjectConfig(
         mr.cnxn, mr.project.project_id)
+    comp_dict = {comp.component_id: comp for comp in config.component_defs}
+    comments = [
+        self._MakeCommentJSON(c, email_dict, comp_dict) for c in comment_list
+    ]
     fd_dict = {fd.field_id: fd for fd in config.field_defs}
     issue_json = {
         'local_id': issue.local_id,
@@ -283,8 +300,8 @@
         issue_json['merged_into'] = merge.local_id
     return issue_json
 
-  # def GetIssueExportJSON(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueExportJSON(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostIssueExportJSON(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostIssueExportJSON(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issueimport.py b/tracker/issueimport.py
index bd54db9..042d0c7 100644
--- a/tracker/issueimport.py
+++ b/tracker/issueimport.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Servlet to import a file of issues in JSON format.
 """
@@ -11,20 +10,14 @@
 
 import collections
 import json
-import logging
-import time
-
-import ezt
 
 from features import filterrules_helpers
-from framework import flaskservlet
 from framework import framework_helpers
 from framework import jsonfeed
 from framework import permissions
 from framework import servlet
-from framework import urls
-from proto import tracker_pb2
-
+from mrproto import tracker_pb2
+from tracker import tracker_bizobj
 
 ParserState = collections.namedtuple(
     'ParserState',
@@ -36,7 +29,7 @@
   """IssueImport loads a file of issues in JSON format."""
 
   _PAGE_TEMPLATE = 'tracker/issue-import-page.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   def AssertBasePermission(self, mr):
     """Make sure that the logged in user has permission to view this page."""
@@ -180,8 +173,9 @@
       issue.owner_id = user_id_dict[issue_json['owner']]
     if issue_json.get('closed'):
       issue.closed_timestamp = issue_json['closed']
+    config = self.services.config.GetProjectConfig(cnxn, project_id)
     comments = [self._ParseComment(
-                    project_id, user_id_dict, comment_json, event_log)
+                    project_id, user_id_dict, comment_json, event_log, config)
                 for comment_json in issue_json.get('comments', [])]
 
     starrers = [user_id_dict[starrer] for starrer in issue_json['starrers']]
@@ -209,7 +203,8 @@
 
     return field
 
-  def _ParseComment(self, project_id, user_id_dict, comment_json, event_log):
+  def _ParseComment(
+      self, project_id, user_id_dict, comment_json, event_log, config):
     comment = tracker_pb2.IssueComment(
         # Note: issue_id is filled in after the issue is saved.
         project_id=project_id,
@@ -219,7 +214,7 @@
 
     for amendment in comment_json['amendments']:
       comment.amendments.append(
-          self._ParseAmendment(amendment, user_id_dict, event_log))
+          self._ParseAmendment(amendment, user_id_dict, event_log, config))
 
     for attachment in comment_json['attachments']:
       comment.attachments.append(
@@ -230,7 +225,7 @@
 
     return comment
 
-  def _ParseAmendment(self, amendment_json, user_id_dict, _event_log):
+  def _ParseAmendment(self, amendment_json, user_id_dict, _event_log, config):
     amendment = tracker_pb2.Amendment(
         field=tracker_pb2.FieldID(amendment_json['field']))
 
@@ -244,7 +239,16 @@
     if 'removed_users' in amendment_json:
       amendment.removed_user_ids.extend(
           [user_id_dict[email] for email in amendment_json['removed_users']])
-
+    if 'added_components' in amendment_json:
+      for comp in amendment_json['added_components']:
+        comp_def = tracker_bizobj.FindComponentDef(comp, config)
+        if comp_def:
+          amendment.added_component_ids.extend(comp_def.component_id)
+    if 'removed_components' in amendment_json:
+      for comp in amendment_json['removed_components']:
+        comp_def = tracker_bizobj.FindComponentDef(comp, config)
+        if comp_def:
+          amendment.removed_component_ids.extend(comp_def.component_id)
     return amendment
 
   def _ParseAttachment(self, attachment_json, _event_log):
@@ -305,11 +309,11 @@
     self.services.issue.SetUsedLocalID(cnxn, project_id)
     event_log.append('Finished import')
 
-  # def GetIssueImport(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueImport(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostIssueImport(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostIssueImport(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class JSONImportError(Exception):
diff --git a/tracker/issueoriginal.py b/tracker/issueoriginal.py
index cbab3b1..f841168 100644
--- a/tracker/issueoriginal.py
+++ b/tracker/issueoriginal.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Servlet to show the original email that caused an issue comment.
 
@@ -13,15 +12,12 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
 import ezt
 
 from businesslogic import work_env
-from framework import flaskservlet
 from framework import filecontent
 from framework import permissions
 from framework import servlet
-from services import issue_svc
 
 
 class IssueOriginal(servlet.Servlet):
@@ -98,5 +94,5 @@
 
     return issue, comment
 
-  # def GetIssueOriginal(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueOriginal(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issuereindex.py b/tracker/issuereindex.py
index 71acfe8..30c80d3 100644
--- a/tracker/issuereindex.py
+++ b/tracker/issuereindex.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Classes that implement an admin utility to re-index issues in bulk."""
 from __future__ import print_function
@@ -12,7 +11,6 @@
 from six.moves import urllib
 
 import settings
-from framework import flaskservlet
 from framework import permissions
 from framework import servlet
 from framework import urls
@@ -23,7 +21,7 @@
   """IssueReindex shows a form to request that issues be indexed."""
 
   _PAGE_TEMPLATE = 'tracker/issue-reindex-page.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -79,16 +77,16 @@
     # and we have not run out of issues to process.
     auto_submit = issues and ('auto_submit' in post_data)
 
-    query_map = {
-      'start': start + num,  # auto-increment start.
-      'num': num,
-      'auto_submit': bool(auto_submit),
-    }
+    query_map = (
+        ('start', start + num),  # auto-increment start.
+        ('num', num),
+        ('auto_submit', bool(auto_submit)),
+    )
     return '/p/%s%s?%s' % (
         mr.project_name, urls.ISSUE_REINDEX, urllib.parse.urlencode(query_map))
 
-  # def GetIssueReindex(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueReindex(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostIssueReindex(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostIssueReindex(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/issuetips.py b/tracker/issuetips.py
index f85bcd6..2b337e8 100644
--- a/tracker/issuetips.py
+++ b/tracker/issuetips.py
@@ -1,25 +1,21 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A class to render a page of issue tracker search tips."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
-
-from framework import flaskservlet
-from framework import servlet
 from framework import permissions
+from framework import servlet
 
 
 class IssueSearchTips(servlet.Servlet):
   """IssueSearchTips on-line help on how to use issue search."""
 
   _PAGE_TEMPLATE = 'tracker/issue-search-tips.ezt'
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page."""
@@ -29,5 +25,5 @@
         'page_perms': self.MakePagePerms(mr, None, permissions.CREATE_ISSUE),
     }
 
-  # def GetIssueSearchTips(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetIssueSearchTips(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/rerank_helpers.py b/tracker/rerank_helpers.py
index f27582c..cc23521 100644
--- a/tracker/rerank_helpers.py
+++ b/tracker/rerank_helpers.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Functions to help rerank issues in a lit.
 
@@ -17,7 +16,7 @@
 
 from framework import exceptions
 
-MAX_RANKING = sys.maxint
+MAX_RANKING = sys.maxsize
 MIN_RANKING = 0
 
 def GetHotlistRerankChanges(hotlist_items, moved_issue_ids, target_position):
diff --git a/tracker/tablecell.py b/tracker/tablecell.py
index afb6468..36bc9b2 100644
--- a/tracker/tablecell.py
+++ b/tracker/tablecell.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Classes that generate value cells in the issue list table."""
 
@@ -221,7 +220,7 @@
     values = [_make_issue_view(issue.project_name, _kw["config"],
                                 _kw["viewable_iids_set"], ref_issue)
               for ref_issue in ref_issues]
-    values.sort(key=lambda x: (x.closed, x.id))
+    values.sort(key=lambda x: (bool(x.closed), x.id))
     table_view_helpers.TableCell.__init__(
         self, table_view_helpers.CELL_TYPE_ISSUES, values, sort_values=False)
 
@@ -235,7 +234,7 @@
     values = [_make_issue_view(issue.project_name, _kw["config"],
                                 _kw["viewable_iids_set"], ref_issue)
               for ref_issue in ref_issues]
-    values.sort(key=lambda x: (x.closed, x.id))
+    values.sort(key=lambda x: (bool(x.closed), x.id))
     table_view_helpers.TableCell.__init__(
         self, table_view_helpers.CELL_TYPE_ISSUES, values, sort_values=False)
 
diff --git a/tracker/template_helpers.py b/tracker/template_helpers.py
index 1f15bcc..afa015b 100644
--- a/tracker/template_helpers.py
+++ b/tracker/template_helpers.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Helper functions for issue template servlets"""
 from __future__ import print_function
@@ -19,7 +18,7 @@
 from tracker import tracker_bizobj
 from tracker import tracker_constants
 from tracker import tracker_helpers
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 MAX_NUM_PHASES = 6
 
@@ -47,9 +46,7 @@
   content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75)
   status = post_data.get('status', '')
   owner_str = post_data.get('owner', '')
-  # TODO(crbug.com/monorail/10936): switch when convert /p to flask
-  # labels = post_data.getlist('label')
-  labels = post_data.getall('label')
+  labels = post_data.getlist('label')
   field_val_strs = collections.defaultdict(list)
   for fd in config.field_defs:
     field_value_key = 'custom_%d' % fd.field_id
@@ -230,8 +227,8 @@
 
 
 def GetIssueFromTemplate(template, project_id, reporter_id):
-  # type: (proto.tracker_pb2.TemplateDef, int, int) ->
-  #     proto.tracker_pb2.Issue
+  # type: (mrproto.tracker_pb2.TemplateDef, int, int) ->
+  #     mrproto.tracker_pb2.Issue
   """Build a templated issue from TemplateDef.
 
   Args:
diff --git a/tracker/templatecreate.py b/tracker/templatecreate.py
index 139c4f5..2f8568b 100644
--- a/tracker/templatecreate.py
+++ b/tracker/templatecreate.py
@@ -1,41 +1,32 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A servlet for project owners to create a new template"""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import collections
-import logging
 import time
 
 import ezt
 
-from framework import authdata
-from framework import flaskservlet
-from framework import framework_bizobj
 from framework import framework_helpers
+from framework import permissions
 from framework import servlet
 from framework import urls
-from framework import permissions
 from tracker import field_helpers
 from tracker import template_helpers
-from tracker import tracker_bizobj
-from tracker import tracker_helpers
 from tracker import tracker_views
-from services import user_svc
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 
 class TemplateCreate(servlet.Servlet):
   """Servlet allowing project owners to create an issue template."""
 
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/template-detail-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -44,7 +35,7 @@
       mr: commonly used info parsed from the request
     """
     super(TemplateCreate, self).AssertBasePermission(mr)
-    if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+    if not permissions.CanEditProjectConfig(mr, self.services):
       raise permissions.PermissionException(
           'User is not allowed to administer this project')
 
@@ -99,7 +90,7 @@
         'fields':
             [
                 view for view in field_views
-                if view.field_def.type_name is not "APPROVAL_TYPE"
+                if view.field_def.type_name != "APPROVAL_TYPE"
             ],
         'initial_add_approvals':
             ezt.boolean(False),
@@ -108,7 +99,7 @@
         'approvals':
             [
                 view for view in field_views
-                if view.field_def.type_name is "APPROVAL_TYPE"
+                if view.field_def.type_name == "APPROVAL_TYPE"
             ],
         'prechecked_approvals': [],
         'required_approval_ids': [],
@@ -172,12 +163,12 @@
           initial_admins=parsed.admin_str,
           labels=parsed.labels,
           fields=[view for view in field_views
-                  if view.field_def.type_name is not 'APPROVAL_TYPE'],
+                  if view.field_def.type_name != 'APPROVAL_TYPE'],
           initial_add_approvals=ezt.boolean(parsed.add_approvals),
           initial_phases=[tracker_pb2.Phase(name=name) for name in
                           parsed.phase_names],
           approvals=[view for view in field_views
-                     if view.field_def.type_name is 'APPROVAL_TYPE'],
+                     if view.field_def.type_name == 'APPROVAL_TYPE'],
           prechecked_approvals=prechecked_approvals,
           required_approval_ids=parsed.required_approval_ids
       )
@@ -193,8 +184,8 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.ADMIN_TEMPLATES, saved=1, ts=int(time.time()))
 
-  # def GetTemplateCreate(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetTemplateCreate(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostTemplateCreate(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostTemplateCreate(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/templatedetail.py b/tracker/templatedetail.py
index a3386b4..4bda006 100644
--- a/tracker/templatedetail.py
+++ b/tracker/templatedetail.py
@@ -1,42 +1,34 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """A servlet for project owners to edit/delete a template"""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
-import collections
-import logging
 import time
 
 import ezt
 
-from framework import authdata
-from framework import flaskservlet
-from framework import framework_bizobj
 from framework import framework_helpers
 from framework import framework_views
+from framework import permissions
 from framework import servlet
 from framework import urls
-from framework import permissions
 from tracker import field_helpers
 from tracker import template_helpers
 from tracker import tracker_bizobj
-from tracker import tracker_helpers
 from tracker import tracker_views
-from proto import tracker_pb2
-from services import user_svc
+from mrproto import tracker_pb2
 
 
 class TemplateDetail(servlet.Servlet):
   """Servlet allowing project owners to edit/delete an issue template"""
 
-  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/template-detail-page.ezt'
-  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
+  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -79,7 +71,7 @@
       framework_views.RevealAllEmailsToMembers(
           mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
     field_name_set = {fd.field_name.lower() for fd in config.field_defs
-                      if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and
+                      if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE and
                       not fd.is_deleted}
     non_masked_labels = tracker_bizobj.NonMaskedLabels(
         template.labels, field_name_set)
@@ -135,7 +127,7 @@
         'fields':
             [
                 view for view in field_views
-                if view.field_def.type_name is not 'APPROVAL_TYPE'
+                if view.field_def.type_name != 'APPROVAL_TYPE'
             ],
         'initial_add_approvals':
             ezt.boolean(prechecked_approvals),
@@ -144,7 +136,7 @@
         'approvals':
             [
                 view for view in field_views
-                if view.field_def.type_name is 'APPROVAL_TYPE'
+                if view.field_def.type_name == 'APPROVAL_TYPE'
             ],
         'prechecked_approvals':
             prechecked_approvals,
@@ -220,12 +212,12 @@
           initial_admins=parsed.admin_str,
           labels=parsed.labels,
           fields=[view for view in field_views
-                  if view.field_def.type_name is not 'APPROVAL_TYPE'],
+                  if view.field_def.type_name != 'APPROVAL_TYPE'],
           initial_add_approvals=ezt.boolean(parsed.add_approvals),
           initial_phases=[tracker_pb2.Phase(name=name) for name in
                           parsed.phase_names],
           approvals=[view for view in field_views
-                     if view.field_def.type_name is 'APPROVAL_TYPE'],
+                     if view.field_def.type_name == 'APPROVAL_TYPE'],
           prechecked_approvals=prechecked_approvals,
           required_approval_ids=parsed.required_approval_ids
       )
@@ -245,8 +237,8 @@
         mr, urls.TEMPLATE_DETAIL, template=template.name,
         saved=1, ts=int(time.time()))
 
-  # def GetTemplateDetail(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetTemplateDetail(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def PostTemplateDetail(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def PostTemplateDetail(self, **kwargs):
+    return self.handler(**kwargs)
diff --git a/tracker/test/attachment_helpers_test.py b/tracker/test/attachment_helpers_test.py
index 18e0efc..cbc7458 100644
--- a/tracker/test/attachment_helpers_test.py
+++ b/tracker/test/attachment_helpers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittest for the tracker helpers module."""
 from __future__ import print_function
@@ -11,7 +10,7 @@
 from mock import Mock, patch
 import unittest
 
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import attachment_helpers
 
 
@@ -146,4 +145,3 @@
     # Anything that is not a video.
     attach.mimetype = 'audio/mp3'
     self.assertIsNone(attachment_helpers.GetVideoURL(attach, download_url))
-
diff --git a/tracker/test/component_helpers_test.py b/tracker/test/component_helpers_test.py
index ee7f56c..3603583 100644
--- a/tracker/test/component_helpers_test.py
+++ b/tracker/test/component_helpers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for the component_helpers module."""
 from __future__ import print_function
@@ -10,7 +9,7 @@
 
 import unittest
 
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from tracker import component_helpers
diff --git a/tracker/test/componentcreate_test.py b/tracker/test/componentcreate_test.py
index 1325d9b..2a14874 100644
--- a/tracker/test/componentcreate_test.py
+++ b/tracker/test/componentcreate_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for the componentcreate servlet."""
 from __future__ import print_function
@@ -17,8 +16,6 @@
 from tracker import componentcreate
 from tracker import tracker_bizobj
 
-import webapp2
-
 
 class ComponentCreateTest(unittest.TestCase):
 
@@ -27,8 +24,7 @@
         user=fake.UserService(),
         config=fake.ConfigService(),
         project=fake.ProjectService())
-    self.servlet = componentcreate.ComponentCreate(
-        'req', 'res', services=self.services)
+    self.servlet = componentcreate.ComponentCreate(services=self.services)
     self.project = self.services.project.TestAddProject('proj')
     self.mr = testing_helpers.MakeMonorailRequest(
         project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
@@ -87,8 +83,7 @@
         cc=[''],
         labels=[''])
     self.assertRaises(
-        webapp2.HTTPException,
-        self.servlet.ProcessFormData, self.mr, post_data)
+        Exception, self.servlet.ProcessFormData, self.mr, post_data)
 
   def testProcessFormData_Normal(self):
     post_data = fake.PostData(
diff --git a/tracker/test/componentdetail_test.py b/tracker/test/componentdetail_test.py
index 6186582..0dcff92 100644
--- a/tracker/test/componentdetail_test.py
+++ b/tracker/test/componentdetail_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for the componentdetail servlet."""
 from __future__ import print_function
@@ -10,7 +9,7 @@
 
 import unittest
 
-from mock import Mock, patch
+from mock import Mock
 
 try:
   from mox3 import mox
@@ -19,7 +18,7 @@
 
 from features import filterrules_helpers
 from framework import permissions
-from proto import project_pb2
+from mrproto import project_pb2
 from services import service_manager
 from services import template_svc
 from testing import fake
@@ -27,8 +26,6 @@
 from tracker import componentdetail
 from tracker import tracker_bizobj
 
-import webapp2
-
 
 class ComponentDetailTest(unittest.TestCase):
 
@@ -39,8 +36,7 @@
         config=fake.ConfigService(),
         template=Mock(spec=template_svc.TemplateService),
         project=fake.ProjectService())
-    self.servlet = componentdetail.ComponentDetail(
-        'req', 'res', services=self.services)
+    self.servlet = componentdetail.ComponentDetail(services=self.services)
     self.project = self.services.project.TestAddProject('proj')
     self.mr = testing_helpers.MakeMonorailRequest(
         project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
@@ -65,9 +61,7 @@
 
   def testGetComponentDef_NotFound(self):
     self.mr.component_path = 'NeverHeardOfIt'
-    self.assertRaises(
-        webapp2.HTTPException,
-        self.servlet._GetComponentDef, self.mr)
+    self.assertRaises(Exception, self.servlet._GetComponentDef, self.mr)
 
   def testGetComponentDef_Normal(self):
     actual_config, actual_cd = self.servlet._GetComponentDef(self.mr)
@@ -215,7 +209,7 @@
       self.servlet.ProcessFormData(self.mr, post_data)
     self.assertEqual(
         'User tried to delete component that had subcomponents',
-        cm.exception.message)
+        str(cm.exception))
 
   def testProcessFormData_Edit(self):
     post_data = fake.PostData(
diff --git a/tracker/test/field_helpers_test.py b/tracker/test/field_helpers_test.py
index f49a147..5d46586 100644
--- a/tracker/test/field_helpers_test.py
+++ b/tracker/test/field_helpers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for the field_helpers module."""
 from __future__ import print_function
@@ -15,8 +14,8 @@
 from framework import exceptions
 from framework import permissions
 from framework import template_helpers
-from proto import project_pb2
-from proto import tracker_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from services import config_svc
 from testing import fake
@@ -1274,3 +1273,43 @@
     self.assertEqual(
         self.mr.errors.min_value, 'Minimum value must be less than maximum.')
     self.assertEqual(self.mr.errors.regex, 'Invalid regular expression.')
+
+  def testValidateLabels_NoLabels(self):
+    err_msg = field_helpers.ValidateLabels(
+        self.mr.cnxn,
+        self.services,
+        self.mr.project_id, [''],
+        ezt_errors=self.errors)
+    self.assertFalse(self.errors.AnyErrors())
+    self.assertEqual(err_msg, None)
+
+  def testValidateLabels_ExistingLabel(self):
+    err_msg = field_helpers.ValidateLabels(
+        self.mr.cnxn,
+        self.services,
+        self.mr.project_id, ['old_label'],
+        ezt_errors=self.errors)
+    self.assertFalse(self.errors.AnyErrors())
+    self.assertEqual(err_msg, None)
+
+  def testValidateLabels_AllowlistedLabel(self):
+    err_msg = field_helpers.ValidateLabels(
+        self.mr.cnxn,
+        self.services,
+        self.mr.project_id, ['old_label', 'CVE-test'],
+        ezt_errors=self.errors)
+    self.assertFalse(self.errors.AnyErrors())
+    self.assertEqual(err_msg, None)
+
+  def testValidateLabels_Error(self):
+    err_msg = field_helpers.ValidateLabels(
+        self.mr.cnxn,
+        self.services,
+        self.mr.project_id, ['freeze_new_label'],
+        ezt_errors=self.errors)
+    self.assertTrue(self.errors.AnyErrors())
+    self.assertEqual(
+        err_msg, (
+            'The creation of new labels is blocked for the Chromium project'
+            ' in Monorail. To continue with editing your issue, please'
+            ' remove: freeze_new_label label(s).'))
diff --git a/tracker/test/fieldcreate_test.py b/tracker/test/fieldcreate_test.py
index 4a8c919..c1b9729 100644
--- a/tracker/test/fieldcreate_test.py
+++ b/tracker/test/fieldcreate_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for the fieldcreate servlet."""
 from __future__ import print_function
@@ -13,13 +12,14 @@
 except ImportError:
   import mox
 import mock
+import six
 import unittest
 import logging
 
 import ezt
 
 from framework import permissions
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
@@ -35,8 +35,7 @@
         user=fake.UserService(),
         config=fake.ConfigService(),
         project=fake.ProjectService())
-    self.servlet = fieldcreate.FieldCreate(
-        'req', 'res', services=self.services)
+    self.servlet = fieldcreate.FieldCreate(services=self.services)
     self.project = self.services.project.TestAddProject('proj')
     self.mr = testing_helpers.MakeMonorailRequest(
         project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
@@ -84,8 +83,8 @@
     page_data = self.servlet.GatherPageData(self.mr)
     self.assertEqual(self.servlet.PROCESS_TAB_LABELS,
                      page_data['admin_tab_mode'])
-    self.assertItemsEqual(
-        ['Defect', 'Enhancement', 'Task', 'Other'],
+    six.assertCountEqual(
+        self, ['Defect', 'Enhancement', 'Task', 'Other'],
         page_data['well_known_issue_types'])
     self.assertEqual(['LaunchApproval'], page_data['approval_names'])
     self.assertEqual('', page_data['initial_admins'])
diff --git a/tracker/test/fielddetail_test.py b/tracker/test/fielddetail_test.py
index 5b31fa3..ecd096b 100644
--- a/tracker/test/fielddetail_test.py
+++ b/tracker/test/fielddetail_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for the fielddetail servlet."""
 from __future__ import print_function
@@ -15,19 +14,14 @@
 import unittest
 import logging
 
-import webapp2
-
-import ezt
-
 from framework import permissions
-from proto import project_pb2
-from proto import tracker_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
 from tracker import fielddetail
 from tracker import tracker_bizobj
-from tracker import tracker_views
 
 
 class FieldDetailTest(unittest.TestCase):
@@ -37,8 +31,7 @@
         user=fake.UserService(),
         config=fake.ConfigService(),
         project=fake.ProjectService())
-    self.servlet = fielddetail.FieldDetail(
-        'req', 'res', services=self.services)
+    self.servlet = fielddetail.FieldDetail(services=self.services)
     self.project = self.services.project.TestAddProject('proj')
     self.mr = testing_helpers.MakeMonorailRequest(
         project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
@@ -80,9 +73,7 @@
 
   def testGetFieldDef_NotFound(self):
     self.mr.field_name = 'NeverHeardOfIt'
-    self.assertRaises(
-        webapp2.HTTPException,
-        self.servlet._GetFieldDef, self.mr)
+    self.assertRaises(Exception, self.servlet._GetFieldDef, self.mr)
 
   def testGetFieldDef_Normal(self):
     actual_config, actual_fd = self.servlet._GetFieldDef(self.mr)
diff --git a/tracker/test/fltconversion_test.py b/tracker/test/fltconversion_test.py
index be66ad2..2526fdc 100644
--- a/tracker/test/fltconversion_test.py
+++ b/tracker/test/fltconversion_test.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for the flt launch issues conversion task."""
 from __future__ import print_function
@@ -21,7 +20,7 @@
 from tracker import tracker_bizobj
 from testing import fake
 from testing import testing_helpers
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 class FLTConvertTask(unittest.TestCase):
 
@@ -411,13 +410,13 @@
   def testFetchAndAssertProjectInfo(self):
 
     # test no 'launch' in request
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, r'bad launch type:',
         self.task.FetchAndAssertProjectInfo, self.mr)
 
     # test bad 'launch' in request
     mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=bad')
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, r'bad launch type: bad',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -429,7 +428,7 @@
     # test no template
     self.task.services.template.GetTemplateByName = mock.Mock(
         return_value=None)
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, r'not found in chromium project',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -438,14 +437,14 @@
         'template', 'sum', 'New', 111, 'content', [], [], [], [])
     self.task.services.template.GetTemplateByName = mock.Mock(
         return_value=template)
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'no approvals or phases in',
         self.task.FetchAndAssertProjectInfo, mr)
 
     # test phases not recognized
     template.phases = [tracker_pb2.Phase(name='WeirdPhase')]
     template.approval_values = [tracker_pb2.ApprovalValue()]
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'one or more phases not recognized',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -457,7 +456,7 @@
         tracker_pb2.ApprovalValue(approval_id=3)]
 
     # test approvals not recognized
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'one or more approvals not recognized',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -471,7 +470,7 @@
     ]
 
     # test approvals not in config's approval_defs
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'one or more approvals not in config.approval_defs',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -481,7 +480,7 @@
         tracker_pb2.ApprovalDef(approval_id=3)]
 
     # test no pm field exists in project
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'project has no FieldDef %s' % fltconversion.PM_FIELD,
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -496,7 +495,7 @@
     ])
 
     # test no USER_TYPE te field exists in project
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'project has no FieldDef %s' % fltconversion.TE_FIELD,
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -510,7 +509,7 @@
         ])
 
     # test no M-Target INT_TYPE multivalued Phase FieldDefs
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError,
         'project has no FieldDef %s' % fltconversion.MTARGET_FIELD,
         self.task.FetchAndAssertProjectInfo, mr)
@@ -519,7 +518,7 @@
     self.config.field_defs[-2].is_multivalued = True
 
     # test no M-Approved INT_TYPE multivalued Phase FieldDefs
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError,
         'project has no FieldDef %s' % fltconversion.MAPPROVED_FIELD,
         self.task.FetchAndAssertProjectInfo, mr)
@@ -538,7 +537,7 @@
     # FINCH special case
     # test approvals for Finch not required
     mr = testing_helpers.MakeMonorailRequest(path='url/url?launch=finch')
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'finch template not set up correctly',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -568,7 +567,7 @@
     # test phases not recognized
     template.phases = [tracker_pb2.Phase(name='Chrome-Test')]
     template.approval_values = [tracker_pb2.ApprovalValue()]
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'one or more phases not recognized',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -580,7 +579,7 @@
         tracker_pb2.ApprovalValue(approval_id=1),
         tracker_pb2.ApprovalValue(approval_id=2),
         tracker_pb2.ApprovalValue(approval_id=3)]
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'os template not set up correctly',
         self.task.FetchAndAssertProjectInfo, mr)
 
@@ -588,7 +587,7 @@
       av.status = tracker_pb2.ApprovalStatus.NEEDS_REVIEW
 
     # test approvals not recognized
-    self.assertRaisesRegexp(
+    self.assertRaisesRegex(
         AssertionError, 'one or more approvals not recognized',
         self.task.FetchAndAssertProjectInfo, mr)
 
diff --git a/tracker/test/issueadmin_test.py b/tracker/test/issueadmin_test.py
index c36a495..c19a21d 100644
--- a/tracker/test/issueadmin_test.py
+++ b/tracker/test/issueadmin_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the issue admin pages."""
 from __future__ import print_function
@@ -12,13 +11,15 @@
   from mox3 import mox
 except ImportError:
   import mox
+import six
 import unittest
 
+import settings
 from mock import Mock, patch
 
 from framework import permissions
 from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from services import template_svc
 from testing import fake
@@ -39,7 +40,7 @@
         issue=fake.IssueService(),
         template=Mock(spec=template_svc.TemplateService),
         features=fake.FeaturesService())
-    self.servlet = servlet_factory('req', 'res', services=self.services)
+    self.servlet = servlet_factory(services=self.services)
     self.project = self.services.project.TestAddProject(
         'proj', project_id=789, contrib_ids=[333])
     self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
@@ -47,6 +48,8 @@
     self.cnxn = fake.MonorailConnection()
     self.mr = testing_helpers.MakeMonorailRequest(
         path='/p/proj/admin', project=self.project)
+    # Default to admin perms given that most tests assume the user can edit.
+    self.mr.perms = permissions.ADMIN_PERMISSIONSET
     self.mox = mox.Mox()
     self.test_template = tracker_bizobj.MakeIssueTemplate(
         'Test Template', 'sum', 'New', 111, 'content', [], [], [], [])
@@ -58,6 +61,8 @@
     self.services.template.GetTemplateSetForProject\
         .return_value = [(12345, 'Test template', 0)]
 
+    settings.config_freeze_project_ids = {}
+
   def tearDown(self):
     self.mox.UnsetStubs()
     self.mox.ResetAll()
@@ -80,9 +85,11 @@
     page_data = self.servlet.GatherPageData(self.mr)
     self.mox.VerifyAll()
 
-    self.assertItemsEqual(
-        ['admin_tab_mode', 'config', 'open_text', 'closed_text', 'labels_text'],
-        list(page_data.keys()))
+    six.assertCountEqual(
+        self, [
+            'admin_tab_mode', 'config', 'open_text', 'closed_text',
+            'labels_text', 'can_edit_project'
+        ], list(page_data.keys()))
     config_view = page_data['config']
     self.assertEqual(789, config_view.project_id)
 
@@ -91,6 +98,7 @@
 
   def setUp(self):
     super(AdminStatusesTest, self).setUpServlet(issueadmin.AdminStatuses)
+    self.servlet.mr = self.mr
 
   @patch('framework.servlet.Servlet.PleaseCorrect')
   def testProcessSubtabForm_MissingInput(self, mock_pc):
@@ -143,10 +151,11 @@
     page_data = self.servlet.GatherPageData(self.mr)
     self.mox.VerifyAll()
 
-    self.assertItemsEqual(
-        ['admin_tab_mode', 'config', 'field_defs',
-         'open_text', 'closed_text', 'labels_text'],
-        list(page_data.keys()))
+    six.assertCountEqual(
+        self, [
+            'admin_tab_mode', 'config', 'field_defs', 'open_text',
+            'closed_text', 'labels_text', 'can_edit_project'
+        ], list(page_data.keys()))
     config_view = page_data['config']
     self.assertEqual(789, config_view.project_id)
     self.assertEqual([], page_data['field_defs'])
@@ -300,11 +309,12 @@
     self.mox.ReplayAll()
     page_data = self.servlet.GatherPageData(self.mr)
     self.mox.VerifyAll()
-    self.assertItemsEqual(
-        ['admin_tab_mode', 'failed_templ', 'component_defs', 'failed_perm',
-         'config', 'failed_subcomp',
-         'open_text', 'closed_text', 'labels_text'],
-        list(page_data.keys()))
+    six.assertCountEqual(
+        self, [
+            'admin_tab_mode', 'failed_templ', 'component_defs', 'failed_perm',
+            'config', 'failed_subcomp', 'open_text', 'closed_text',
+            'labels_text', 'can_edit_project'
+        ], list(page_data.keys()))
     config_view = page_data['config']
     self.assertEqual(789, config_view.project_id)
     self.assertEqual([], page_data['component_defs'])
@@ -367,11 +377,12 @@
     page_data = self.servlet.GatherPageData(self.mr)
     self.mox.VerifyAll()
 
-    self.assertItemsEqual(
-        ['canned_queries', 'admin_tab_mode', 'config', 'issue_notify',
-         'new_query_indexes', 'max_queries',
-         'open_text', 'closed_text', 'labels_text'],
-        list(page_data.keys()))
+    six.assertCountEqual(
+        self, [
+            'canned_queries', 'admin_tab_mode', 'config', 'issue_notify',
+            'new_query_indexes', 'max_queries', 'open_text', 'closed_text',
+            'labels_text', 'can_edit_project'
+        ], list(page_data.keys()))
     config_view = page_data['config']
     self.assertEqual(789, config_view.project_id)
 
@@ -424,8 +435,9 @@
     self.assertEqual('label1-sub1', y_attr)
 
     # Test that multibyte strings are not mangled.
-    spec = ('\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9 '
-            '\xe5\x9c\xb0\xe3\x81\xa6-\xe5\xbd\x93-\xe3\x81\xbe\xe3\x81\x99')
+    spec = (
+        b'\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9 '
+        b'\xe5\x9c\xb0\xe3\x81\xa6-\xe5\xbd\x93-\xe3\x81\xbe\xe3\x81\x99')
     spec = spec.decode('utf-8')
     (col_spec, sort_spec, x_attr, y_attr, member_default_query,
      ) = issueadmin._ParseListPreferences(
@@ -437,10 +449,10 @@
         )
     self.assertEqual(spec, col_spec)
     self.assertEqual(' '.join(spec.split()), sort_spec)
-    self.assertEqual('\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9'.decode('utf-8'),
-                     x_attr)
-    self.assertEqual('\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9'.decode('utf-8'),
-                     y_attr)
+    self.assertEqual(
+        b'\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9'.decode('utf-8'), x_attr)
+    self.assertEqual(
+        b'\xe7\xaa\xbf\xe8\x8b\xa5-\xe7\xb9\xb9'.decode('utf-8'), y_attr)
     self.assertEqual(spec, member_default_query)
 
 
@@ -455,10 +467,12 @@
     page_data = self.servlet.GatherPageData(self.mr)
     self.mox.VerifyAll()
 
-    self.assertItemsEqual(
-        ['admin_tab_mode', 'config', 'rules', 'new_rule_indexes',
-         'max_rules', 'open_text', 'closed_text', 'labels_text'],
-        list(page_data.keys()))
+    six.assertCountEqual(
+        self, [
+            'admin_tab_mode', 'config', 'rules', 'new_rule_indexes',
+            'max_rules', 'open_text', 'closed_text', 'labels_text',
+            'can_edit_project'
+        ], list(page_data.keys()))
     config_view = page_data['config']
     self.assertEqual(789, config_view.project_id)
     self.assertEqual([], page_data['rules'])
diff --git a/tracker/test/issueadvsearch_test.py b/tracker/test/issueadvsearch_test.py
index fd1ee2e..a84e4e1 100644
--- a/tracker/test/issueadvsearch_test.py
+++ b/tracker/test/issueadvsearch_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for monorail.tracker.issueadvsearch."""
 from __future__ import print_function
@@ -25,8 +24,7 @@
         user=fake.UserService(),
         project=fake.ProjectService())
     self.project = self.services.project.TestAddProject('proj', project_id=987)
-    self.servlet = issueadvsearch.IssueAdvancedSearch(
-        'req', 'res', services=self.services)
+    self.servlet = issueadvsearch.IssueAdvancedSearch(services=self.services)
 
   def testGatherData(self):
     mr = testing_helpers.MakeMonorailRequest(
diff --git a/tracker/test/issueattachment_test.py b/tracker/test/issueattachment_test.py
index f782f22..0330e19 100644
--- a/tracker/test/issueattachment_test.py
+++ b/tracker/test/issueattachment_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for monorail.tracker.issueattachment."""
 from __future__ import print_function
@@ -16,11 +15,10 @@
   from mox3 import mox
 except ImportError:
   import mox
-import webapp2
 
-from framework import gcs_helpers
+from framework import exceptions, gcs_helpers
 from framework import permissions
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
@@ -45,8 +43,7 @@
         issue=fake.IssueService(),
         user=fake.UserService())
     self.project = services.project.TestAddProject('proj')
-    self.servlet = issueattachment.AttachmentPage(
-        'req', webapp2.Response(), services=services)
+    self.servlet = issueattachment.AttachmentPage(services=services)
     services.user.TestAddUser('commenter@example.com', 111)
     self.issue = fake.MakeTestIssue(
         self.project.project_id, 1, 'summary', 'New', 111)
@@ -79,7 +76,7 @@
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project, path=path,
         perms=permissions.EMPTY_PERMISSIONSET)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet.GatherPageData(mr)
     self.assertEqual(404, cm.exception.code)
 
@@ -125,18 +122,17 @@
         'app_default_bucket',
         '/pid/attachments/object_id-download'
         ).AndReturn('googleusercontent.com/...-download...')
-    self.mox.StubOutWithMock(self.servlet, 'redirect')
     path = '/p/proj/issues/attachment?aid=%s&signed_aid=signed_%d' % (
         aid, aid)
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project, path=path,
         perms=permissions.READ_ONLY_PERMISSIONSET)  # includes VIEW
-    self.servlet.redirect(
-      mox.And(mox.StrContains('googleusercontent.com'),
-              mox.StrContains('-download')), abort=True)
     self.mox.ReplayAll()
-    self.servlet.GatherPageData(mr)
+    with self.assertRaises(exceptions.RedirectException) as e:
+      self.servlet.GatherPageData(mr)
     self.mox.VerifyAll()
+    self.assertIn('googleusercontent.com', str(e.exception))
+    self.assertIn('-download', str(e.exception))
 
   def testGatherPageData_Download_WithoutDisposition(self):
     aid = self.attachment.attachment_id
@@ -152,16 +148,15 @@
         'app_default_bucket',
         '/pid/attachments/object_id'
         ).AndReturn('googleusercontent.com/...')
-    self.mox.StubOutWithMock(self.servlet, 'redirect')
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project, path=path,
         perms=permissions.READ_ONLY_PERMISSIONSET)  # includes VIEW
-    self.servlet.redirect(
-      mox.And(mox.StrContains('googleusercontent.com'),
-              mox.Not(mox.StrContains('-download'))), abort=True)
     self.mox.ReplayAll()
-    self.servlet.GatherPageData(mr)
+    with self.assertRaises(exceptions.RedirectException) as e:
+      self.servlet.GatherPageData(mr)
     self.mox.VerifyAll()
+    self.assertIn('googleusercontent.com', str(e.exception))
+    self.assertNotIn('-download', str(e.exception))
 
   def testGatherPageData_DownloadBadFilename(self):
     aid = self.attachment.attachment_id
@@ -179,14 +174,12 @@
         'app_default_bucket',
         '/pid/attachments/object_id-download'
         ).AndReturn('googleusercontent.com/...-download...')
-    self.mox.StubOutWithMock(self.servlet, 'redirect')
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project,
         path=path,
         perms=permissions.READ_ONLY_PERMISSIONSET)  # includes VIEW
-    self.servlet.redirect(mox.And(
-        mox.Not(mox.StrContains(self.attachment.filename)),
-        mox.StrContains('googleusercontent.com')), abort=True)
     self.mox.ReplayAll()
-    self.servlet.GatherPageData(mr)
+    with self.assertRaises(exceptions.RedirectException) as e:
+      self.servlet.GatherPageData(mr)
     self.mox.VerifyAll()
+    self.assertIn('googleusercontent.com', str(e.exception))
diff --git a/tracker/test/issueattachmenttext_test.py b/tracker/test/issueattachmenttext_test.py
index f7dda8d..f6a2447 100644
--- a/tracker/test/issueattachmenttext_test.py
+++ b/tracker/test/issueattachmenttext_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for issueattachmenttext."""
 from __future__ import print_function
@@ -15,10 +14,8 @@
 from google.appengine.ext import testbed
 from google.cloud import storage
 
-import webapp2
-
 from framework import permissions
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
@@ -38,8 +35,7 @@
         issue=fake.IssueService(),
         user=fake.UserService())
     self.project = services.project.TestAddProject('proj')
-    self.servlet = issueattachmenttext.AttachmentText(
-        'req', 'res', services=services)
+    self.servlet = issueattachmenttext.AttachmentText(services=services)
 
     services.user.TestAddUser('commenter@example.com', 111)
 
@@ -88,7 +84,7 @@
     self.blob = mock.MagicMock()
     self.client.get_bucket = mock.MagicMock(return_value=self.bucket)
     self.bucket.get_blob = mock.MagicMock(return_value=self.blob)
-    self.blob.download_as_bytes = mock.MagicMock()
+    self.blob.download_as_bytes = mock.MagicMock(return_value=b'')
     mock.patch.object(storage, 'Client', return_value=self.client).start()
 
   def tearDown(self):
@@ -142,7 +138,7 @@
         project=self.project,
         path='/p/proj/issues/attachmentText?aid=9999',
         perms=permissions.READ_ONLY_PERMISSIONSET)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet.GatherPageData(mr)
     self.assertEqual(404, cm.exception.code)
 
@@ -153,13 +149,13 @@
         path='/p/proj/issues/attachmentText?aid=1234',
         perms=permissions.READ_ONLY_PERMISSIONSET)
     self.attach1.deleted = True
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet.GatherPageData(mr)
-    self.assertEqual(404, cm.exception.code)
+      self.assertEqual(404, cm.exception.code)
 
   def testGatherPageData_Normal(self):
     self.blob.download_as_bytes = mock.MagicMock(
-        return_value='/app_default_bucket/pid/attachments/abcdefg')
+        return_value=b'/app_default_bucket/pid/attachments/abcdefg')
 
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project,
@@ -176,8 +172,8 @@
     file_lines = page_data['file_lines']
     self.assertEqual(1, len(file_lines))
     self.assertEqual(1, file_lines[0].num)
-    self.assertEqual('/app_default_bucket/pid/attachments/abcdefg',
-                     file_lines[0].line)
+    self.assertEqual(
+        '/app_default_bucket/pid/attachments/abcdefg', file_lines[0].line)
 
     self.assertEqual(None, page_data['code_reviews'])
 
diff --git a/tracker/test/issuebulkedit_test.py b/tracker/test/issuebulkedit_test.py
index 89d9bc3..c7bd1ca 100644
--- a/tracker/test/issuebulkedit_test.py
+++ b/tracker/test/issuebulkedit_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for monorail.tracker.issuebulkedit."""
 from __future__ import print_function
@@ -11,14 +10,15 @@
 import mock
 import os
 import unittest
-import webapp2
+import flask
 
 from google.appengine.api import memcache
 from google.appengine.ext import testbed
 
 from framework import exceptions
 from framework import permissions
-from proto import tracker_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from services import tracker_fulltext
 from testing import fake
@@ -45,8 +45,7 @@
         issue_star=fake.IssueStarService(),
         user=fake.UserService(),
         usergroup=fake.UserGroupService())
-    self.servlet = issuebulkedit.IssueBulkEdit(
-        'req', 'res', services=self.services)
+    self.servlet = issuebulkedit.IssueBulkEdit(services=self.services)
     self.mr = testing_helpers.MakeMonorailRequest(
         perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
     self.project = self.services.project.TestAddProject(
@@ -177,8 +176,7 @@
         project=self.project)
     mr.local_id_list = [local_id_1]
 
-    self.assertRaises(webapp2.HTTPException,
-                      self.servlet.GatherPageData, mr)
+    self.assertRaises(Exception, self.servlet.GatherPageData, mr)
 
   def testGatherPageData_TypeLabels(self):
     """Test that GPD displays a custom field for appropriate issues."""
@@ -226,6 +224,38 @@
     url = self.servlet.ProcessFormData(mr, post_data)
     self.assertTrue('list?can=1&q=&saved=1' in url)
 
+  def testProcessFormData_FreezeLabels(self):
+    """Test that PFD works in a normal no-corner-cases case."""
+    created_issue_1 = fake.MakeTestIssue(
+        789, 1, 'issue summary', 'New', 111, reporter_id=111)
+    self.services.issue.TestAddIssue(created_issue_1)
+    local_id_1 = created_issue_1.local_id
+
+    mr = testing_helpers.MakeMonorailRequest(
+        project=self.project,
+        perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+        user_info={'user_id': 111})
+    mr.local_id_list = [local_id_1]
+
+    post_data = fake.PostData(
+        owner=['owner@example.com'],
+        can=[1],
+        q=[''],
+        colspec=[''],
+        sort=[''],
+        groupby=[''],
+        start=[0],
+        num=[100],
+        label=['freeze_new_label'])
+    self._MockMethods()
+    self.servlet.response = flask.Response()
+    self.servlet.ProcessFormData(mr, post_data)
+    self.assertEqual(
+        (
+            "The creation of new labels is blocked for the Chromium project"
+            " in Monorail. To continue with editing your issue, please"
+            " remove: freeze_new_label label(s)."), mr.errors.labels)
+
   def testProcessFormData_NoIssues(self):
     """Test PFD when no issues are specified."""
     mr = testing_helpers.MakeMonorailRequest(
@@ -233,10 +263,10 @@
         perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
         user_info={'user_id': 111})
     post_data = fake.PostData()
-    self.servlet.response = Response()
+    self.servlet.response = flask.Response()
     self.servlet.ProcessFormData(mr, post_data)
     # 400 == bad request
-    self.assertEqual(400, self.servlet.response.status)
+    self.assertEqual(400, self.servlet.response.status_code)
 
   def testProcessFormData_NoUser(self):
     """Test PFD when the user is not logged in."""
@@ -244,10 +274,10 @@
         project=self.project)
     mr.local_id_list = [99999]
     post_data = fake.PostData()
-    self.servlet.response = Response()
+    self.servlet.response = flask.Response()
     self.servlet.ProcessFormData(mr, post_data)
     # 400 == bad request
-    self.assertEqual(400, self.servlet.response.status)
+    self.assertEqual(400, self.servlet.response.status_code)
 
   def testProcessFormData_CantComment(self):
     """Test PFD when the user can't comment on any of the issues."""
@@ -257,10 +287,10 @@
         user_info={'user_id': 111})
     mr.local_id_list = [99999]
     post_data = fake.PostData()
-    self.servlet.response = Response()
+    self.servlet.response = flask.Response()
     self.servlet.ProcessFormData(mr, post_data)
     # 400 == bad request
-    self.assertEqual(400, self.servlet.response.status)
+    self.assertEqual(400, self.servlet.response.status_code)
 
   def testProcessFormData_CantEdit(self):
     """Test PFD when the user can't edit any issue metadata."""
@@ -270,10 +300,10 @@
         user_info={'user_id': 111})
     mr.local_id_list = [99999]
     post_data = fake.PostData()
-    self.servlet.response = Response()
+    self.servlet.response = flask.Response()
     self.servlet.ProcessFormData(mr, post_data)
     # 400 == bad request
-    self.assertEqual(400, self.servlet.response.status)
+    self.assertEqual(400, self.servlet.response.status_code)
 
   def testProcessFormData_CantMove(self):
     """Test PFD when the user can't move issues."""
@@ -283,10 +313,10 @@
         user_info={'user_id': 111})
     mr.local_id_list = [99999]
     post_data = fake.PostData(move_to=['proj'])
-    self.servlet.response = Response()
+    self.servlet.response = flask.Response()
     self.servlet.ProcessFormData(mr, post_data)
     # 400 == bad request
-    self.assertEqual(400, self.servlet.response.status)
+    self.assertEqual(400, self.servlet.response.status_code)
 
     created_issue_1 = fake.MakeTestIssue(
         789, 1, 'issue summary', 'New', 111, reporter_id=111)
@@ -699,7 +729,7 @@
 
     # Verify CC lists and owner were merged to the merge_into issue.
     self.assertEqual(
-            [113, 120, 114, 115, 118, 111], merge_into_issue.cc_ids)
+            [113, 120, 111, 114, 115, 118], merge_into_issue.cc_ids)
     # Verify new starrers were added to the merge_into issue.
     self.assertEqual(4,
                       self.services.issue_star.CountItemStars(
@@ -801,7 +831,7 @@
     self.assertEqual('Invalid issue ID 54321', mr.errors.blocking)
 
   def testProcessFormData_BlockIssuesOnItself(self):
-    """Test PFD processes invalid blocked_on and blocking values."""
+    """Test PFD processes same issue blocked_on and blocking values."""
     created_issue_1 = fake.MakeTestIssue(
         789, 1, 'issue summary', 'New', 111, reporter_id=111)
     self.services.issue.TestAddIssue(created_issue_1)
@@ -828,6 +858,49 @@
     self.assertEqual('Cannot block an issue on itself.', mr.errors.blocked_on)
     self.assertEqual('Cannot block an issue on itself.', mr.errors.blocking)
 
+  def testProcessFormData_BlockIssuesOnArchivedProject(self):
+    """Test PFD processes blocked_on and blocking issues without permissions."""
+    created_issue_1 = fake.MakeTestIssue(
+        789, 1, 'issue summary', 'New', 111, reporter_id=111)
+    self.services.issue.TestAddIssue(created_issue_1)
+    local_id_1 = created_issue_1.local_id
+    # Add issue to archived project.
+    archived_proj = self.services.project.TestAddProject(
+        name='archived-proj', project_id=789987, owner_ids=[111])
+    archived_proj.state = project_pb2.ProjectState.ARCHIVED
+    archived_iid = 2
+    created_issue_2 = fake.MakeTestIssue(
+        789987, archived_iid, 'issue summary', 'New', 111, reporter_id=111)
+    self.services.issue.TestAddIssue(created_issue_2)
+    mr = testing_helpers.MakeMonorailRequest(
+        project=self.project,
+        perms=permissions.OWNER_ACTIVE_PERMISSIONSET,
+        user_info={'user_id': 111})
+    mr.project_name = 'proj'
+    mr.local_id_list = [local_id_1]
+
+    global_id = 'archived-proj:2'
+    self._MockMethods()
+    post_data = fake.PostData(
+        op_blockedonenter=['append'],
+        blocked_on=[global_id],
+        op_blockingenter=['append'],
+        blocking=[global_id],
+        can=[1],
+        q=[''],
+        colspec=[''],
+        sort=[''],
+        groupby=[''],
+        start=[0],
+        num=[100])
+    self.servlet.ProcessFormData(mr, post_data)
+
+    self.assertEqual(
+        'Target issue %s cannot be modified' % archived_iid,
+        mr.errors.blocked_on)
+    self.assertEqual(
+        'Target issue %s cannot be modified' % archived_iid, mr.errors.blocking)
+
   @mock.patch('framework.cloud_tasks_helpers.create_task')
   def testProcessFormData_NormalBlockIssues(self, _create_task_mock):
     """Test PFD processes blocked_on and blocking values."""
diff --git a/tracker/test/issuedetailezt_test.py b/tracker/test/issuedetailezt_test.py
index fe3d22f..fe51ddf 100644
--- a/tracker/test/issuedetailezt_test.py
+++ b/tracker/test/issuedetailezt_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for monorail.tracker.issuedetailezt."""
 from __future__ import print_function
@@ -19,7 +18,7 @@
 
 import settings
 from businesslogic import work_env
-from proto import features_pb2
+from mrproto import features_pb2
 from features import hotlist_views
 from features import send_notifications
 from framework import authdata
@@ -31,9 +30,9 @@
 from framework import profiler
 from framework import sorting
 from framework import template_helpers
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
 from services import service_manager
 from services import issue_svc
 from services import tracker_fulltext
@@ -144,12 +143,9 @@
         project=fake.ProjectService())
     self.project = self.services.project.TestAddProject(
       'proj', project_id=987, committer_ids=[111])
-    self.next_servlet = issuedetailezt.FlipperNext(
-        'req', 'res', services=self.services)
-    self.prev_servlet = issuedetailezt.FlipperPrev(
-        'req', 'res', services=self.services)
-    self.list_servlet = issuedetailezt.FlipperList(
-        'req', 'res', services=self.services)
+    self.next_servlet = issuedetailezt.FlipperNext(services=self.services)
+    self.prev_servlet = issuedetailezt.FlipperPrev(services=self.services)
+    self.list_servlet = issuedetailezt.FlipperList(services=self.services)
     mr = testing_helpers.MakeMonorailRequest(project=self.project)
     mr.local_id = 123
     mr.me_user_id = 111
@@ -177,7 +173,7 @@
     patchGetAdjacentIssue.return_value = self.fake_issue_2
     self.next_servlet.mr.GetIntParam = mock.Mock(return_value=None)
 
-    self.next_servlet.get(project_name='proj', viewed_username=None)
+    self.next_servlet.get()
     self.next_servlet.mr.GetIntParam.assert_called_once_with('hotlist_id')
     patchGetAdjacentIssue.assert_called_once()
     self.next_servlet.redirect.assert_called_once_with(
@@ -189,7 +185,7 @@
     self.next_servlet.mr.GetIntParam = mock.Mock(return_value=123)
     # TODO(jeffcarp): Mock hotlist_id param on path here.
 
-    self.next_servlet.get(project_name='proj', viewed_username=None)
+    self.next_servlet.get()
     self.next_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
     self.next_servlet.redirect.assert_called_once_with(
       '/p/potato/issues/detail?id=789')
@@ -199,7 +195,7 @@
     patchGetAdjacentIssue.return_value = self.fake_issue_2
     self.next_servlet.mr.GetIntParam = mock.Mock(return_value=None)
 
-    self.prev_servlet.get(project_name='proj', viewed_username=None)
+    self.prev_servlet.get()
     self.prev_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
     patchGetAdjacentIssue.assert_called_once()
     self.prev_servlet.redirect.assert_called_once_with(
@@ -211,7 +207,7 @@
     self.prev_servlet.mr.GetIntParam = mock.Mock(return_value=123)
     # TODO(jeffcarp): Mock hotlist_id param on path here.
 
-    self.prev_servlet.get(project_name='proj', viewed_username=None)
+    self.prev_servlet.get()
     self.prev_servlet.mr.GetIntParam.assert_called_with('hotlist_id')
     self.prev_servlet.redirect.assert_called_once_with(
       '/p/potato/issues/detail?id=789')
diff --git a/tracker/test/issueentry_test.py b/tracker/test/issueentry_test.py
index b7461ae..b18f70b 100644
--- a/tracker/test/issueentry_test.py
+++ b/tracker/test/issueentry_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for the issueentry servlet."""
 from __future__ import print_function
@@ -19,7 +18,6 @@
 
 from google.appengine.ext import testbed
 from mock import Mock, patch
-import webapp2
 
 from framework import framework_bizobj
 from framework import framework_views
@@ -30,8 +28,8 @@
 from testing import testing_helpers
 from tracker import issueentry
 from tracker import tracker_bizobj
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
 
 
 class IssueEntryTest(unittest.TestCase):
@@ -51,10 +49,7 @@
         template=Mock(spec=template_svc.TemplateService),
         features=fake.FeaturesService())
     self.project = self.services.project.TestAddProject('proj', project_id=987)
-    request = webapp2.Request.blank('/p/proj/issues/entry')
-    response = webapp2.Response()
-    self.servlet = issueentry.IssueEntry(
-        request, response, services=self.services)
+    self.servlet = issueentry.IssueEntry(services=self.services)
     self.user = self.services.user.TestAddUser('to_pass_tests', 0)
     self.services.features.TestAddHotlist(
         name='dontcare', summary='', owner_ids=[0])
@@ -752,6 +747,46 @@
     self.assertEqual(field_values[0].int_value, 3)
     self.assertEqual(field_values[1].int_value, 3737)
 
+  def testProcessFormData_RejectNewLabels(self):
+    """We raise an AssertionError when new labels are added."""
+    mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/entry')
+    mr.perms = permissions.USER_PERMISSIONSET
+    mr.auth.user_view = framework_views.StuffUserView(100, 'user@invalid', True)
+    post_data = fake.PostData(
+        template_name=['rutabaga'],
+        summary=['Nya nya I modified the summary'],
+        comment=[self.template.content],
+        status=['New'],
+        label=['freeze_new_label'])
+
+    self.mox.StubOutWithMock(self.servlet, 'PleaseCorrect')
+    self.servlet.PleaseCorrect(
+        mr,
+        component_required=None,
+        fields=[],
+        initial_blocked_on='',
+        initial_blocking='',
+        initial_cc='',
+        initial_comment=self.template.content,
+        initial_components='',
+        initial_owner='',
+        initial_status='New',
+        initial_summary='Nya nya I modified the summary',
+        initial_hotlists='',
+        labels=['freeze_new_label'],
+        template_name='rutabaga')
+    self.mox.ReplayAll()
+    url = self.servlet.ProcessFormData(mr, post_data)
+    self.mox.VerifyAll()
+    self.assertEqual(
+        (
+            "The creation of new labels is blocked for the Chromium project"
+            " in Monorail. To continue with editing your issue, please"
+            " remove: freeze_new_label label(s)."),
+        mr.errors.labels,
+    )
+    self.assertIsNone(url)
+
   def testProcessFormData_RejectRestrictedFields(self):
     """We raise an AssertionError when restricted fields are set w/o perms."""
     mr = testing_helpers.MakeMonorailRequest(
diff --git a/tracker/test/issueexport_test.py b/tracker/test/issueexport_test.py
index 4e70ab7..7d51c84 100644
--- a/tracker/test/issueexport_test.py
+++ b/tracker/test/issueexport_test.py
@@ -1,19 +1,19 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for the issueexport servlet."""
 from __future__ import print_function
 from __future__ import division
 from __future__ import absolute_import
 
+import six
 import unittest
 
 from mock import Mock, patch
 
 from framework import permissions
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import testing_helpers
 from testing import fake
@@ -32,10 +32,8 @@
     )
     self.cnxn = 'fake connection'
     self.project = self.services.project.TestAddProject('proj', project_id=789)
-    self.servlet = issueexport.IssueExport(
-        'req', 'res', services=self.services)
-    self.jsonfeed = issueexport.IssueExportJSON(
-        'req', 'res', services=self.services)
+    self.servlet = issueexport.IssueExport(services=self.services)
+    self.jsonfeed = issueexport.IssueExportJSON(services=self.services)
     self.mr = testing_helpers.MakeMonorailRequest(
         project=self.project, perms=permissions.OWNER_ACTIVE_PERMISSIONSET)
     self.mr.can = 1
@@ -62,8 +60,8 @@
                      {'version': 1, 'who': None, 'when': 1234,
                       'project': 'proj', 'start': 0, 'num': 100})
     self.assertEqual(json_data['issues'], [])
-    self.assertItemsEqual(
-        json_data['emails'], ['user1@test.com', 'user2@test.com'])
+    six.assertCountEqual(
+        self, json_data['emails'], ['user1@test.com', 'user2@test.com'])
 
   # TODO(jojwang): test attachments, amendments, comment details
   def testMakeIssueJSON(self):
diff --git a/tracker/test/issueimport_test.py b/tracker/test/issueimport_test.py
index c0e38af..05c7526 100644
--- a/tracker/test/issueimport_test.py
+++ b/tracker/test/issueimport_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for the issueimport servlet."""
 from __future__ import print_function
@@ -14,15 +13,14 @@
 from services import service_manager
 from testing import testing_helpers
 from tracker import issueimport
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 
 class IssueExportTest(unittest.TestCase):
 
   def setUp(self):
     self.services = service_manager.Services()
-    self.servlet = issueimport.IssueImport(
-        'req', 'res', services=self.services)
+    self.servlet = issueimport.IssueImport(services=self.services)
     self.event_log = None
 
   def testAssertBasePermission(self):
@@ -37,6 +35,12 @@
   def testParseComment(self):
     """Test a Comment JSON is correctly parsed."""
     users_id_dict = {'adam@test.com': 111}
+    config = {
+        'component_defs': [{
+            'path': 'comp1',
+            'component_id': 1,
+        }],
+    }
     json = {
         'timestamp': 123,
         'commenter': 'adam@test.com',
@@ -46,7 +50,12 @@
         'description_num': None,
         }
     comment = self.servlet._ParseComment(
-        12, users_id_dict, json, self.event_log)
+        12,
+        users_id_dict,
+        json,
+        self.event_log,
+        config,
+    )
     self.assertEqual(
         comment, tracker_pb2.IssueComment(
             project_id=12, timestamp=123, user_id=111,
@@ -61,7 +70,7 @@
         'attachments': [],
     }
     desc_comment = self.servlet._ParseComment(
-        12, users_id_dict, json_desc, self.event_log)
+        12, users_id_dict, json_desc, self.event_log, config)
     self.assertEqual(
         desc_comment, tracker_pb2.IssueComment(
             project_id=12, timestamp=223, user_id=111,
diff --git a/tracker/test/issueoriginal_test.py b/tracker/test/issueoriginal_test.py
index 1b2b7d6..ff4e44e 100644
--- a/tracker/test/issueoriginal_test.py
+++ b/tracker/test/issueoriginal_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for the issueoriginal module."""
 from __future__ import print_function
@@ -11,13 +10,9 @@
 import mock
 import unittest
 
-import webapp2
-
 from framework import exceptions
-from framework import framework_helpers
-from framework import monorailrequest
 from framework import permissions
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
@@ -45,8 +40,7 @@
         config=fake.ConfigService(),
         issue=fake.IssueService(),
         user=fake.UserService())
-    self.servlet = issueoriginal.IssueOriginal(
-        'req', 'res', services=self.services)
+    self.servlet = issueoriginal.IssueOriginal(services=self.services)
 
     self.proj = self.services.project.TestAddProject('proj', project_id=789)
     summary = 'System wont boot'
@@ -171,14 +165,14 @@
     _request, mr = testing_helpers.GetRequestObjects(
         path='/p/proj/issues/original',
         project=self.proj)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet.GatherPageData(mr)
     self.assertEqual(404, cm.exception.code)
 
     _request, mr = testing_helpers.GetRequestObjects(
         path='/p/proj/issues/original?id=1&seq=999',
         project=self.proj)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet.GatherPageData(mr)
     self.assertEqual(404, cm.exception.code)
 
@@ -200,7 +194,7 @@
     _request, mr = testing_helpers.GetRequestObjects(
         path='/p/proj/issues/original?id=1&seq=99',
         project=self.proj)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet._GetIssueAndComment(mr)
     self.assertEqual(404, cm.exception.code)
 
@@ -208,14 +202,14 @@
     _request, mr = testing_helpers.GetRequestObjects(
         path='/p/proj/issues/original',
         project=self.proj)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet._GetIssueAndComment(mr)
     self.assertEqual(404, cm.exception.code)
 
     _request, mr = testing_helpers.GetRequestObjects(
         path='/p/proj/issues/original?id=1',
         project=self.proj)
-    with self.assertRaises(webapp2.HTTPException) as cm:
+    with self.assertRaises(Exception) as cm:
       self.servlet._GetIssueAndComment(mr)
     self.assertEqual(404, cm.exception.code)
 
diff --git a/tracker/test/issuereindex_test.py b/tracker/test/issuereindex_test.py
index 715da9a..9929076 100644
--- a/tracker/test/issuereindex_test.py
+++ b/tracker/test/issuereindex_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for monorail.tracker.issuereindex."""
 from __future__ import print_function
@@ -17,7 +16,6 @@
 
 import settings
 from framework import permissions
-from framework import template_helpers
 from services import service_manager
 from services import tracker_fulltext
 from testing import fake
@@ -45,27 +43,25 @@
     # Non-members and contributors do not have permission to view this page.
     for permission in (permissions.USER_PERMISSIONSET,
                        permissions.COMMITTER_ACTIVE_PERMISSIONSET):
-      request, mr = testing_helpers.GetRequestObjects(
+      _, mr = testing_helpers.GetRequestObjects(
           project=self.project, perms=permission)
-      servlet = issuereindex.IssueReindex(
-          request, 'res', services=self.services)
+      servlet = issuereindex.IssueReindex(services=self.services)
     with self.assertRaises(permissions.PermissionException) as cm:
       servlet.AssertBasePermission(mr)
-    self.assertEqual('You are not allowed to administer this project',
-                     cm.exception.message)
+    self.assertEqual(
+        'You are not allowed to administer this project', str(cm.exception))
 
   def testAssertBasePermission_WithAccess(self):
     # Owners and admins have permission to view this page.
     for permission in (permissions.OWNER_ACTIVE_PERMISSIONSET,
                        permissions.ADMIN_PERMISSIONSET):
-      request, mr = testing_helpers.GetRequestObjects(
+      _, mr = testing_helpers.GetRequestObjects(
           project=self.project, perms=permission)
-      servlet = issuereindex.IssueReindex(
-          request, 'res', services=self.services)
+      servlet = issuereindex.IssueReindex(services=self.services)
       servlet.AssertBasePermission(mr)
 
   def testGatherPageData(self):
-    servlet = issuereindex.IssueReindex('req', 'res', services=self.services)
+    servlet = issuereindex.IssueReindex(services=self.services)
 
     mr = testing_helpers.MakeMonorailRequest()
     mr.auto_submit = True
@@ -76,7 +72,7 @@
     self.assertTrue(ret['page_perms'].CreateIssue)
 
   def _callProcessFormData(self, post_data, index_issue_1=True):
-    servlet = issuereindex.IssueReindex('req', 'res', services=self.services)
+    servlet = issuereindex.IssueReindex(services=self.services)
 
     mr = testing_helpers.MakeMonorailRequest(project=self.project)
     mr.cnxn = self.cnxn
@@ -103,13 +99,13 @@
     post_data = {'start': 1, 'num': 5}
     ret = self._callProcessFormData(post_data)
     self.assertEqual(
-        '/p/None/issues/reindex?start=6&auto_submit=False&num=5', ret)
+        '/p/None/issues/reindex?start=6&num=5&auto_submit=False', ret)
 
   def testProcessFormData_LargeInputs(self):
     post_data = {'start': 0, 'num': 10000000}
     ret = self._callProcessFormData(post_data)
     self.assertEqual(
-        '/p/None/issues/reindex?start=%s&auto_submit=False&num=%s' % (
+        '/p/None/issues/reindex?start=%s&num=%s&auto_submit=False' % (
             settings.max_artifact_search_results_per_page,
             settings.max_artifact_search_results_per_page), ret)
 
@@ -117,11 +113,11 @@
     post_data = {'start': 1, 'num': 5, 'auto_submit': 1}
     ret = self._callProcessFormData(post_data)
     self.assertEqual(
-        '/p/None/issues/reindex?start=6&auto_submit=True&num=5', ret)
+        '/p/None/issues/reindex?start=6&num=5&auto_submit=True', ret)
 
   def testProcessFormData_WithAutoSubmitButNoMoreIssues(self):
     """This project has no issues 6-10, so stop autosubmitting."""
     post_data = {'start': 6, 'num': 5, 'auto_submit': 1}
     ret = self._callProcessFormData(post_data, index_issue_1=False)
     self.assertEqual(
-        '/p/None/issues/reindex?start=11&auto_submit=False&num=5', ret)
+        '/p/None/issues/reindex?start=11&num=5&auto_submit=False', ret)
diff --git a/tracker/test/issuetips_test.py b/tracker/test/issuetips_test.py
index 44f5f70..9734b92 100644
--- a/tracker/test/issuetips_test.py
+++ b/tracker/test/issuetips_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for issuetips module."""
 from __future__ import print_function
@@ -24,8 +23,7 @@
         issue=fake.IssueService(),
         user=fake.UserService(),
         project=fake.ProjectService())
-    self.servlet = issuetips.IssueSearchTips(
-        'req', 'res', services=self.services)
+    self.servlet = issuetips.IssueSearchTips(services=self.services)
 
   def testGatherPageData(self):
     mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/tips')
diff --git a/tracker/test/rerank_helpers_test.py b/tracker/test/rerank_helpers_test.py
index 47ddd47..d550f53 100644
--- a/tracker/test/rerank_helpers_test.py
+++ b/tracker/test/rerank_helpers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittests for monorail.tracker.rerank_helpers."""
 from __future__ import print_function
diff --git a/tracker/test/tablecell_test.py b/tracker/test/tablecell_test.py
index c8b7292..e50efd2 100644
--- a/tracker/test/tablecell_test.py
+++ b/tracker/test/tablecell_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for issuelist module."""
 from __future__ import print_function
@@ -14,7 +13,7 @@
 from framework import framework_constants
 from framework import table_view_helpers
 from framework import template_helpers
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from testing import fake
 from testing import testing_helpers
 from tracker import tablecell
diff --git a/tracker/test/template_helpers_test.py b/tracker/test/template_helpers_test.py
index 6c4a034..982e8bd 100644
--- a/tracker/test/template_helpers_test.py
+++ b/tracker/test/template_helpers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittest for the template helpers module."""
 from __future__ import print_function
@@ -9,6 +8,7 @@
 from __future__ import absolute_import
 
 import logging
+import six
 import unittest
 
 import settings
@@ -18,7 +18,7 @@
 from testing import testing_helpers
 from tracker import template_helpers
 from tracker import tracker_bizobj
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 
 class TemplateHelpers(unittest.TestCase):
@@ -87,7 +87,7 @@
     self.assertFalse(parsed.component_required)
     self.assertFalse(parsed.owner_defaults_to_member)
     self.assertFalse(parsed.add_approvals)
-    self.assertItemsEqual(parsed.phase_names, ['', '', '', '', '', ''])
+    six.assertCountEqual(self, parsed.phase_names, ['', '', '', '', '', ''])
     self.assertEqual(parsed.approvals_to_phase_idx, {})
     self.assertEqual(parsed.required_approval_ids, [])
 
@@ -139,10 +139,11 @@
     self.assertFalse(parsed.owner_defaults_to_member)
     self.assertTrue(parsed.add_approvals)
     self.assertEqual(parsed.admin_str, 'jojwang@test.com, annajo@test.com')
-    self.assertItemsEqual(parsed.phase_names,
-                          ['Canary', 'Stable-Exp', 'Stable', '', '', 'Oops'])
+    six.assertCountEqual(
+        self, parsed.phase_names,
+        ['Canary', 'Stable-Exp', 'Stable', '', '', 'Oops'])
     self.assertEqual(parsed.approvals_to_phase_idx, {3: 2, 4: None})
-    self.assertItemsEqual(parsed.required_approval_ids, [3, 4])
+    six.assertCountEqual(self, parsed.required_approval_ids, [3, 4])
 
   def testGetTemplateInfoFromParsed_Normal(self):
     self.config.field_defs.extend([self.fd_1, self.fd_2])
@@ -269,8 +270,7 @@
     (prechecked_approvals, required_approval_ids,
      phases) = template_helpers.GatherApprovalsPageData(
          approval_values, tmpl_phases, self.config)
-    self.assertItemsEqual(prechecked_approvals,
-                          ['4_phase_0', '5'])
+    six.assertCountEqual(self, prechecked_approvals, ['4_phase_0', '5'])
     self.assertEqual(required_approval_ids, [4])
     self.assertEqual(phases[0], tmpl_phases[1])
     self.assertIsNone(phases[1].name)
@@ -280,8 +280,7 @@
     approvals_to_phase_idx = {23: 0, 25: 1, 26: None}
     checked = template_helpers.GetCheckedApprovalsFromParsed(
         approvals_to_phase_idx)
-    self.assertItemsEqual(checked,
-                          ['23_phase_0', '25_phase_1', '26'])
+    six.assertCountEqual(self, checked, ['23_phase_0', '25_phase_1', '26'])
 
   def testGetIssueFromTemplate(self):
     """Can fill and return the templated issue"""
diff --git a/tracker/test/templatecreate_test.py b/tracker/test/templatecreate_test.py
index 78664c0..442bf8b 100644
--- a/tracker/test/templatecreate_test.py
+++ b/tracker/test/templatecreate_test.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit test for Template creation servlet."""
 from __future__ import print_function
@@ -27,7 +26,7 @@
 from tracker import templatecreate
 from tracker import tracker_bizobj
 from tracker import tracker_views
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 
 class TemplateCreateTest(unittest.TestCase):
@@ -40,8 +39,7 @@
         config=fake.ConfigService(),
         template=Mock(spec=template_svc.TemplateService),
         user=fake.UserService())
-    self.servlet = templatecreate.TemplateCreate('req', 'res',
-        services=self.services)
+    self.servlet = templatecreate.TemplateCreate(services=self.services)
     self.project = self.services.project.TestAddProject('proj')
 
     self.fd_1 = tracker_bizobj.MakeFieldDef(
@@ -372,6 +370,16 @@
                 tracker_pb2.ApprovalStatus.NEEDS_REVIEW), phase_id=1)
         ]
     self.services.template.CreateIssueTemplateDef.assert_called_once_with(
-        self.mr.cnxn, 47925, 'secondtemplate', 'HEY WHY', 'TLDR', True,
-        'Accepted', True, False, True, 0, ['label-One', 'label-Two'], [], [],
-        [fv], phases=phases, approval_values=approval_values)
+        self.mr.cnxn,
+        self.mr.project_id,
+        'secondtemplate',
+        'HEY WHY',
+        'TLDR',
+        True,
+        'Accepted',
+        True,
+        False,
+        True,
+        0, ['label-One', 'label-Two'], [], [], [fv],
+        phases=phases,
+        approval_values=approval_values)
diff --git a/tracker/test/templatedetail_test.py b/tracker/test/templatedetail_test.py
index 42fc46b..3968554 100644
--- a/tracker/test/templatedetail_test.py
+++ b/tracker/test/templatedetail_test.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for Template editing/viewing servlet."""
 from __future__ import print_function
@@ -13,6 +12,7 @@
 except ImportError:
   import mox
 import logging
+import six
 import unittest
 import settings
 
@@ -27,7 +27,7 @@
 from testing import testing_helpers
 from tracker import templatedetail
 from tracker import tracker_bizobj
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 
 
 class TemplateDetailTest(unittest.TestCase):
@@ -41,8 +41,7 @@
                                              template=mock_template_service,
                                              usergroup=fake.UserGroupService(),
                                              user=fake.UserService())
-    self.servlet = templatedetail.TemplateDetail('req', 'res',
-                                               services=self.services)
+    self.servlet = templatedetail.TemplateDetail(services=self.services)
 
     self.services.user.TestAddUser('gatsby@example.com', 111)
     self.services.user.TestAddUser('sport@example.com', 222)
@@ -271,8 +270,8 @@
     self.assertTrue(page_data['initial_owner_defaults_to_member'])
     self.assertEqual(page_data['initial_components'], 'BackEnd')
     self.assertFalse(page_data['initial_component_required'])
-    self.assertItemsEqual(
-        page_data['labels'],
+    six.assertCountEqual(
+        self, page_data['labels'],
         ['label1', 'label2', 'GateTarget-Should-Not', 'GateTarget-Be-Masked'])
     self.assertEqual(page_data['initial_admins'], 'sport@example.com')
     self.assertTrue(page_data['initial_add_approvals'])
@@ -280,8 +279,8 @@
     phases = [phase for phase in page_data['initial_phases'] if phase.name]
     self.assertEqual(len(phases), 2)
     self.assertEqual(len(page_data['approvals']), 2)
-    self.assertItemsEqual(page_data['prechecked_approvals'],
-                          ['3_phase_0', '4_phase_1'])
+    six.assertCountEqual(
+        self, page_data['prechecked_approvals'], ['3_phase_0', '4_phase_1'])
     self.assertTrue(page_data['fields'][3].is_editable)  #nonRestrictedField
     self.assertIsNone(page_data['fields'][4].is_editable)  #restrictedField
 
@@ -439,7 +438,7 @@
 
     self.services.template.UpdateIssueTemplateDef.assert_called_once_with(
         self.mr.cnxn,
-        47925,
+        self.mr.project_id,
         12345,
         status='Accepted',
         component_required=True,
@@ -496,19 +495,32 @@
     self.assertTrue('/templates/detail?saved=1&template=TestTemplate&' in url)
 
     self.services.template.UpdateIssueTemplateDef.assert_called_once_with(
-        self.mr.cnxn, 47925, 12345, status='Accepted', component_required=True,
+        self.mr.cnxn,
+        self.mr.project_id,
+        12345,
+        status='Accepted',
+        component_required=True,
         phases=[
             tracker_pb2.Phase(name='Canary', rank=0, phase_id=0),
-            tracker_pb2.Phase(name='Stable', rank=1, phase_id=1)],
-        approval_values=[tracker_pb2.ApprovalValue(approval_id=3, phase_id=0),
-                         tracker_pb2.ApprovalValue(approval_id=4, phase_id=1)],
-        name='TestTemplate', field_values=[
+            tracker_pb2.Phase(name='Stable', rank=1, phase_id=1)
+        ],
+        approval_values=[
+            tracker_pb2.ApprovalValue(approval_id=3, phase_id=0),
+            tracker_pb2.ApprovalValue(approval_id=4, phase_id=1)
+        ],
+        name='TestTemplate',
+        field_values=[
             tracker_pb2.FieldValue(field_id=1, str_value='NO', derived=False),
-            tracker_pb2.FieldValue(
-                field_id=2, str_value='MOOD', derived=False)],
-        labels=['label-One', 'label-Two'], owner_defaults_to_member=True,
-        admin_ids=[], content='HEY WHY', component_ids=[1],
-        summary_must_be_edited=False, summary='TLDR', members_only=True,
+            tracker_pb2.FieldValue(field_id=2, str_value='MOOD', derived=False)
+        ],
+        labels=['label-One', 'label-Two'],
+        owner_defaults_to_member=True,
+        admin_ids=[],
+        content='HEY WHY',
+        component_ids=[1],
+        summary_must_be_edited=False,
+        summary='TLDR',
+        members_only=True,
         owner_id=333)
 
   def testProcessFormData_Delete(self):
@@ -521,4 +533,4 @@
 
     self.assertTrue('/p/None/adminTemplates?deleted=1' in url)
     self.services.template.DeleteIssueTemplateDef\
-        .assert_called_once_with(self.mr.cnxn, 47925, 12345)
+        .assert_called_once_with(self.mr.cnxn, self.mr.project_id, 12345)
diff --git a/tracker/test/tracker_bizobj_test.py b/tracker/test/tracker_bizobj_test.py
index 29351b0..603b7c5 100644
--- a/tracker/test/tracker_bizobj_test.py
+++ b/tracker/test/tracker_bizobj_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Tests for issue  bizobj functions."""
 from __future__ import print_function
@@ -10,10 +9,11 @@
 
 import unittest
 import logging
+import six
 
 from framework import framework_constants
 from framework import framework_views
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
@@ -77,8 +77,8 @@
     av_2 = tracker_pb2.ApprovalValue()
     av_3 = tracker_pb2.ApprovalValue(approver_ids=[222, 333])
     issue.approval_values = [av_1, av_2, av_3]
-    self.assertItemsEqual(
-        tracker_bizobj.GetApproverIds(issue), [111, 222, 333])
+    six.assertCountEqual(
+        self, tracker_bizobj.GetApproverIds(issue), [111, 222, 333])
 
   def testGetLabels(self):
     issue = tracker_pb2.Issue()
@@ -179,9 +179,9 @@
     config.field_defs = [subfd_1, subfd_2, subfd_3, subfd_4]
 
     subfields_dict = tracker_bizobj.FindApprovalsSubfields([1, 2], config)
-    self.assertItemsEqual(subfields_dict[1], [subfd_1, subfd_3])
-    self.assertItemsEqual(subfields_dict[2], [subfd_2])
-    self.assertItemsEqual(subfields_dict[3], [])
+    six.assertCountEqual(self, subfields_dict[1], [subfd_1, subfd_3])
+    six.assertCountEqual(self, subfields_dict[2], [subfd_2])
+    six.assertCountEqual(self, subfields_dict[3], [])
 
   def testFindPhaseByID_Normal(self):
     canary_phase = tracker_pb2.Phase(phase_id=2, name='Canary')
@@ -758,7 +758,7 @@
 
   def CheckDefaultConfig(self, config):
     self.assertTrue(len(config.well_known_statuses) > 0)
-    self.assertTrue(config.statuses_offer_merge > 0)
+    self.assertTrue(len(config.statuses_offer_merge) > 0)
     self.assertTrue(len(config.well_known_labels) > 0)
     self.assertTrue(len(config.exclusive_label_prefixes) > 0)
     # TODO(jrobbins): test actual values from default config
@@ -839,10 +839,10 @@
          'Pri-4'],
         result_labels[:result_labels.index('OpSys-All')])
     self.assertEqual('Pri -status', harmonized.default_sort_spec.strip())
-    self.assertItemsEqual(c1.field_defs + c2.field_defs,
-                          harmonized.field_defs)
-    self.assertItemsEqual(c1.approval_defs + c2.approval_defs,
-                          harmonized.approval_defs)
+    six.assertCountEqual(
+        self, c1.field_defs + c2.field_defs, harmonized.field_defs)
+    six.assertCountEqual(
+        self, c1.approval_defs + c2.approval_defs, harmonized.approval_defs)
 
   def testHarmonizeConfigsMeansOpen(self):
     c1 = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
@@ -1778,8 +1778,8 @@
             default_project_name=issue.project_name)
     ]
     self.assertEqual(actual_amendments, expected_amendments)
-    self.assertItemsEqual(
-        actual_impacted_iids, [
+    six.assertCountEqual(
+        self, actual_impacted_iids, [
             blocked_on_add.issue_id, blocking_add.issue_id, blocked_on.issue_id,
             blocking.issue_id
         ])
@@ -1868,46 +1868,46 @@
 
   def testDiffValueLists(self):
     added, removed = tracker_bizobj.DiffValueLists([], [])
-    self.assertItemsEqual([], added)
-    self.assertItemsEqual([], removed)
+    six.assertCountEqual(self, [], added)
+    six.assertCountEqual(self, [], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([], None)
-    self.assertItemsEqual([], added)
-    self.assertItemsEqual([], removed)
+    six.assertCountEqual(self, [], added)
+    six.assertCountEqual(self, [], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([1, 2], [])
-    self.assertItemsEqual([1, 2], added)
-    self.assertItemsEqual([], removed)
+    six.assertCountEqual(self, [1, 2], added)
+    six.assertCountEqual(self, [], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([], [8, 9])
-    self.assertItemsEqual([], added)
-    self.assertItemsEqual([8, 9], removed)
+    six.assertCountEqual(self, [], added)
+    six.assertCountEqual(self, [8, 9], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([1, 2], [8, 9])
-    self.assertItemsEqual([1, 2], added)
-    self.assertItemsEqual([8, 9], removed)
+    six.assertCountEqual(self, [1, 2], added)
+    six.assertCountEqual(self, [8, 9], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([1, 2, 5, 6], [5, 6, 8, 9])
-    self.assertItemsEqual([1, 2], added)
-    self.assertItemsEqual([8, 9], removed)
+    six.assertCountEqual(self, [1, 2], added)
+    six.assertCountEqual(self, [8, 9], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([5, 6], [5, 6, 8, 9])
-    self.assertItemsEqual([], added)
-    self.assertItemsEqual([8, 9], removed)
+    six.assertCountEqual(self, [], added)
+    six.assertCountEqual(self, [8, 9], removed)
 
     added, removed = tracker_bizobj.DiffValueLists([1, 2, 5, 6], [5, 6])
-    self.assertItemsEqual([1, 2], added)
-    self.assertItemsEqual([], removed)
+    six.assertCountEqual(self, [1, 2], added)
+    six.assertCountEqual(self, [], removed)
 
     added, removed = tracker_bizobj.DiffValueLists(
         [1, 2, 2, 5, 6], [5, 6, 8, 9])
-    self.assertItemsEqual([1, 2, 2], added)
-    self.assertItemsEqual([8, 9], removed)
+    six.assertCountEqual(self, [1, 2, 2], added)
+    six.assertCountEqual(self, [8, 9], removed)
 
     added, removed = tracker_bizobj.DiffValueLists(
         [1, 2, 5, 6], [5, 6, 8, 8, 9])
-    self.assertItemsEqual([1, 2], added)
-    self.assertItemsEqual([8, 8, 9], removed)
+    six.assertCountEqual(self, [1, 2], added)
+    six.assertCountEqual(self, [8, 8, 9], removed)
 
   def testMakeFieldAmendment_NoSuchFieldDef(self):
     config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
@@ -2053,7 +2053,10 @@
         tracker_pb2.ComponentDef(component_id=2, path='DB')]
     self.assertEqual(
         tracker_bizobj.MakeAmendment(
-            tracker_pb2.FieldID.COMPONENTS, '-UI DB', [], []),
+            tracker_pb2.FieldID.COMPONENTS,
+            '-UI DB', [], [],
+            added_component_ids=[2],
+            removed_component_ids=[1]),
         tracker_bizobj.MakeComponentsAmendment([2], [1], config))
 
   def testMakeBlockedOnAmendment(self):
diff --git a/tracker/test/tracker_helpers_test.py b/tracker/test/tracker_helpers_test.py
index 4f89cc9..b7e930b 100644
--- a/tracker/test/tracker_helpers_test.py
+++ b/tracker/test/tracker_helpers_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittest for the tracker helpers module."""
 from __future__ import print_function
@@ -11,6 +10,8 @@
 import copy
 import mock
 import unittest
+import io
+import six
 
 import settings
 
@@ -21,15 +22,16 @@
 from framework import permissions
 from framework import template_helpers
 from framework import urls
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
 from tracker import tracker_bizobj
 from tracker import tracker_constants
 from tracker import tracker_helpers
+from werkzeug.datastructures import FileStorage
 
 TEST_ID_MAP = {
     'a@example.com': 1,
@@ -166,39 +168,39 @@
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['', ' ', '  \t', '-'])
-    self.assertItemsEqual([], add)
-    self.assertItemsEqual([], remove)
+    six.assertCountEqual(self, [], add)
+    six.assertCountEqual(self, [], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['a', 'b', 'c'])
-    self.assertItemsEqual(['a', 'b', 'c'], add)
-    self.assertItemsEqual([], remove)
+    six.assertCountEqual(self, ['a', 'b', 'c'], add)
+    six.assertCountEqual(self, [], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['a-a-a', 'b-b', 'c-'])
-    self.assertItemsEqual(['a-a-a', 'b-b', 'c-'], add)
-    self.assertItemsEqual([], remove)
+    six.assertCountEqual(self, ['a-a-a', 'b-b', 'c-'], add)
+    six.assertCountEqual(self, [], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['-a'])
-    self.assertItemsEqual([], add)
-    self.assertItemsEqual(['a'], remove)
+    six.assertCountEqual(self, [], add)
+    six.assertCountEqual(self, ['a'], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['-a', 'b', 'c-c'])
-    self.assertItemsEqual(['b', 'c-c'], add)
-    self.assertItemsEqual(['a'], remove)
+    six.assertCountEqual(self, ['b', 'c-c'], add)
+    six.assertCountEqual(self, ['a'], remove)
 
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['-a', '-b-b', '-c-'])
-    self.assertItemsEqual([], add)
-    self.assertItemsEqual(['a', 'b-b', 'c-'], remove)
+    six.assertCountEqual(self, [], add)
+    six.assertCountEqual(self, ['a', 'b-b', 'c-'], remove)
 
     # We dedup, but we don't cancel out items that are both added and removed.
     add, remove = tracker_helpers._ClassifyPlusMinusItems(
         ['a', 'a', '-a'])
-    self.assertItemsEqual(['a'], add)
-    self.assertItemsEqual(['a'], remove)
+    six.assertCountEqual(self, ['a'], add)
+    six.assertCountEqual(self, ['a'], remove)
 
   def testParseIssueRequestFields(self):
     parsed_fields = tracker_helpers._ParseIssueRequestFields(fake.PostData({
@@ -227,22 +229,25 @@
             }}))
 
   def testParseIssueRequestAttachments(self):
-    file1 = testing_helpers.Blank(
+    file1 = FileStorage(
+        stream=io.BytesIO(b'hello world'),
         filename='hello.c',
-        value='hello world')
-
-    file2 = testing_helpers.Blank(
+    )
+    file2 = FileStorage(
+        stream=io.BytesIO(b'Welcome to our project'),
         filename='README',
-        value='Welcome to our project')
+    )
 
-    file3 = testing_helpers.Blank(
+    file3 = FileStorage(
+        stream=io.BytesIO(b'Abort, Retry, or Fail?'),
         filename='c:\\dir\\subdir\\FILENAME.EXT',
-        value='Abort, Retry, or Fail?')
+    )
 
     # Browsers send this if FILE field was not filled in.
-    file4 = testing_helpers.Blank(
+    file4 = FileStorage(
+        stream=io.BytesIO(b''),
         filename='',
-        value='')
+    )
 
     attachments = tracker_helpers._ParseIssueRequestAttachments({})
     self.assertEqual([], attachments)
@@ -250,26 +255,31 @@
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file1': [file1],
         }))
-    self.assertEqual(
-        [('hello.c', 'hello world', 'text/plain')],
-        attachments)
+    self.assertEqual([('hello.c', b'hello world', 'text/plain')], attachments)
+    file1.seek(0)
 
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file1': [file1],
         'file2': [file2],
         }))
     self.assertEqual(
-        [('hello.c', 'hello world', 'text/plain'),
-         ('README', 'Welcome to our project', 'text/plain')],
-        attachments)
+        [
+            ('hello.c', b'hello world', 'text/plain'),
+            ('README', b'Welcome to our project', 'text/plain')
+        ], attachments)
+    file1.seek(0)
+    file2.seek(0)
 
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file3': [file3],
         }))
     self.assertEqual(
-        [('FILENAME.EXT', 'Abort, Retry, or Fail?',
-          'application/octet-stream')],
-        attachments)
+        [
+            (
+                'FILENAME.EXT', b'Abort, Retry, or Fail?',
+                'application/octet-stream')
+        ], attachments)
+    file3.seek(0)
 
     attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
         'file1': [file4],  # Does not appear in result
@@ -277,9 +287,12 @@
         'file4': [file4],  # Does not appear in result
         }))
     self.assertEqual(
-        [('FILENAME.EXT', 'Abort, Retry, or Fail?',
-          'application/octet-stream')],
-        attachments)
+        [
+            (
+                'FILENAME.EXT', b'Abort, Retry, or Fail?',
+                'application/octet-stream')
+        ], attachments)
+    file3.seek(0)
 
   def testParseIssueRequestKeptAttachments(self):
     pass  # TODO(jrobbins): Write this test.
@@ -368,12 +381,12 @@
     self.assertEqual('', parsed_users.owner_username)
     self.assertEqual(
         framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
-    self.assertItemsEqual(['c@example.com', 'a@example.com'],
-                          parsed_users.cc_usernames)
+    six.assertCountEqual(
+        self, ['c@example.com', 'a@example.com'], parsed_users.cc_usernames)
     self.assertEqual(['b@example.com'], parsed_users.cc_usernames_remove)
-    self.assertItemsEqual([TEST_ID_MAP['c@example.com'],
-                           TEST_ID_MAP['a@example.com']],
-                          parsed_users.cc_ids)
+    six.assertCountEqual(
+        self, [TEST_ID_MAP['c@example.com'], TEST_ID_MAP['a@example.com']],
+        parsed_users.cc_ids)
     self.assertEqual([TEST_ID_MAP['b@example.com']],
                       parsed_users.cc_ids_remove)
 
@@ -386,11 +399,12 @@
     self.assertEqual('fuhqwhgads@example.com', parsed_users.owner_username)
     gen_uid = framework_helpers.MurmurHash3_x86_32(parsed_users.owner_username)
     self.assertEqual(gen_uid, parsed_users.owner_id)  # autocreated user
-    self.assertItemsEqual(
-        ['c@example.com', 'fuhqwhgads@example.com'], parsed_users.cc_usernames)
+    six.assertCountEqual(
+        self, ['c@example.com', 'fuhqwhgads@example.com'],
+        parsed_users.cc_usernames)
     self.assertEqual([], parsed_users.cc_usernames_remove)
-    self.assertItemsEqual(
-       [TEST_ID_MAP['c@example.com'], gen_uid], parsed_users.cc_ids)
+    six.assertCountEqual(
+        self, [TEST_ID_MAP['c@example.com'], gen_uid], parsed_users.cc_ids)
     self.assertEqual([], parsed_users.cc_ids_remove)
 
     post_data = fake.PostData({
@@ -398,12 +412,12 @@
         })
     parsed_users = tracker_helpers._ParseIssueRequestUsers(
         'fake connection', post_data, self.services)
-    self.assertItemsEqual(
-        ['c@example.com', 'b@example.com'], parsed_users.cc_usernames)
+    six.assertCountEqual(
+        self, ['c@example.com', 'b@example.com'], parsed_users.cc_usernames)
     self.assertEqual([], parsed_users.cc_usernames_remove)
-    self.assertItemsEqual(
-       [TEST_ID_MAP['c@example.com'], TEST_ID_MAP['b@example.com']],
-       parsed_users.cc_ids)
+    six.assertCountEqual(
+        self, [TEST_ID_MAP['c@example.com'], TEST_ID_MAP['b@example.com']],
+        parsed_users.cc_ids)
     self.assertEqual([], parsed_users.cc_ids_remove)
 
   def testParseBlockers_BlockedOnNothing(self):
@@ -698,9 +712,9 @@
   @mock.patch('tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', 1)
   def testComputeNewQuotaBytesUsed_ProjectQuota(self):
     upload_1 = framework_helpers.AttachmentUpload(
-        'matter not', 'three men make a tiger', 'matter not')
+        'matter not', b'three men make a tiger', 'matter not')
     upload_2 = framework_helpers.AttachmentUpload(
-        'matter not', 'chicken', 'matter not')
+        'matter not', b'chicken', 'matter not')
     attachments = [upload_1, upload_2]
 
     project = fake.Project()
@@ -713,7 +727,7 @@
     self.assertEqual(actual_new, expected_new)
 
     upload_3 = framework_helpers.AttachmentUpload(
-        'matter not', 'donut', 'matter not')
+        'matter not', b'donut', 'matter not')
     attachments.append(upload_3)
     with self.assertRaises(exceptions.OverAttachmentQuota):
       tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
@@ -722,7 +736,7 @@
       'tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', len('tiger'))
   def testComputeNewQuotaBytesUsed_GeneralQuota(self):
     upload_1 = framework_helpers.AttachmentUpload(
-        'matter not', 'tiger', 'matter not')
+        'matter not', b'tiger', 'matter not')
     attachments = [upload_1]
 
     project = fake.Project()
@@ -732,13 +746,13 @@
     self.assertEqual(actual_new, expected_new)
 
     upload_2 = framework_helpers.AttachmentUpload(
-        'matter not', 'donut', 'matter not')
+        'matter not', b'donut', 'matter not')
     attachments.append(upload_2)
     with self.assertRaises(exceptions.OverAttachmentQuota):
       tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
 
     upload_3 = framework_helpers.AttachmentUpload(
-        'matter not', 'donut', 'matter not')
+        'matter not', b'donut', 'matter not')
     attachments.append(upload_3)
     with self.assertRaises(exceptions.OverAttachmentQuota):
       tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
@@ -901,7 +915,7 @@
 
   # ParseMergeFields is tested in IssueMergeTest.
   # AddIssueStarrers is tested in IssueMergeTest.testMergeIssueStars().
-  # IsMergeAllowed is tested in IssueMergeTest.
+  # CanEditProjectIssue is tested in IssueMergeTest.
 
   def testPairDerivedValuesWithRuleExplanations_Nothing(self):
     """Test we return nothing for an issue with no derived values."""
@@ -990,8 +1004,9 @@
     issue_list = [self.issue1, self.issue2, self.issue3]
     users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
         'fake cnxn', issue_list, self.user)
-    self.assertItemsEqual([0, 1, 1001, 1002, 1003, 2001, 2002, 3002],
-                          list(users_by_id.keys()))
+    six.assertCountEqual(
+        self, [0, 1, 1001, 1002, 1003, 2001, 2002, 3002],
+        list(users_by_id.keys()))
     for user_id in [1001, 1002, 1003, 2001]:
       self.assertEqual(users_by_id[user_id].user_id, user_id)
 
@@ -999,8 +1014,8 @@
     issue_list = [self.issue1, self.issue2, self.issue3]
     users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
         'fake cnxn', issue_list, self.user, omit_ids=[1001, 1003])
-    self.assertItemsEqual([0, 1, 1002, 2001, 2002, 3002],
-        list(users_by_id.keys()))
+    six.assertCountEqual(
+        self, [0, 1, 1002, 2001, 2002, 3002], list(users_by_id.keys()))
     for user_id in [1002, 2001, 2002, 3002]:
       self.assertEqual(users_by_id[user_id].user_id, user_id)
 
@@ -1008,7 +1023,7 @@
     issue_list = []
     users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
         'fake cnxn', issue_list, self.user)
-    self.assertItemsEqual([], list(users_by_id.keys()))
+    six.assertCountEqual(self, [], list(users_by_id.keys()))
 
 
 class GetAllIssueProjectsTest(unittest.TestCase):
@@ -1236,19 +1251,34 @@
     self.assertEqual(str(mergee_issue.local_id), text)
     self.assertEqual(mergee_issue, merge_into_issue)
 
-  def testIsMergeAllowed(self):
+  def testCanEditProjectIssue(self):
     mr = testing_helpers.MakeMonorailRequest()
-    issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
+    issue = fake.MakeTestIssue(
+        self.project.project_id, 1, 'summary', 'New', 111)
     issue.project_name = self.project.project_name
 
-    for (perm_set, expected_merge_allowed) in (
-            (permissions.READ_ONLY_PERMISSIONSET, False),
-            (permissions.COMMITTER_INACTIVE_PERMISSIONSET, False),
-            (permissions.COMMITTER_ACTIVE_PERMISSIONSET, True),
-            (permissions.OWNER_ACTIVE_PERMISSIONSET, True)):
-      mr.perms = perm_set
-      merge_allowed = tracker_helpers.IsMergeAllowed(issue, mr, self.services)
-      self.assertEqual(expected_merge_allowed, merge_allowed)
+    non_member_not_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(False, non_member_not_allowed)
+
+    committer_id = 3
+    self.project.committer_ids.extend([committer_id])
+    mr.auth.effective_ids.add(committer_id)
+    committer_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(True, committer_allowed)
+
+    self.project.state = project_pb2.ProjectState.ARCHIVED
+    committer_read_only_not_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(False, committer_read_only_not_allowed)
+
+    owner_id = 1
+    self.project.owner_ids.extend([owner_id])
+    mr.auth.effective_ids.add(owner_id)
+    owner_read_only_not_allowed = tracker_helpers.CanEditProjectIssue(
+        mr, self.project, issue, None)
+    self.assertEqual(False, owner_read_only_not_allowed)
 
   def testMergeIssueStars(self):
     mr = testing_helpers.MakeMonorailRequest()
@@ -1276,13 +1306,13 @@
 
     new_starrers = tracker_helpers.GetNewIssueStarrers(
         self.cnxn, self.services, [1, 3], 2)
-    self.assertItemsEqual(new_starrers, [1, 2, 6])
+    six.assertCountEqual(self, new_starrers, [1, 2, 6])
     tracker_helpers.AddIssueStarrers(
         self.cnxn, self.services, mr, 2, self.project, new_starrers)
     issue_2_starrers = self.services.issue_star.LookupItemStarrers(
         self.cnxn, 2)
     # XXX(jrobbins): these tests incorrectly mix local IDs with IIDs.
-    self.assertItemsEqual([1, 2, 3, 4, 5, 6], issue_2_starrers)
+    six.assertCountEqual(self, [1, 2, 3, 4, 5, 6], issue_2_starrers)
 
 
 class MergeLinkedMembersTest(unittest.TestCase):
@@ -1345,58 +1375,61 @@
   def testUnsignedUser_NormalProject(self):
     visible_members = self.DoFiltering(
         permissions.READ_ONLY_PERMISSIONSET, unsigned_user=True)
-    self.assertItemsEqual(
-        [self.owner_email, self.committer_email, self.contributor_email,
-         self.indirect_member_email],
-        visible_members)
+    six.assertCountEqual(
+        self, [
+            self.owner_email, self.committer_email, self.contributor_email,
+            self.indirect_member_email
+        ], visible_members)
 
   def testUnsignedUser_RestrictedProject(self):
     self.project.only_owners_see_contributors = True
     visible_members = self.DoFiltering(
         permissions.READ_ONLY_PERMISSIONSET, unsigned_user=True)
-    self.assertItemsEqual(
+    six.assertCountEqual(
+        self,
         [self.owner_email, self.committer_email, self.indirect_member_email],
         visible_members)
 
   def testOwnersAndAdminsCanSeeAll_NormalProject(self):
     visible_members = self.DoFiltering(
         permissions.OWNER_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.ADMIN_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
   def testOwnersAndAdminsCanSeeAll_HubAndSpoke(self):
     self.project.only_owners_see_contributors = True
 
     visible_members = self.DoFiltering(
         permissions.OWNER_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.ADMIN_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.COMMITTER_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
   def testNonOwnersCanSeeAll_NormalProject(self):
     visible_members = self.DoFiltering(
         permissions.COMMITTER_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
     visible_members = self.DoFiltering(
         permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(self.all_emails, visible_members)
+    six.assertCountEqual(self, self.all_emails, visible_members)
 
   def testCommittersSeeOnlySameDomain_HubAndSpoke(self):
     self.project.only_owners_see_contributors = True
 
     visible_members = self.DoFiltering(
         permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
-    self.assertItemsEqual(
+    six.assertCountEqual(
+        self,
         [self.owner_email, self.committer_email, self.indirect_member_email],
         visible_members)
 
@@ -1618,29 +1651,44 @@
     tracker_helpers.AssertValidIssueForCreate(
         self.cnxn, self.services, input_issue, 'nonempty description')
 
+  def testAssertValidIssueForCreate_ValidatesLabels(self):
+    input_issue = tracker_pb2.Issue(
+        summary='sum',
+        labels=['freeze_new_label'],
+        status='New',
+        owner_id=111,
+        project_id=789)
+    with self.assertRaisesRegex(
+        exceptions.InputException,
+        ("The creation of new labels is blocked for the Chromium project"
+         " in Monorail. To continue with editing your issue, please"
+         " remove: freeze_new_label label\\(s\\)")):
+      tracker_helpers.AssertValidIssueForCreate(
+          self.cnxn, self.services, input_issue, 'nonempty description')
+
   def testAssertValidIssueForCreate_ValidatesOwner(self):
     input_issue = tracker_pb2.Issue(
         summary='sum', status='New', owner_id=222, project_id=789)
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Issue owner must be a project member'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Issue owner must be a project member'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
     input_issue.owner_id = 333
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Issue owner user ID not found'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Issue owner user ID not found'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
     input_issue.owner_id = 999
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Issue owner cannot be a user group'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Issue owner cannot be a user group'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
   def testAssertValidIssueForCreate_ValidatesSummary(self):
     input_issue = tracker_pb2.Issue(
         summary='', status='New', owner_id=111, project_id=789)
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Summary is required'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Summary is required'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
       input_issue.summary = '   '
@@ -1650,8 +1698,8 @@
   def testAssertValidIssueForCreate_ValidatesDescription(self):
     input_issue = tracker_pb2.Issue(
         summary='sum', status='New', owner_id=111, project_id=789)
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Description is required'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Description is required'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, '')
       tracker_helpers.AssertValidIssueForCreate(
@@ -1678,8 +1726,8 @@
       return None
 
     self.services.config.LookupStatusID = mock_status_lookup
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 'Undefined status: DNE_status'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Undefined status: DNE_status'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
@@ -1691,9 +1739,8 @@
         owner_id=111,
         project_id=789,
         component_ids=[3])
-    with self.assertRaisesRegexp(
-        exceptions.InputException,
-        'Undefined or deprecated component with id: 3'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Undefined or deprecated component with id: 3'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
@@ -1704,9 +1751,8 @@
         owner_id=111,
         project_id=789,
         component_ids=[self.component_def_2.component_id])
-    with self.assertRaisesRegexp(
-        exceptions.InputException,
-        'Undefined or deprecated component with id: 2'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                'Undefined or deprecated component with id: 2'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
 
@@ -1728,8 +1774,8 @@
                 user_fd.field_id, None, None, 124, None, None, False)
         ])
     copied_issue = copy.deepcopy(input_issue)
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 r'users/123: .+\nusers/124: .+'):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                r'users/123: .+\nusers/124: .+'):
       tracker_helpers.AssertValidIssueForCreate(
           self.cnxn, self.services, input_issue, 'nonempty description')
     self.assertEqual(input_issue, copied_issue)
@@ -1894,7 +1940,7 @@
     expected_merge_add = copy.deepcopy(merge_add)
     expected_merge_add.assume_stale = False
     # We are adding 333 and removing 222 in issue_main with delta_main.
-    expected_merge_add.cc_ids = [expected_main.owner_id, 333, 111]
+    expected_merge_add.cc_ids = sorted([expected_main.owner_id, 111, 333])
     expected_merged_from_add[expected_merge_add.issue_id] = [
         issue_main.issue_id
     ]
@@ -2086,9 +2132,9 @@
     ]
 
     upload_1 = framework_helpers.AttachmentUpload(
-        'dragon', 'OOOOOO\n', 'text/plain')
+        'dragon', b'OOOOOO\n', 'text/plain')
     upload_2 = framework_helpers.AttachmentUpload(
-        'snake', 'ooooo\n', 'text/plain')
+        'snake', b'ooooo\n', 'text/plain')
     attachment_uploads = [upload_1, upload_2]
 
     actual = tracker_helpers._EnforceAttachmentQuotaLimits(
@@ -2118,13 +2164,13 @@
     ]
 
     upload_1 = framework_helpers.AttachmentUpload(
-        'dragon', 'OOOOOO\n', 'text/plain')
+        'dragon', b'OOOOOO\n', 'text/plain')
     upload_2 = framework_helpers.AttachmentUpload(
-        'snake', 'ooooo\n', 'text/plain')
+        'snake', b'ooooo\n', 'text/plain')
     attachment_uploads = [upload_1, upload_2]
 
-    with self.assertRaisesRegexp(exceptions.OverAttachmentQuota,
-                                 r'.+ project Patroclus\n.+ project Circe'):
+    with self.assertRaisesRegex(exceptions.OverAttachmentQuota,
+                                r'.+ project Patroclus\n.+ project Circe'):
       tracker_helpers._EnforceAttachmentQuotaLimits(
           self.cnxn, issue_delta_pairs, self.services, attachment_uploads)
 
@@ -2225,6 +2271,21 @@
             delta_8, delta_9, delta_10, delta_11
         ])
 
+  def testAssertIssueChangesValid_ValidatesLabels(self):
+    """Asserts labels."""
+    issue_1 = _Issue('chicken', 1)
+    self.services.issue.TestAddIssue(issue_1)
+    delta_1 = tracker_pb2.IssueDelta(labels_add=['freeze_new_label'])
+    issue_delta_pairs = [(issue_1, delta_1)]
+    comment = 'just a plain comment'
+    with self.assertRaisesRegex(
+        exceptions.InputException,
+        ("The creation of new labels is blocked for the Chromium project"
+         " in Monorail. To continue with editing your issue, please"
+         " remove: freeze_new_label label\\(s\\).")):
+      tracker_helpers._AssertIssueChangesValid(
+          self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
+
   def testAssertIssueChangesValid_RequiredField(self):
     """Asserts fields and requried fields.."""
     issue_1 = _Issue('chicken', 1)
@@ -2323,8 +2384,8 @@
         '%s: MERGED type statuses must accompany mergedInto values.' %
         issue_3_ref)
 
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 '\n'.join(expected_err_msgs)):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                '\n'.join(expected_err_msgs)):
       tracker_helpers._AssertIssueChangesValid(
           self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
 
@@ -2390,8 +2451,8 @@
         (issue_7, delta_7),
     ]
 
-    with self.assertRaisesRegexp(exceptions.InputException,
-                                 '\n'.join(expected_err_msgs)):
+    with self.assertRaisesRegex(exceptions.InputException,
+                                '\n'.join(expected_err_msgs)):
       tracker_helpers._AssertIssueChangesValid(
           self.cnxn, issue_delta_pairs, self.services)
 
@@ -2426,13 +2487,13 @@
 
     new_cc_ids = tracker_helpers._ComputeNewCcsFromIssueMerge(
         target_issue, [source_issue_1, source_issue_2, source_issue_3])
-    self.assertItemsEqual(new_cc_ids, [444, 555, 222])
+    six.assertCountEqual(self, new_cc_ids, [444, 555, 222])
 
   def testComputeNewCcsFromIssueMerge_Empty(self):
     target_issue = fake.MakeTestIssue(789, 10, 'Target issue', 'New', 111)
     self.services.issue.TestAddIssue(target_issue)
     new_cc_ids = tracker_helpers._ComputeNewCcsFromIssueMerge(target_issue, [])
-    self.assertItemsEqual(new_cc_ids, [])
+    six.assertCountEqual(self, new_cc_ids, [])
 
   def testEnforceNonMergeStatusDeltas(self):
     # No updates: user is setting to a non-MERGED status with no
@@ -2692,7 +2753,7 @@
             [('proj', m_remove.local_id)], default_project_name='proj')
         ]
     self.assertEqual(actual_amendments, expected_amendments)
-    self.assertItemsEqual(actual_new_starrers, [333, 444])
+    six.assertCountEqual(self, actual_new_starrers, [333, 444])
 
     expected_issue.cc_ids.append(777)
     expected_issue.blocked_on_iids = [78404, bo_add.issue_id]
@@ -2767,7 +2828,7 @@
     dne_users = [2, 3]
     existing = [1, 1001, 1002, 1003, 2001, 2002, 3002]
     all_users = existing + dne_users
-    with self.assertRaisesRegexp(
+    with self.assertRaisesRegex(
         exceptions.InputException,
         'users/2: User does not exist.\nusers/3: User does not exist.'):
       with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
diff --git a/tracker/test/tracker_views_test.py b/tracker/test/tracker_views_test.py
index ddc2a3e..db047c3 100644
--- a/tracker/test/tracker_views_test.py
+++ b/tracker/test/tracker_views_test.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unittest for issue tracker views."""
 from __future__ import print_function
@@ -23,8 +22,8 @@
 from framework import gcs_helpers
 from framework import template_helpers
 from framework import urls
-from proto import project_pb2
-from proto import tracker_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
diff --git a/tracker/test/webcomponentspage_test.py b/tracker/test/webcomponentspage_test.py
index 65cfc66..86d3606 100644
--- a/tracker/test/webcomponentspage_test.py
+++ b/tracker/test/webcomponentspage_test.py
@@ -1,7 +1,6 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2020 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 """Tests for the Monorail SPA pages, as served by EZT."""
 from __future__ import print_function
 from __future__ import division
@@ -10,12 +9,9 @@
 import mock
 import unittest
 
-import ezt
 
-import settings
-from framework import permissions
-from proto import project_pb2
-from proto import site_pb2
+from framework import exceptions
+from mrproto import project_pb2
 from services import service_manager
 from tracker import webcomponentspage
 from testing import fake
@@ -35,8 +31,7 @@
     self.hotlist = self.services.features.TestAddHotlist(
         'HotlistName', summary='summary', owner_ids=[111], hotlist_id=1236)
 
-    self.servlet = webcomponentspage.WebComponentsPage(
-        'req', 'res', services=self.services)
+    self.servlet = webcomponentspage.WebComponentsPage(services=self.services)
 
   def testHotlistPage_OldUiUrl(self):
     mr = testing_helpers.MakeMonorailRequest(
@@ -76,8 +71,7 @@
     self.project_a = self.services.project.TestAddProject('a', project_id=1)
     self.project_b = self.services.project.TestAddProject('b', project_id=2)
 
-    self.servlet = webcomponentspage.ProjectListPage(
-        'req', 'res', services=self.services)
+    self.servlet = webcomponentspage.ProjectListPage(services=self.services)
 
   @mock.patch('settings.domain_to_default_project', {})
   def testMaybeRedirectToDomainDefaultProject_NoMatch(self):
@@ -114,7 +108,6 @@
     mr = testing_helpers.MakeMonorailRequest()
     mr.request.host = 'example.com'
     self.servlet.redirect = mock.Mock()
-    msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
-    print('msg: ' + msg)
-    self.assertTrue(msg.startswith('Redirected'))
-    self.servlet.redirect.assert_called_once()
+    with self.assertRaises(exceptions.RedirectException) as e:
+      self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+    self.assertIn('/p/a', str(e.exception))
diff --git a/tracker/tracker_bizobj.py b/tracker/tracker_bizobj.py
index f3f2594..f90b24f 100644
--- a/tracker/tracker_bizobj.py
+++ b/tracker/tracker_bizobj.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Business objects for the Monorail issue tracker.
 
@@ -26,7 +25,7 @@
 from framework import framework_helpers
 from framework import timestr
 from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import tracker_constants
 
 
@@ -435,15 +434,15 @@
 
 def GetIssueComponentsAndAncestors(issue, config):
   """Return a list of all the components that an issue is in."""
-  result = set()
+  result = []
   for component_id in issue.component_ids:
     cd = FindComponentDefByID(component_id, config)
     if cd is None:
       logging.error('Tried to look up non-existent component %r' % component_id)
       continue
     ancestors = FindAncestorComponents(config, cd)
-    result.add(cd)
-    result.update(ancestors)
+    result.append(cd)
+    result.extend(ancestors)
 
   return sorted(result, key=lambda cd: cd.path)
 
@@ -854,8 +853,8 @@
 
 
 def UsersInvolvedInApprovalDefs(approval_defs, matching_fds):
-  # type: (Sequence[proto.tracker_pb2.ApprovalDef],
-  #     Sequence[proto.tracker_pb2.FieldDef]) -> Collection[int]
+  # type: (Sequence[mrproto.tracker_pb2.ApprovalDef],
+  #     Sequence[mrproto.tracker_pb2.FieldDef]) -> Collection[int]
   """Return a set of user IDs referenced in the approval_defs and field defs"""
   result = set()
   for ad in approval_defs:
@@ -986,8 +985,8 @@
         fd_removed_values_by_phase[fv.phase_id].append(fv)
       # Use all_fv_phase_ids to create Amendments, so no empty amendments
       # are created for issue phases that had no field value changes.
-      all_fv_phase_ids = set(
-          fd_removed_values_by_phase.keys() + fd_added_values_by_phase.keys())
+      all_fv_phase_ids = set(fd_removed_values_by_phase.keys()) | set(
+          fd_added_values_by_phase.keys())
       for phase_id in all_fv_phase_ids:
         new_values = [GetFieldValue(fv, {}) for fv
                       in fd_added_values_by_phase.get(phase_id, [])]
@@ -1226,8 +1225,12 @@
   impacted_iids = set()
 
   def addAmendment(add_iids, remove_iids, amendment_func):
-    add_refs = issue_service.LookupIssueRefs(cnxn, add_iids).values()
-    remove_refs = issue_service.LookupIssueRefs(cnxn, remove_iids).values()
+    add_refs_dict = issue_service.LookupIssueRefs(cnxn, add_iids)
+    add_refs = [add_refs_dict[iid] for iid in add_iids if iid in add_refs_dict]
+    remove_refs_dict = issue_service.LookupIssueRefs(cnxn, remove_iids)
+    remove_refs = [
+        remove_refs_dict[iid] for iid in remove_iids if iid in remove_refs_dict
+    ]
     new_am = amendment_func(
         add_refs, remove_refs, default_project_name=issue.project_name)
     amendments.append(new_am)
@@ -1269,8 +1272,14 @@
 
 
 def MakeAmendment(
-    field, new_value, added_ids, removed_ids, custom_field_name=None,
-    old_value=None):
+    field,
+    new_value,
+    added_ids,
+    removed_ids,
+    custom_field_name=None,
+    old_value=None,
+    added_component_ids=None,
+    removed_component_ids=None):
   """Utility function to populate an Amendment PB.
 
   Args:
@@ -1296,6 +1305,12 @@
   if custom_field_name is not None:
     amendment.custom_field_name = custom_field_name
 
+  if added_component_ids is not None:
+    amendment.added_component_ids.extend(added_component_ids)
+
+  if removed_component_ids is not None:
+    amendment.removed_component_ids.extend(removed_component_ids)
+
   return amendment
 
 
@@ -1505,20 +1520,26 @@
   # lookups (and maybe permission checks in the future).  But, what
   # about history that references deleleted components?
   added_comp_paths = []
+  valid_added_comp_ids = []
   for comp_id in added_comp_ids:
     cd = FindComponentDefByID(comp_id, config)
     if cd:
       added_comp_paths.append(cd.path)
+      valid_added_comp_ids.append(comp_id)
 
   removed_comp_paths = []
+  valid_removed_comp_ids = []
   for comp_id in removed_comp_ids:
     cd = FindComponentDefByID(comp_id, config)
     if cd:
       removed_comp_paths.append(cd.path)
-
-  return _PlusMinusAmendment(
+      valid_removed_comp_ids.append(comp_id)
+  values = _PlusMinusString(added_comp_paths, removed_comp_paths)
+  return MakeAmendment(
       tracker_pb2.FieldID.COMPONENTS,
-      added_comp_paths, removed_comp_paths)
+      values, [], [],
+      added_component_ids=valid_added_comp_ids,
+      removed_component_ids=valid_removed_comp_ids)
 
 
 def MakeBlockedOnAmendment(
diff --git a/tracker/tracker_constants.py b/tracker/tracker_constants.py
index e0fe1b2..bbe242b 100644
--- a/tracker/tracker_constants.py
+++ b/tracker/tracker_constants.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Some constants used in Monorail issue tracker pages."""
 from __future__ import print_function
@@ -10,7 +9,7 @@
 
 import re
 
-from proto import user_pb2
+from mrproto import user_pb2
 
 
 # Default columns shown on issue list page, and other built-in cols.
@@ -75,7 +74,7 @@
 
 # Pattern for matching a full component name, not just a single leaf.
 # Allows any number of repeating valid leaf names separated by ">" characters.
-COMPONENT_PATH_PATTERN = '%s(\>%s)*' % (
+COMPONENT_PATH_PATTERN = r'%s(\>%s)*' % (
     COMPONENT_LEAF_PATTERN, COMPONENT_LEAF_PATTERN)
 
 # Regular expression used to validate new field names.
diff --git a/tracker/tracker_helpers.py b/tracker/tracker_helpers.py
index cd9acfa..f339b95 100644
--- a/tracker/tracker_helpers.py
+++ b/tracker/tracker_helpers.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Helper functions and classes used by the Monorail Issue Tracker pages.
 
@@ -38,7 +37,7 @@
 from framework import template_helpers
 from framework import urls
 from project import project_helpers
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import client_config_svc
 from tracker import field_helpers
 from tracker import tracker_bizobj
@@ -153,16 +152,14 @@
   status = post_data.get('status', '')
   template_name = urllib.parse.unquote_plus(post_data.get('template_name', ''))
   component_str = post_data.get('components', '')
-  # TODO: switch when convert /p to flask
-  # label_strs = post_data.getlist('label')
-  label_strs = post_data.getall('label')
+  label_strs = post_data.getlist('label')
 
   if is_description:
     tmpl_txt = post_data.get('tmpl_txt', '')
     comment = MarkupDescriptionOnInput(comment, tmpl_txt)
 
   comp_paths, comp_paths_remove = _ClassifyPlusMinusItems(
-      re.split('[,;\s]+', component_str))
+      re.split(r'[,;\s]+', component_str))
   parsed_components = ParsedComponents(
       component_str, comp_paths, comp_paths_remove)
   labels, labels_remove = _ClassifyPlusMinusItems(label_strs)
@@ -234,7 +231,7 @@
 def _ParseHotlists(post_data):
   entered_str = post_data.get('hotlists', '').strip()
   hotlist_refs = []
-  for ref_str in re.split('[,;\s]+', entered_str):
+  for ref_str in re.split(r'[,;\s]+', entered_str):
     if not ref_str:
       continue
     if ':' in ref_str:
@@ -259,9 +256,7 @@
   phase_field_val_strs_remove = collections.defaultdict(dict)
   for key in post_data.keys():
     if key.startswith(_CUSTOM_FIELD_NAME_PREFIX):
-      # TODO: switch when convert /p to flask
-      # val_strs = [v for v in post_data.getlist(key) if v]
-      val_strs = [v for v in post_data.getall(key) if v]
+      val_strs = [v for v in post_data.getlist(key) if v]
       if val_strs:
         try:
           field_id = int(key[len(_CUSTOM_FIELD_NAME_PREFIX):])
@@ -315,9 +310,10 @@
         item.filename = item.filename[item.filename.rindex('\\') + 1:]
       if not item.filename:
         continue  # Skip any FILE fields that were not filled in.
-      attachments.append((
-          item.filename, item.value,
-          filecontent.GuessContentTypeFromFilename(item.filename)))
+      attachments.append(
+          (
+              item.filename, item.read(),
+              filecontent.GuessContentTypeFromFilename(item.filename)))
 
   return attachments
 
@@ -331,9 +327,7 @@
   Returns:
     a list of attachment ids for kept attachments
   """
-  # TODO: switch when convert /p to flask
-  # kept_attachments = post_data.getlist('keep-attachment')
-  kept_attachments = post_data.getall('keep-attachment')
+  kept_attachments = post_data.getlist('keep-attachment')
   return [int(aid) for aid in kept_attachments]
 
 
@@ -359,7 +353,7 @@
   owner_email = post_data.get('owner', '').strip().lower()
 
   cc_usernames, cc_usernames_remove = _ClassifyPlusMinusItems(
-      re.split('[,;\s]+', cc_username_str))
+      re.split(r'[,;\s]+', cc_username_str))
 
   # Figure out the email addresses to lookup and do the lookup.
   emails_to_lookup = cc_usernames + cc_usernames_remove
@@ -401,7 +395,7 @@
   federated_ref_strings = []
 
   issue_ref = None
-  for ref_str in re.split('[,;\s]+', entered_str):
+  for ref_str in re.split(r'[,;\s]+', entered_str):
     # Handle federated references.
     if federated.IsShortlinkValid(ref_str):
       federated_ref_strings.append(ref_str)
@@ -940,7 +934,7 @@
 
 def ParsePostDataUsers(cnxn, pd_users_str, user_service):
   """Parse all the usernames from a users string found in a post data."""
-  emails, _remove = _ClassifyPlusMinusItems(re.split('[,;\s]+', pd_users_str))
+  emails, _remove = _ClassifyPlusMinusItems(re.split(r'[,;\s]+', pd_users_str))
   users_ids_by_email = user_service.LookupUserIDs(cnxn, emails, autocreate=True)
   user_ids = [users_ids_by_email[username] for username in emails if username]
   return user_ids, pd_users_str
@@ -1023,25 +1017,19 @@
       cnxn, services, config, merge_into_iid, new_starrers, True)
 
 
-def IsMergeAllowed(merge_into_issue, mr, services):
-  """Check to see if user has permission to merge with specified issue."""
-  merge_into_project = services.project.GetProjectByName(
-      mr.cnxn, merge_into_issue.project_name)
-  merge_into_config = services.config.GetProjectConfig(
-      mr.cnxn, merge_into_project.project_id)
-  merge_granted_perms = tracker_bizobj.GetGrantedPerms(
-      merge_into_issue, mr.auth.effective_ids, merge_into_config)
+def CanEditProjectIssue(mr, project, issue, granted_perms):
+  """Check if user permissions in another project allow editing.
 
-  merge_view_allowed = mr.perms.CanUsePerm(
-      permissions.VIEW, mr.auth.effective_ids,
-      merge_into_project, permissions.GetRestrictions(merge_into_issue),
-      granted_perms=merge_granted_perms)
-  merge_edit_allowed = mr.perms.CanUsePerm(
-      permissions.EDIT_ISSUE, mr.auth.effective_ids,
-      merge_into_project, permissions.GetRestrictions(merge_into_issue),
-      granted_perms=merge_granted_perms)
+  Wraps CanEditIssue with a call to get user permissions in given project.
 
-  return merge_view_allowed and merge_edit_allowed
+  We deviate from using CanUsePerm because that method does not calculate
+  Project state as part of the permissions. This seems to have deviated in
+  2018. CanEditIssue uses Project state to authorize user actions.
+  """
+  project_perms = permissions.GetPermissions(
+      mr.auth.user_pb, mr.auth.effective_ids, project)
+  return permissions.CanEditIssue(
+      mr.auth.effective_ids, project_perms, project, issue, granted_perms)
 
 
 def GetVisibleMembers(mr, project, services):
@@ -1243,8 +1231,8 @@
 
 
 def _GetEnumFieldValuesAndDocstrings(field_def, config):
-  # type: (proto.tracker_pb2.LabelDef, proto.tracker_pb2.ProjectIssueConfig) ->
-  #     Sequence[tuple(string, string)]
+  # type: (mrproto.tracker_pb2.LabelDef,
+  #   mrproto.tracker_pb2.ProjectIssueConfig) -> Sequence[tuple(string, string)]
   """Get sequence of value, docstring tuples for an enum field"""
   label_defs = config.well_known_labels
   lower_field_name = field_def.field_name.lower()
@@ -1354,8 +1342,8 @@
 
 
 def UpdateClosedTimestamp(config, issue, old_effective_status):
-  # type: (proto.tracker_pb2.ProjectIssueConfig, proto.tracker_pb2.Issue, str)
-  #     -> None
+  # type: (mrproto.tracker_pb2.ProjectIssueConfig,
+  #   mrproto.tracker_pb2.Issue, str) -> None
   """Sets or unsets the closed_timestamp based based on status changes.
 
   If the status is changing from open to closed, the closed_timestamp is set to
@@ -1496,7 +1484,7 @@
 
   new_bytes_by_pid = {}
   with exceptions.ErrorAggregator(exceptions.OverAttachmentQuota) as err_agg:
-    for pid, count in issue_count_by_pid.items():
+    for pid, count in sorted(issue_count_by_pid.items()):
       project = projects_by_id[pid]
       try:
         new_bytes_used = ComputeNewQuotaBytesUsed(
@@ -1597,6 +1585,10 @@
         err_agg.AddErrorMessage('{}: Summary required.', issue_ref)
       if delta.status == '':
         err_agg.AddErrorMessage('{}: Status is required.', issue_ref)
+      labels_err_msgs = field_helpers.ValidateLabels(
+          cnxn, services, issue.project_id, delta.labels_add)
+      if labels_err_msgs:
+        err_agg.AddErrorMessage('{}: {}', issue_ref, labels_err_msgs)
       # Do not pass in issue for validation, as issue is pre-update, and would
       # result in being unable to edit issues in invalid states.
       fvs_err_msgs = field_helpers.ValidateCustomFields(
@@ -1659,6 +1651,11 @@
     all_users.extend(field_users)
     AssertUsersExist(cnxn, services, all_users, err_agg)
 
+    label_validity_error = field_helpers.ValidateLabels(
+        cnxn, services, issue.project_id, issue.labels)
+    if label_validity_error:
+      err_agg.AddErrorMessage(label_validity_error)
+
     field_validity_errors = field_helpers.ValidateCustomFields(
         cnxn, services, issue.field_values, config, project, issue=issue)
     if field_validity_errors:
@@ -1695,7 +1692,10 @@
     if issue.owner_id:
       new_cc_ids.add(issue.owner_id)
 
-  return [cc_id for cc_id in new_cc_ids if cc_id not in merge_into_issue.cc_ids]
+  return [
+      cc_id for cc_id in sorted(new_cc_ids)
+      if cc_id not in merge_into_issue.cc_ids
+  ]
 
 
 def _EnforceNonMergeStatusDeltas(cnxn, issue_delta_pairs, services):
@@ -1745,9 +1745,11 @@
   def ComputeAllImpactedIIDs(self):
     # type: () -> Collection[int]
     """Computes the unique set of all impacted issue ids."""
-    return set(self.blocking_add.keys() + self.blocking_remove.keys() +
-               self.blocked_on_add.keys() + self.blocked_on_remove.keys() +
-               self.merged_from_add.keys() + self.merged_from_remove.keys())
+    return (
+        set(self.blocking_add.keys()) | set(self.blocking_remove.keys())
+        | set(self.blocked_on_add.keys()) | set(self.blocked_on_remove.keys())
+        | set(self.merged_from_add.keys())
+        | set(self.merged_from_remove.keys()))
 
   def TrackImpactedIssues(self, issue, delta):
     # type: (Issue, IssueDelta) -> None
diff --git a/tracker/tracker_views.py b/tracker/tracker_views.py
index c2687db..4a87249 100644
--- a/tracker/tracker_views.py
+++ b/tracker/tracker_views.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """View objects to help display tracker business objects in templates."""
 from __future__ import print_function
@@ -29,7 +28,7 @@
 from framework import template_helpers
 from framework import timestr
 from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import attachment_helpers
 from tracker import tracker_bizobj
 from tracker import tracker_constants
@@ -456,7 +455,7 @@
 
   # Make a phase field's view for each unique phase_name found in phases.
   (_, _, _, _, phases_by_name) = precomp_view_info
-  for phase_name in phases_by_name.keys():
+  for phase_name in sorted(phases_by_name.keys()):
     field_value_views.extend([
         _MakeFieldValueView(
             fd, config, precomp_view_info, users_by_id, phase_name=phase_name)
@@ -527,10 +526,10 @@
 
 def MakeBounceFieldValueViews(
     field_vals, phase_field_vals, config, applicable_fields=None):
-  # type: (Sequence[proto.tracker_pb2.FieldValue],
-  #     Sequence[proto.tracker_pb2.FieldValue],
-  #     proto.tracker_pb2.ProjectIssueConfig
-  #     Sequence[proto.tracker_pb2.FieldDef]) -> Sequence[FieldValueView]
+  # type: (Sequence[mrproto.tracker_pb2.FieldValue],
+  #     Sequence[mrproto.tracker_pb2.FieldValue],
+  #     mrproto.tracker_pb2.ProjectIssueConfig
+  #     Sequence[mrproto.tracker_pb2.FieldDef]) -> Sequence[FieldValueView]
   """Return a list of field values to display on a validation bounce page."""
   applicable_set = set()
   # Handle required fields
diff --git a/tracker/webcomponentspage.py b/tracker/webcomponentspage.py
index eadd983..e9afa70 100644
--- a/tracker/webcomponentspage.py
+++ b/tracker/webcomponentspage.py
@@ -1,7 +1,6 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2018 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Classes that implement a web components page.
 
@@ -16,11 +15,9 @@
 import logging
 
 import settings
-from framework import flaskservlet
-from framework import servlet
-from framework import framework_helpers
+from framework import exceptions
 from framework import permissions
-from framework import urls
+from framework import servlet
 
 
 class WebComponentsPage(servlet.Servlet):
@@ -57,21 +54,27 @@
         old_ui_url = '/u/%s/hotlists/%s' % (hotlist.owner_ids[0], hotlist.name)
 
     return {
-       'local_id': mr.local_id,
-       'old_ui_url': old_ui_url,
-      }
+        'local_id': mr.local_id,
+        'old_ui_url': old_ui_url,
+    }
 
-  # def GetWebComponentsIssueDetail(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetWebComponentsIssueDetail(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def GetWebComponentsIssueList(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetWebComponentsIssueList(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def GetWebComponentsIssueWizard(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetWebComponentsIssueWizard(self, **kwargs):
+    return self.handler(**kwargs)
 
-  # def GetWebComponentsIssueNewEntry(self, **kwargs):
-  #   return self.handler(**kwargs)
+  def GetWebComponentsIssueNewEntry(self, **kwargs):
+    return self.handler(**kwargs)
+
+  def GetWebComponentsHotlist(self, **kwargs):
+    return self.handler(**kwargs)
+
+  def GetWebComponentsUser(self, **kwargs):
+    return self.handler(**kwargs)
 
 
 class ProjectListPage(WebComponentsPage):
@@ -126,5 +129,7 @@
       return 'User cannot view default project: %r' % project
 
     project_url = '/p/%s' % project_name
-    self.redirect(project_url, abort=True)
-    return 'Redirected to %r' % project_url
+    raise exceptions.RedirectException(project_url)
+
+  def GetProjectListPage(self, **kwargs):
+    return self.handler(**kwargs)