Merge branch 'main' into avm99963-monorail

Merged commit 4137ed7879acadbf891e8c471108acb874dae886.

GitOrigin-RevId: b6100ffc5b1da355a35f37b13fcaaf746ee8b307
diff --git a/framework/servlet.py b/framework/servlet.py
index 1ed6935..e1c0cf1 100644
--- a/framework/servlet.py
+++ b/framework/servlet.py
@@ -90,17 +90,6 @@
 #         'cloudtrace', 'v1', credentials=credentials)
 #   except Exception as e:
 #     logging.warning('could not get trace service: %s', e)
-
-
-class MethodNotSupportedError(NotImplementedError):
-  """An exception class for indicating that the method is not supported.
-
-  Used by GatherPageData and ProcessFormData to indicate that GET and POST,
-  respectively, are not supported methods on the given Servlet.
-  """
-  pass
-
-
 class Servlet(webapp2.RequestHandler):
   """Base class for all Monorail servlets.
 
@@ -318,7 +307,7 @@
       csp_supports_report_sample = (
         (browser == 'Chrome' and browser_major_version >= 59) or
         (browser == 'Opera' and browser_major_version >= 46))
-      version_base = _VersionBaseURL(self.mr.request)
+      version_base = servlet_helpers.VersionBaseURL(self.mr.request)
       self.response.headers.add(csp_header,
            ("default-src %(scheme)s ; "
             "script-src"
@@ -355,7 +344,7 @@
       with self.mr.profiler.Phase('rendering template'):
         self._RenderResponse(page_data)
 
-    except (MethodNotSupportedError, NotImplementedError) as e:
+    except (servlet_helpers.MethodNotSupportedError, NotImplementedError) as e:
       # Instead of these pages throwing 500s display the 404 message and log.
       # The motivation of this is to minimize 500s on the site to keep alerts
       # meaningful during fuzzing. For more context see
@@ -374,7 +363,7 @@
       logging.warning('mr.perms is %s', self.mr.perms)
       if not self.mr.auth.user_id:
         # If not logged in, let them log in
-        url = _SafeCreateLoginURL(self.mr)
+        url = servlet_helpers.SafeCreateLoginURL(self.mr)
         self.redirect(url, abort=True)
       else:
         # Display the missing permissions template.
@@ -388,12 +377,6 @@
         self._missing_permissions_template.WriteResponse(
             self.response, page_data, content_type=self.content_type)
 
-  def SetCacheHeaders(self, response):
-    """Set headers to allow the response to be cached."""
-    headers = framework_helpers.StaticCacheHeaders()
-    for name, value in headers:
-      response.headers[name] = value
-
   def GetTemplate(self, _page_data):
     """Get the template to use for writing the http response.
 
@@ -437,7 +420,7 @@
     Returns:
       String URL to redirect the user to, or None if response was already sent.
     """
-    raise MethodNotSupportedError()
+    raise servlet_helpers.MethodNotSupportedError()
 
   def post(self, **kwargs):
     """Parse the request, check base perms, and call form-specific code."""
@@ -607,7 +590,7 @@
     project_thumbnail_url = ''
     if project:
       project_summary = project.summary
-      project_alert = _CalcProjectAlert(project)
+      project_alert = servlet_helpers.CalcProjectAlert(project)
       project_read_only = project.read_only_reason
       project_home_page = project.home_page
       project_thumbnail_url = tracker_views.LogoView(project).thumbnail_url
@@ -653,10 +636,10 @@
     offer_saved_queries_subtab = (
         viewing_self or mr.auth.user_pb and mr.auth.user_pb.is_site_admin)
 
-    login_url = _SafeCreateLoginURL(mr)
-    logout_url = _SafeCreateLogoutURL(mr)
+    login_url = servlet_helpers.SafeCreateLoginURL(mr)
+    logout_url = servlet_helpers.SafeCreateLogoutURL(mr)
     logout_url_goto_home = users.create_logout_url('/')
-    version_base = _VersionBaseURL(mr.request)
+    version_base = servlet_helpers.VersionBaseURL(mr.request)
 
     base_data = {
         # EZT does not have constants for True and False, so we pass them in.
@@ -697,7 +680,7 @@
         'project':
             project_view,
         'project_is_restricted':
-            ezt.boolean(_ProjectIsRestricted(mr)),
+            ezt.boolean(servlet_helpers.ProjectIsRestricted(mr)),
         'offer_contributor_list':
             ezt.boolean(permissions.CanViewContributorList(mr, mr.project)),
         'logged_in_user':
@@ -784,10 +767,13 @@
             mr.num,
         'groupby':
             mr.group_by_spec,
-        'q_field_size': (min(
-            framework_constants.MAX_ARTIFACT_SEARCH_FIELD_SIZE,
-            max(framework_constants.MIN_ARTIFACT_SEARCH_FIELD_SIZE,
-                len(mr.query) + framework_constants.AUTOSIZE_STEP))),
+        'q_field_size':
+            (
+                min(
+                    framework_constants.MAX_ARTIFACT_SEARCH_FIELD_SIZE,
+                    max(
+                        framework_constants.MIN_ARTIFACT_SEARCH_FIELD_SIZE,
+                        len(mr.query) + framework_constants.AUTOSIZE_STEP))),
         'mode':
             None,  # Display mode, e.g., grid mode.
         'ajah':
@@ -866,7 +852,7 @@
 
   def GatherPageData(self, mr):
     """Return a dict of page-specific ezt data."""
-    raise MethodNotSupportedError()
+    raise servlet_helpers.MethodNotSupportedError()
 
   # pylint: disable=unused-argument
   def GatherHelpData(self, mr, page_data):
@@ -907,7 +893,7 @@
   def GatherDebugData(self, mr, page_data):
     """Return debugging info for display at the very bottom of the page."""
     if mr.debug_enabled:
-      debug = [_ContextDebugCollection('Page data', page_data)]
+      debug = [servlet_helpers.ContextDebugCollection('Page data', page_data)]
       return {
           'dbg': 'on',
           'debug': debug,
@@ -939,109 +925,3 @@
           now - framework_constants.VISIT_RESOLUTION):
         user_pb.last_visit_timestamp = now
         self.services.user.UpdateUser(mr.cnxn, user_pb.user_id, user_pb)
-
-
-def _CalcProjectAlert(project):
-  """Return a string to be shown as red text explaning the project state."""
-
-  project_alert = None
-
-  if project.read_only_reason:
-    project_alert = 'READ-ONLY: %s.' % project.read_only_reason
-  if project.moved_to:
-    project_alert = 'This project has moved to: %s.' % project.moved_to
-  elif project.delete_time:
-    delay_seconds = project.delete_time - time.time()
-    delay_days = delay_seconds // framework_constants.SECS_PER_DAY
-    if delay_days <= 0:
-      project_alert = 'Scheduled for deletion today.'
-    else:
-      days_word = 'day' if delay_days == 1 else 'days'
-      project_alert = (
-          'Scheduled for deletion in %d %s.' % (delay_days, days_word))
-  elif project.state == project_pb2.ProjectState.ARCHIVED:
-    project_alert = 'Project is archived: read-only by members only.'
-
-  return project_alert
-
-
-class _ContextDebugItem(object):
-  """Wrapper class to generate on-screen debugging output."""
-
-  def __init__(self, key, val):
-    """Store the key and generate a string for the value."""
-    self.key = key
-    if isinstance(val, list):
-      nested_debug_strs = [self.StringRep(v) for v in val]
-      self.val = '[%s]' % ', '.join(nested_debug_strs)
-    else:
-      self.val = self.StringRep(val)
-
-  def StringRep(self, val):
-    """Make a useful string representation of the given value."""
-    try:
-      return val.DebugString()
-    except Exception:
-      try:
-        return str(val.__dict__)
-      except Exception:
-        return repr(val)
-
-
-class _ContextDebugCollection(object):
-  """Attach a title to a dictionary for exporting as a table of debug info."""
-
-  def __init__(self, title, collection):
-    self.title = title
-    self.collection = [_ContextDebugItem(key, collection[key])
-                       for key in sorted(collection.keys())]
-
-
-def _ProjectIsRestricted(mr):
-  """Return True if the mr has a 'private' project."""
-  return (mr.project and
-          mr.project.access != project_pb2.ProjectAccess.ANYONE)
-
-
-def _SafeCreateLoginURL(mr, continue_url=None):
-  """Make a login URL w/ a detailed continue URL, otherwise use a short one."""
-  continue_url = continue_url or mr.current_page_url
-  try:
-    url = users.create_login_url(continue_url)
-  except users.RedirectTooLongError:
-    if mr.project_name:
-      url = users.create_login_url('/p/%s' % mr.project_name)
-    else:
-      url = users.create_login_url('/')
-
-  # Give the user a choice of existing accounts in their session
-  # or the option to add an account, even if they are currently
-  # signed in to exactly one account.
-  if mr.auth.user_id:
-    # Notice: this makes assuptions about the output of users.create_login_url,
-    # which can change at any time. See https://crbug.com/monorail/3352.
-    url = url.replace('/ServiceLogin', '/AccountChooser', 1)
-  return url
-
-
-def _SafeCreateLogoutURL(mr):
-  """Make a logout URL w/ a detailed continue URL, otherwise use a short one."""
-  try:
-    return users.create_logout_url(mr.current_page_url)
-  except users.RedirectTooLongError:
-    if mr.project_name:
-      return users.create_logout_url('/p/%s' % mr.project_name)
-    else:
-      return users.create_logout_url('/')
-
-
-def _VersionBaseURL(request):
-  """Return a version-specific URL that we use to load static assets."""
-  if settings.local_mode:
-    version_base = '%s://%s' % (request.scheme, request.host)
-  else:
-    version_base = '%s://%s-dot-%s' % (
-      request.scheme, modules.get_current_version_name(),
-      app_identity.get_default_version_hostname())
-
-  return version_base