Merge branch 'main' into avm99963-monorail

Merged commit cd4b3b336f1f14afa02990fdc2eec5d9467a827e

GitOrigin-RevId: e67bbf185d5538e1472bb42e0abb2a141f88bac1
diff --git a/tracker/componentcreate.py b/tracker/componentcreate.py
index 9cb713c..9974879 100644
--- a/tracker/componentcreate.py
+++ b/tracker/componentcreate.py
@@ -11,6 +11,7 @@
 import logging
 import time
 
+from framework import flaskservlet
 from framework import framework_helpers
 from framework import framework_views
 from framework import jsonfeed
@@ -28,7 +29,7 @@
 class ComponentCreate(servlet.Servlet):
   """Servlet allowing project owners to create a component."""
 
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/component-create-page.ezt'
 
   def AssertBasePermission(self, mr):
@@ -136,6 +137,12 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.ADMIN_COMPONENTS, saved=1, ts=int(time.time()))
 
+  # def GetComponentCreatePage(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostComponentCreatePage(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 def LeafNameErrorMessage(parent_path, leaf_name, config):
   """Return an error message for the given component name, or None."""
diff --git a/tracker/fieldcreate.py b/tracker/fieldcreate.py
index ead72ad..b1f2316 100644
--- a/tracker/fieldcreate.py
+++ b/tracker/fieldcreate.py
@@ -15,6 +15,7 @@
 import ezt
 
 from framework import exceptions
+from framework import flaskservlet
 from framework import framework_helpers
 from framework import jsonfeed
 from framework import permissions
@@ -30,7 +31,7 @@
 class FieldCreate(servlet.Servlet):
   """Servlet allowing project owners to create a custom field."""
 
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/field-create-page.ezt'
 
   def AssertBasePermission(self, mr):
@@ -198,6 +199,12 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.ADMIN_LABELS, saved=1, ts=int(time.time()))
 
+  # def GetFieldCreate(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostFieldCreate(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 def FieldNameErrorMessage(field_name, config):
   """Return an error message for the given field name, or None."""
diff --git a/tracker/fielddetail.py b/tracker/fielddetail.py
index 3e7ebb3..76cf378 100644
--- a/tracker/fielddetail.py
+++ b/tracker/fielddetail.py
@@ -15,6 +15,7 @@
 import ezt
 
 from framework import exceptions
+from framework import flaskservlet
 from framework import framework_helpers
 from framework import framework_views
 from framework import permissions
@@ -30,7 +31,7 @@
 class FieldDetail(servlet.Servlet):
   """Servlet allowing project owners to view and edit a custom field."""
 
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/field-detail-page.ezt'
 
   def _GetFieldDef(self, mr):
@@ -247,3 +248,9 @@
     return framework_helpers.FormatAbsoluteURL(
           mr, urls.FIELD_DETAIL, field=field_def.field_name,
           saved=1, ts=int(time.time()))
+
+  # def GetFieldDetail(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 c26ab62..e42b432 100644
--- a/tracker/fltconversion.py
+++ b/tracker/fltconversion.py
@@ -134,6 +134,7 @@
     'phase_map, approvals_to_labels, labels_re')
 
 
+# TODO: change to FlaskInternalTask when convert to flask
 class FLTConvertTask(jsonfeed.InternalTask):
   """FLTConvert converts current Type=Launch issues into Type=FLT-Launch."""
 
@@ -530,6 +531,12 @@
     return tracker_bizobj.MakeFieldValue(
         field_id, None, None, user_id, None, None, False)
 
+  # def GetFLTConvertTask(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostFLTConvertTask(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 def ConvertMLabels(
     labels, phases, m_target_id, m_approved_id, labels_re, phase_map):
diff --git a/tracker/issueadmin.py b/tracker/issueadmin.py
index 5c34f72..10fbdc8 100644
--- a/tracker/issueadmin.py
+++ b/tracker/issueadmin.py
@@ -24,6 +24,7 @@
 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
@@ -43,7 +44,7 @@
 class IssueAdminBase(servlet.Servlet):
   """Base class for servlets allowing project owners to configure tracker."""
 
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
   _PROCESS_SUBTAB = None  # specified in subclasses
 
   def GatherPageData(self, mr):
@@ -90,7 +91,7 @@
   """Servlet allowing project owners to configure well-known statuses."""
 
   _PAGE_TEMPLATE = 'tracker/admin-statuses-page.ezt'
-  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_STATUSES
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_STATUSES
 
   def ProcessSubtabForm(self, post_data, mr):
     """Process the status definition section of the admin page.
@@ -141,12 +142,18 @@
 
     return urls.ADMIN_STATUSES
 
+  # def GetAdminStatusesPage(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 = servlet.Servlet.PROCESS_TAB_LABELS
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_LABELS
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -224,12 +231,18 @@
 
     return urls.ADMIN_LABELS
 
+  # def GetAdminLabelsPage(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 = servlet.Servlet.PROCESS_TAB_TEMPLATES
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -288,12 +301,18 @@
     return (GetSelectedTemplateID('default_template_for_developers'),
             GetSelectedTemplateID('default_template_for_users'))
 
+  # def GetAdminTemplatesPage(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 = servlet.Servlet.PROCESS_TAB_COMPONENTS
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_COMPONENTS
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -398,12 +417,18 @@
         failed_templ=','.join(templates_errors),
         deleted=','.join(deleted_components))
 
+  # def GetAdminComponentsPage(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 = servlet.Servlet.PROCESS_TAB_VIEWS
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_VIEWS
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page.
@@ -463,6 +488,12 @@
 
     return urls.ADMIN_VIEWS
 
+  # def GetAdminViewsPage(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostAdminViewsPage(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 def _ParseListPreferences(post_data):
   """Parse the part of a project admin form about artifact list preferences."""
@@ -509,7 +540,7 @@
   """Servlet allowing project owners to configure filter rules."""
 
   _PAGE_TEMPLATE = 'tracker/admin-rules-page.ezt'
-  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_RULES
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_RULES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -585,3 +616,9 @@
           mr.cnxn, self.services, mr.project, config)
 
     return urls.ADMIN_RULES
+
+  # def GetAdminRulesPage(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 d824098..f702763 100644
--- a/tracker/issueadvsearch.py
+++ b/tracker/issueadvsearch.py
@@ -17,6 +17,7 @@
 import re
 
 from features import savedqueries_helpers
+from framework import flaskservlet
 from framework import framework_helpers
 from framework import permissions
 from framework import servlet
@@ -31,7 +32,7 @@
   """IssueAdvancedSearch shows a form to enter an advanced search."""
 
   _PAGE_TEMPLATE = 'tracker/issue-advsearch-page.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
 
   # This form *only* redirects to a GET request, and permissions are checked
   # in that handler.
@@ -121,3 +122,9 @@
       values = VALUE_RE.findall(user_input)
       search_term = '%s%s' % (operator, ','.join(values))
       search_query.append(search_term)
+
+  # def GetIssueAdvSearchPage(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostIssueAdvSearchPage(self, **kwargs):
+  #   return self.handler(**kwargs)
\ No newline at end of file
diff --git a/tracker/issueattachment.py b/tracker/issueattachment.py
index d6fa978..26982d0 100644
--- a/tracker/issueattachment.py
+++ b/tracker/issueattachment.py
@@ -18,14 +18,13 @@
 import logging
 import os
 import re
-import urllib
-
-import webapp2
+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
@@ -54,20 +53,20 @@
     Returns: dict of values used by EZT for rendering the page.
     """
     if mr.signed_aid != attachment_helpers.SignAttachmentID(mr.aid):
-      webapp2.abort(400, 'Please reload the issue page')
+      self.abort(400, 'Please reload the issue page')
 
     try:
       attachment, _issue = tracker_helpers.GetAttachmentIfAllowed(
           mr, self.services)
     except exceptions.NoSuchIssueException:
-      webapp2.abort(404, 'issue not found')
+      self.abort(404, 'issue not found')
     except exceptions.NoSuchAttachmentException:
-      webapp2.abort(404, 'attachment not found')
+      self.abort(404, 'attachment not found')
     except exceptions.NoSuchCommentException:
-      webapp2.abort(404, 'comment not found')
+      self.abort(404, 'comment not found')
 
     if not attachment.gcs_object_id:
-      webapp2.abort(404, 'attachment data not found')
+      self.abort(404, 'attachment data not found')
 
     bucket_name = app_identity.get_default_gcs_bucket_name()
 
@@ -91,3 +90,6 @@
 
     url = gcs_helpers.SignUrl(bucket_name, gcs_object_id)
     self.redirect(url, abort=True)
+
+  # def GetAttachmentPage(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/issueattachmenttext.py b/tracker/issueattachmenttext.py
index d3daaf9..40db170 100644
--- a/tracker/issueattachmenttext.py
+++ b/tracker/issueattachmenttext.py
@@ -14,15 +14,13 @@
 
 import logging
 
-import webapp2
-
-from google.appengine.api import app_identity
-
-from third_party import cloudstorage
 import ezt
+from google.appengine.api import app_identity
+from google.cloud import storage
 
 from features import prettify
 from framework import exceptions
+from framework import flaskservlet
 from framework import filecontent
 from framework import permissions
 from framework import servlet
@@ -36,7 +34,7 @@
   """AttachmentText displays textual attachments much like source browsing."""
 
   _PAGE_TEMPLATE = 'tracker/issue-attachment-text.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
 
   def GatherPageData(self, mr):
     """Parse the attachment ID from the request and serve its content.
@@ -52,19 +50,27 @@
         attachment, issue = tracker_helpers.GetAttachmentIfAllowed(
             mr, self.services)
       except exceptions.NoSuchIssueException:
-        webapp2.abort(404, 'issue not found')
+        self.abort(404, 'issue not found')
       except exceptions.NoSuchAttachmentException:
-        webapp2.abort(404, 'attachment not found')
+        self.abort(404, 'attachment not found')
       except exceptions.NoSuchCommentException:
-        webapp2.abort(404, 'comment not found')
+        self.abort(404, 'comment not found')
 
-    content = []
+    content = b''
     if attachment.gcs_object_id:
       bucket_name = app_identity.get_default_gcs_bucket_name()
       full_path = '/' + bucket_name + attachment.gcs_object_id
       logging.info("reading gcs: %s" % full_path)
-      with cloudstorage.open(full_path, 'r') as f:
-        content = f.read()
+
+      # Strip leading slash from object ID for backwards compatibility.
+      blob_name = attachment.gcs_object_id
+      if blob_name.startswith('/'):
+        blob_name = blob_name[1:]
+
+      client = storage.Client()
+      bucket = client.get_bucket(bucket_name)
+      blob = bucket.get_blob(blob_name)
+      content = blob.download_as_bytes()
 
     filesize = len(content)
 
@@ -101,3 +107,6 @@
           len(lines), attachment.filename))
 
     return page_data
+
+  # def GetAttachmentText(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/issuebulkedit.py b/tracker/issuebulkedit.py
index c1f5229..3ea4d3f 100644
--- a/tracker/issuebulkedit.py
+++ b/tracker/issuebulkedit.py
@@ -14,7 +14,7 @@
 from __future__ import absolute_import
 
 import collections
-import httplib
+from six.moves import http_client
 import itertools
 import logging
 import time
@@ -24,6 +24,7 @@
 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
@@ -41,7 +42,7 @@
   """IssueBulkEdit lists multiple issues and allows an edit to all of them."""
 
   _PAGE_TEMPLATE = 'tracker/issue-bulk-edit-page.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
   _SECONDS_OVERHEAD = 4
   _SECONDS_PER_UPDATE = 0.12
   _SLOWNESS_THRESHOLD = 10
@@ -170,31 +171,41 @@
     """
     if not mr.local_id_list:
       logging.info('missing issue local IDs, probably tampered')
-      self.response.status = httplib.BAD_REQUEST
+      #TODO: switch when convert /p to flask
+      # self.response.status_code = http_client.BAD_REQUEST
+      self.response.status = 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')
-      self.response.status = httplib.BAD_REQUEST  # xxx should raise except
+      #TODO: switch when convert /p to flask
+      # self.response.status_code = http_client.BAD_REQUEST
+      self.response.status = 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')
-      self.response.status = httplib.BAD_REQUEST
+      #TODO: switch when convert /p to flask
+      # self.response.status_code = http_client.BAD_REQUEST
+      self.response.status = http_client.BAD_REQUEST
       return
 
     if not self.CheckPerm(mr, permissions.EDIT_ISSUE):
       logging.info('user has no permission to edit issue metadata')
-      self.response.status = httplib.BAD_REQUEST
+      #TODO: switch when convert /p to flask
+      # self.response.status_code = http_client.BAD_REQUEST
+      self.response.status = 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')
-      self.response.status = httplib.BAD_REQUEST
+      #TODO: switch when convert /p to flask
+      # self.response.status_code = http_client.BAD_REQUEST
+      self.response.status = http_client.BAD_REQUEST
       return
 
     config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
@@ -471,3 +482,9 @@
     # TODO(jrobbins): implement bulk=N param for a better confirmation alert.
     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 PostIssueBulkEdit(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/issuedetailezt.py b/tracker/issuedetailezt.py
index 9460669..3a10443 100644
--- a/tracker/issuedetailezt.py
+++ b/tracker/issuedetailezt.py
@@ -14,7 +14,6 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import httplib
 import json
 import logging
 import time
@@ -28,6 +27,7 @@
 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
@@ -159,14 +159,23 @@
 class FlipperNext(FlipperRedirectBase):
   next_handler = True
 
+  # 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)
+
 
 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):
     with work_env.WorkEnv(self.mr, self.services) as we:
       hotlist_id = self.mr.GetIntParam('hotlist_id')
@@ -190,7 +199,11 @@
                                                hotlist, self.services)
     self.redirect(url)
 
+  # 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.
 
@@ -261,6 +274,12 @@
       'total_count': total_count,
     }
 
+  # def GetFlipperIndex(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostFlipperIndex(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 def _ShouldShowFlipper(mr, services):
   """Return True if we should show the flipper."""
diff --git a/tracker/issueentry.py b/tracker/issueentry.py
index 77de114..2ae59d8 100644
--- a/tracker/issueentry.py
+++ b/tracker/issueentry.py
@@ -18,6 +18,7 @@
 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
@@ -45,7 +46,7 @@
   """IssueEntry shows a page with a simple form to enter a new issue."""
 
   _PAGE_TEMPLATE = 'tracker/issue-entry-page.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.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
@@ -511,6 +512,12 @@
 
     return template
 
+  # def GetIssueEntry(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostIssueEntry(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 def _AttachDefaultApprovers(config, approval_values):
   approval_defs_by_id = {ad.approval_id: ad for ad in config.approval_defs}
diff --git a/tracker/issueentryafterlogin.py b/tracker/issueentryafterlogin.py
index d25a7c1..3008d54 100644
--- a/tracker/issueentryafterlogin.py
+++ b/tracker/issueentryafterlogin.py
@@ -11,6 +11,7 @@
 
 import logging
 
+from framework import flaskservlet
 from framework import servlet
 from framework import servlet_helpers
 
@@ -25,8 +26,9 @@
     if not mr.auth.user_id:
       self.abort(400, 'Only signed-in users should reach this URL.')
 
-    with mr.profiler.Phase('getting config'):
-      config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
-    entry_page_url = servlet_helpers.ComputeIssueEntryURL(mr, config)
+    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)
diff --git a/tracker/issueexport.py b/tracker/issueexport.py
index a457a17..91620c4 100644
--- a/tracker/issueexport.py
+++ b/tracker/issueexport.py
@@ -16,6 +16,7 @@
 
 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 servlet
@@ -26,7 +27,7 @@
   """IssueExportControls let's an admin choose how to export issues."""
 
   _PAGE_TEMPLATE = 'tracker/issue-export-page.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
 
   def AssertBasePermission(self, mr):
     """Make sure that the logged in user has permission to view this page."""
@@ -69,7 +70,11 @@
         'saved_queries': saved_query_views,
     }
 
+  # 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."""
 
@@ -277,3 +282,9 @@
       if merge.project_id == mr.project.project_id:
         issue_json['merged_into'] = merge.local_id
     return issue_json
+
+  # def GetIssueExportJSON(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 1e0289b..bd54db9 100644
--- a/tracker/issueimport.py
+++ b/tracker/issueimport.py
@@ -17,6 +17,7 @@
 import ezt
 
 from features import filterrules_helpers
+from framework import flaskservlet
 from framework import framework_helpers
 from framework import jsonfeed
 from framework import permissions
@@ -35,7 +36,7 @@
   """IssueImport loads a file of issues in JSON format."""
 
   _PAGE_TEMPLATE = 'tracker/issue-import-page.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
 
   def AssertBasePermission(self, mr):
     """Make sure that the logged in user has permission to view this page."""
@@ -304,6 +305,12 @@
     self.services.issue.SetUsedLocalID(cnxn, project_id)
     event_log.append('Finished import')
 
+  # def GetIssueImport(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostIssueImport(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 class JSONImportError(Exception):
   """Exception to raise if imported JSON is invalid."""
diff --git a/tracker/issueoriginal.py b/tracker/issueoriginal.py
index 55a494f..cbab3b1 100644
--- a/tracker/issueoriginal.py
+++ b/tracker/issueoriginal.py
@@ -17,6 +17,7 @@
 import ezt
 
 from businesslogic import work_env
+from framework import flaskservlet
 from framework import filecontent
 from framework import permissions
 from framework import servlet
@@ -96,3 +97,6 @@
       self.abort(404, 'comment not found')
 
     return issue, comment
+
+  # def GetIssueOriginal(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/issuereindex.py b/tracker/issuereindex.py
index de5d2f0..71acfe8 100644
--- a/tracker/issuereindex.py
+++ b/tracker/issuereindex.py
@@ -9,9 +9,10 @@
 from __future__ import absolute_import
 
 import logging
-import urllib
+from six.moves import urllib
 
 import settings
+from framework import flaskservlet
 from framework import permissions
 from framework import servlet
 from framework import urls
@@ -22,7 +23,7 @@
   """IssueReindex shows a form to request that issues be indexed."""
 
   _PAGE_TEMPLATE = 'tracker/issue-reindex-page.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -83,5 +84,11 @@
       'num': num,
       'auto_submit': bool(auto_submit),
     }
-    return '/p/%s%s?%s' % (mr.project_name, urls.ISSUE_REINDEX,
-                           urllib.urlencode(query_map))
+    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 PostIssueReindex(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/issuetips.py b/tracker/issuetips.py
index eb75265..f85bcd6 100644
--- a/tracker/issuetips.py
+++ b/tracker/issuetips.py
@@ -10,6 +10,7 @@
 
 import logging
 
+from framework import flaskservlet
 from framework import servlet
 from framework import permissions
 
@@ -18,7 +19,7 @@
   """IssueSearchTips on-line help on how to use issue search."""
 
   _PAGE_TEMPLATE = 'tracker/issue-search-tips.ezt'
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_ISSUES
 
   def GatherPageData(self, mr):
     """Build up a dictionary of data values to use when rendering the page."""
@@ -27,3 +28,6 @@
         'issue_tab_mode': 'issueSearchTips',
         'page_perms': self.MakePagePerms(mr, None, permissions.CREATE_ISSUE),
     }
+
+  # def GetIssueSearchTips(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/spam.py b/tracker/spam.py
deleted file mode 100644
index a30fc3e..0000000
--- a/tracker/spam.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# 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
-
-"""Classes that implement spam flagging features.
-"""
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-
-import httplib
-import logging
-
-from framework import framework_helpers
-from framework import paginate
-from framework import permissions
-from framework import urls
-from framework import servlet
-from framework import template_helpers
-from framework import xsrf
-from tracker import spam_helpers
-from tracker import tracker_bizobj
-
-
-class ModerationQueue(servlet.Servlet):
-  _PAGE_TEMPLATE = 'tracker/spam-moderation-queue.ezt'
-
-  def GatherPageData(self, mr):
-    if not self.CheckPerm(mr, permissions.MODERATE_SPAM):
-      raise permissions.PermissionException()
-
-    page_perms = self.MakePagePerms(
-        mr, None, permissions.MODERATE_SPAM,
-        permissions.EDIT_ISSUE, permissions.CREATE_ISSUE,
-        permissions.SET_STAR)
-
-    # TODO(seanmccullough): Figure out how to get the IssueFlagQueue either
-    # integrated into this page data, or on its own subtab of spam moderation.
-    # Also figure out the same for Comments.
-    issue_items, total_count = self.services.spam.GetIssueClassifierQueue(
-        mr.cnxn, self.services.issue, mr.project.project_id, mr.start, mr.num)
-
-    issue_queue = spam_helpers.DecorateIssueClassifierQueue(mr.cnxn,
-        self.services.issue, self.services.spam, self.services.user,
-        issue_items)
-
-    url_params = [(name, mr.GetParam(name)) for name in
-                  framework_helpers.RECOGNIZED_PARAMS]
-    p = paginate.ArtifactPagination(
-        [], mr.num, mr.GetPositiveIntParam('start'),
-        mr.project_name, urls.SPAM_MODERATION_QUEUE, total_count=total_count,
-        url_params=url_params)
-
-    return {
-        'issue_queue': issue_queue,
-        'projectname': mr.project.project_name,
-        'pagination': p,
-        'page_perms': page_perms,
-    }
diff --git a/tracker/spam_helpers.py b/tracker/spam_helpers.py
deleted file mode 100644
index 2bf2c90..0000000
--- a/tracker/spam_helpers.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# 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
-
-"""Set of helpers for constructing spam-related pages.
-"""
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from framework import template_helpers
-import ezt
-
-from datetime import datetime
-
-def DecorateIssueClassifierQueue(
-    cnxn, issue_service, spam_service, user_service, moderation_items):
-  issue_ids = [item.issue_id for item in moderation_items]
-  issues = issue_service.GetIssues(cnxn, issue_ids)
-  issue_map = {}
-  for issue in issues:
-    issue_map[issue.issue_id] = issue
-
-  flag_counts = spam_service.LookupIssueFlagCounts(cnxn, issue_ids)
-
-  reporter_ids = [issue.reporter_id for issue in issues]
-  reporters = user_service.GetUsersByIDs(cnxn, reporter_ids)
-  comments = issue_service.GetCommentsForIssues(cnxn, issue_ids)
-
-  items = []
-  for item in moderation_items:
-    issue=issue_map[item.issue_id]
-    first_comment = comments.get(item.issue_id, ["[Empty]"])[0]
-
-    items.append(template_helpers.EZTItem(
-        issue=issue,
-        summary=template_helpers.FitUnsafeText(issue.summary, 80),
-        comment_text=template_helpers.FitUnsafeText(first_comment.content, 80),
-        reporter=reporters[issue.reporter_id],
-        flag_count=flag_counts.get(issue.issue_id, 0),
-        is_spam=ezt.boolean(item.is_spam),
-        verdict_time=item.verdict_time,
-        classifier_confidence=item.classifier_confidence,
-        reason=item.reason,
-    ))
-
-  return items
diff --git a/tracker/template_helpers.py b/tracker/template_helpers.py
index c567b4a..1f15bcc 100644
--- a/tracker/template_helpers.py
+++ b/tracker/template_helpers.py
@@ -47,6 +47,8 @@
   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')
   field_val_strs = collections.defaultdict(list)
   for fd in config.field_defs:
diff --git a/tracker/templatecreate.py b/tracker/templatecreate.py
index e10a25b..139c4f5 100644
--- a/tracker/templatecreate.py
+++ b/tracker/templatecreate.py
@@ -15,6 +15,7 @@
 import ezt
 
 from framework import authdata
+from framework import flaskservlet
 from framework import framework_bizobj
 from framework import framework_helpers
 from framework import servlet
@@ -32,9 +33,9 @@
 class TemplateCreate(servlet.Servlet):
   """Servlet allowing project owners to create an issue template."""
 
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/template-detail-page.ezt'
-  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -191,3 +192,9 @@
 
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.ADMIN_TEMPLATES, saved=1, ts=int(time.time()))
+
+  # def GetTemplateCreate(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 cd48a80..a3386b4 100644
--- a/tracker/templatedetail.py
+++ b/tracker/templatedetail.py
@@ -15,6 +15,7 @@
 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
@@ -33,9 +34,9 @@
 class TemplateDetail(servlet.Servlet):
   """Servlet allowing project owners to edit/delete an issue template"""
 
-  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+  _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
   _PAGE_TEMPLATE = 'tracker/template-detail-page.ezt'
-  _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES
+  _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
 
   def AssertBasePermission(self, mr):
     """Check whether the user has any permission to visit this page.
@@ -243,3 +244,9 @@
     return framework_helpers.FormatAbsoluteURL(
         mr, urls.TEMPLATE_DETAIL, template=template.name,
         saved=1, ts=int(time.time()))
+
+  # def GetTemplateDetail(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def PostTemplateDetail(self, **kwargs):
+  #   return self.handler(**kwargs)
diff --git a/tracker/test/issueattachment_test.py b/tracker/test/issueattachment_test.py
index 8c65014..9fccba5 100644
--- a/tracker/test/issueattachment_test.py
+++ b/tracker/test/issueattachment_test.py
@@ -10,7 +10,6 @@
 
 import unittest
 
-from google.appengine.api import images
 from google.appengine.ext import testbed
 
 import mox
@@ -18,16 +17,12 @@
 
 from framework import gcs_helpers
 from framework import permissions
-from framework import servlet
 from proto import tracker_pb2
 from services import service_manager
 from testing import fake
 from testing import testing_helpers
 from tracker import attachment_helpers
 from tracker import issueattachment
-from tracker import tracker_helpers
-
-from third_party import cloudstorage
 
 
 class IssueattachmentTest(unittest.TestCase):
@@ -41,9 +36,6 @@
     self.testbed.init_urlfetch_stub()
     self.attachment_data = ""
 
-    self._old_gcs_open = cloudstorage.open
-    cloudstorage.open = fake.gcs_open
-
     services = service_manager.Services(
         project=fake.ProjectService(),
         config=fake.ConfigService(),
@@ -74,7 +66,6 @@
     self.mox.UnsetStubs()
     self.mox.ResetAll()
     self.testbed.deactivate()
-    cloudstorage.open = self._old_gcs_open
     attachment_helpers.SignAttachmentID = self.orig_sign_attachment_id
 
   def testGatherPageData_NotFound(self):
diff --git a/tracker/test/issueattachmenttext_test.py b/tracker/test/issueattachmenttext_test.py
index 187aa42..f7dda8d 100644
--- a/tracker/test/issueattachmenttext_test.py
+++ b/tracker/test/issueattachmenttext_test.py
@@ -8,18 +8,15 @@
 from __future__ import division
 from __future__ import absolute_import
 
-import logging
+import mock
 import unittest
-from mock import patch
 
-from google.appengine.ext import testbed
-
-from third_party import cloudstorage
 import ezt
+from google.appengine.ext import testbed
+from google.cloud import storage
 
 import webapp2
 
-from framework import filecontent
 from framework import permissions
 from proto import tracker_pb2
 from services import service_manager
@@ -85,12 +82,18 @@
     services.issue.TestAddAttachment(
         self.attach1, self.comment1.id, self.issue.issue_id)
     # TODO(jrobbins): add tests for binary content
-    self._old_gcs_open = cloudstorage.open
-    cloudstorage.open = fake.gcs_open
+
+    self.client = mock.MagicMock()
+    self.bucket = mock.MagicMock()
+    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()
+    mock.patch.object(storage, 'Client', return_value=self.client).start()
 
   def tearDown(self):
     self.testbed.deactivate()
-    cloudstorage.open = self._old_gcs_open
+    mock.patch.stopall()
 
   def testGatherPageData_CommentDeleted(self):
     """If the attachment's comment was deleted, give a 403."""
@@ -155,6 +158,9 @@
     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')
+
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project,
         path='/p/proj/issues/attachmentText?id=1&aid=1234',
@@ -175,7 +181,7 @@
 
     self.assertEqual(None, page_data['code_reviews'])
 
-  @patch('framework.filecontent.DecodeFileContents')
+  @mock.patch('framework.filecontent.DecodeFileContents')
   def testGatherPageData_HugeFile(self, mock_DecodeFileContents):
     _request, mr = testing_helpers.GetRequestObjects(
         project=self.project,
diff --git a/tracker/tracker_helpers.py b/tracker/tracker_helpers.py
index c9f9e5a..cd9acfa 100644
--- a/tracker/tracker_helpers.py
+++ b/tracker/tracker_helpers.py
@@ -17,7 +17,7 @@
 import logging
 import re
 import time
-import urllib
+from six.moves import urllib
 
 from google.appengine.api import app_identity
 
@@ -151,8 +151,10 @@
   comment = post_data.get('comment', '')
   is_description = bool(post_data.get('description', ''))
   status = post_data.get('status', '')
-  template_name = urllib.unquote_plus(post_data.get('template_name', ''))
+  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')
 
   if is_description:
@@ -257,6 +259,8 @@
   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]
       if val_strs:
         try:
@@ -327,6 +331,8 @@
   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')
   return [int(aid) for aid in kept_attachments]
 
@@ -616,8 +622,10 @@
     url = urls.ISSUE_LIST
     kwargs['projects'] = ','.join(sorted(project_names))
 
-  param_strings = ['%s=%s' % (k, urllib.quote((u'%s' % v).encode('utf-8')))
-                   for k, v in kwargs.items()]
+  param_strings = [
+      '%s=%s' % (k, urllib.parse.quote((u'%s' % v).encode('utf-8')))
+      for k, v in kwargs.items()
+  ]
   if param_strings:
     url += '?' + '&'.join(sorted(param_strings))
   if absolute:
diff --git a/tracker/tracker_views.py b/tracker/tracker_views.py
index 0c54555..c2687db 100644
--- a/tracker/tracker_views.py
+++ b/tracker/tracker_views.py
@@ -12,7 +12,7 @@
 import logging
 import re
 import time
-import urllib
+from six.moves import urllib
 
 from google.appengine.api import app_identity
 import ezt
@@ -252,9 +252,12 @@
     self.thumbnail_url = gcs_helpers.SignUrl(bucket_name,
         gcs_object + '-thumbnail')
     self.viewurl = (
-        gcs_helpers.SignUrl(bucket_name, gcs_object) + '&' + urllib.urlencode(
-            {'response-content-displacement':
-                ('attachment; filename=%s' % self.filename)}))
+        gcs_helpers.SignUrl(bucket_name, gcs_object) + '&' +
+        urllib.parse.urlencode(
+            {
+                'response-content-displacement':
+                    ('attachment; filename=%s' % self.filename)
+            }))
 
 
 class AttachmentView(template_helpers.PBProxy):
diff --git a/tracker/webcomponentspage.py b/tracker/webcomponentspage.py
index 4e2ad0d..eadd983 100644
--- a/tracker/webcomponentspage.py
+++ b/tracker/webcomponentspage.py
@@ -16,6 +16,7 @@
 import logging
 
 import settings
+from framework import flaskservlet
 from framework import servlet
 from framework import framework_helpers
 from framework import permissions
@@ -60,6 +61,18 @@
        'old_ui_url': old_ui_url,
       }
 
+  # def GetWebComponentsIssueDetail(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def GetWebComponentsIssueList(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def GetWebComponentsIssueWizard(self, **kwargs):
+  #   return self.handler(**kwargs)
+
+  # def GetWebComponentsIssueNewEntry(self, **kwargs):
+  #   return self.handler(**kwargs)
+
 
 class ProjectListPage(WebComponentsPage):