Merge branch 'main' into avm99963-monorail

Merged commit 4137ed7879acadbf891e8c471108acb874dae886.

GitOrigin-RevId: b6100ffc5b1da355a35f37b13fcaaf746ee8b307
diff --git a/framework/servlet_helpers.py b/framework/servlet_helpers.py
index 68eb0c4..89fe587 100644
--- a/framework/servlet_helpers.py
+++ b/framework/servlet_helpers.py
@@ -8,28 +8,83 @@
 from __future__ import division
 from __future__ import absolute_import
 
+import settings
 import calendar
 import datetime
 import logging
 import urllib
+import time
 
+from framework import framework_constants
 from framework import framework_bizobj
 from framework import framework_helpers
 from framework import permissions
 from framework import template_helpers
 from framework import urls
 from framework import xsrf
+from proto import project_pb2
+
+from google.appengine.api import app_identity
+from google.appengine.api import modules
+from google.appengine.api import users
 
 _ZERO = datetime.timedelta(0)
 
+
+class MethodNotSupportedError(NotImplementedError):
+  """An exception class for indicating that the method is not supported.
+
+  Used by GatherPageData and ProcessFormData in Servlet.
+  """
+  pass
+
+
+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())
+    ]
+
+
 class _UTCTimeZone(datetime.tzinfo):
-    """UTC"""
-    def utcoffset(self, _dt):
-        return _ZERO
-    def tzname(self, _dt):
-        return "UTC"
-    def dst(self, _dt):
-        return _ZERO
+  """UTC"""
+
+  def utcoffset(self, _dt):
+    return _ZERO
+
+  def tzname(self, _dt):
+    return "UTC"
+
+  def dst(self, _dt):
+    return _ZERO
+
 
 _UTC = _UTCTimeZone()
 
@@ -132,21 +187,11 @@
     case. Otherewise it will be a fully qualified URL that includes some
     query string parameters.
   """
+  # TODO: remove the custom_issue_entry_url since its no longer
   if not config.custom_issue_entry_url:
     return '/p/%s/issues/entry' % (mr.project_name)
 
-  base_url = config.custom_issue_entry_url
-  sep = '&' if '?' in base_url else '?'
-  token = xsrf.GenerateToken(
-    mr.auth.user_id, '/p/%s%s%s' % (mr.project_name, urls.ISSUE_ENTRY, '.do'))
-  role_name = framework_helpers.GetRoleName(mr.auth.effective_ids, mr.project)
-
-  continue_url = urllib.quote(framework_helpers.FormatAbsoluteURL(
-      mr, urls.ISSUE_ENTRY + '.do'))
-
-  return '%s%stoken=%s&role=%s&continue=%s' % (
-      base_url, sep, urllib.quote(token),
-      urllib.quote(role_name or ''), continue_url)
+  return '/p/chromium/issues/wizard'
 
 
 def IssueListURL(mr, config, query_string=None):
@@ -158,3 +203,76 @@
     if config and config.member_default_query:
       url += '?q=' + urllib.quote_plus(config.member_default_query)
   return url
+
+
+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
+
+
+def CalcProjectAlert(project):
+  """Return a string to be shown as red text explaining 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