launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #10778
[Merge] lp:~wgrant/launchpad/noro into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/noro into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #978768 in Launchpad itself: "read-only mode code is now dead code"
https://bugs.launchpad.net/launchpad/+bug/978768
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/noro/+merge/118867
This branch rips out all the non-config-related read-only mode code, since fastdowntime has rendered read-only mode obsolete. Pretty simple.
--
https://code.launchpad.net/~wgrant/launchpad/noro/+merge/118867
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/noro into lp:launchpad.
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2012-05-16 21:40:42 +0000
+++ lib/lp/app/browser/configure.zcml 2012-08-09 04:24:19 +0000
@@ -301,6 +301,7 @@
name="index.html"
permission="zope.Public"
class="lp.services.webapp.login.UnauthorizedView"
+ template="../templates/launchpad-forbidden.pt"
attribute="__call__"
/>
@@ -421,15 +422,6 @@
class="lp.services.webapp.error.TranslationUnavailableView"
/>
- <!-- ReadOnlyModeViolation -->
- <browser:page
- for="lp.services.webapp.interfaces.ReadOnlyModeViolation"
- name="index.html"
- permission="zope.Public"
- template="../templates/launchpad-readonlyfailure.pt"
- class="lp.services.webapp.error.ReadOnlyErrorView"
- />
-
<!-- Vocabularies -->
<browser:page
for="*"
=== removed file 'lib/lp/app/stories/basics/xx-read-only-mode.txt'
--- lib/lp/app/stories/basics/xx-read-only-mode.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/app/stories/basics/xx-read-only-mode.txt 1970-01-01 00:00:00 +0000
@@ -1,150 +0,0 @@
-= Read-Only Mode =
-
-During upgrades, Launchpad can be put into read-only mode using a config
-file switch. When in read-only mode, queries can be made to the slave
-database but attempts to access the master database or make database
-changes fail, returning an error page to the user.
-
- >>> from lp.services.database.tests.readonly import (
- ... touch_read_only_file, remove_read_only_file)
- >>> touch_read_only_file()
-
-
-== Notification of read-only mode. ==
-
-Users are warned when Launchpad is running in read-only mode.
-
- >>> user_browser.open('http://launchpad.dev')
- >>> print extract_text(first_tag_by_class(
- ... user_browser.contents, 'warning message'))
- Launchpad is undergoing maintenance
- ...
-
-Anonymous users are also warned, or they might try to signup.
-
- >>> anon_browser.open("http://launchpad.dev/~name16")
- >>> print extract_text(first_tag_by_class(
- ... anon_browser.contents, 'warning message'))
- Launchpad is undergoing maintenance
- ...
-
-There is no warning when Launchpad is running normally.
-
- >>> remove_read_only_file()
- >>> user_browser.open('http://launchpad.dev')
- >>> print first_tag_by_class(
- ... user_browser.contents, 'warning message')
- None
-
-== Operations requiring write permissions fail ==
-
-In read-only mode, all requests for non-read permissions are denied.
-This causes edit buttons and similar to not be displayed.
-
- >>> touch_read_only_file()
- >>> user_browser.open('http://launchpad.dev/people/+me')
- >>> user_browser.getLink('Change details')
- Traceback (most recent call last):
- ...
- LinkNotFoundError
-
-Even if the user manages to follow a link to a form, such as clicking
-on a link rendered before read-only mode was switched on or someone
-forgetting to properly protect the edit buttons, the form is replaced
-with a nice 503 error page informing the user what is going on.
-
- >>> remove_read_only_file()
- >>> user_browser.open('http://launchpad.dev/people/+me')
- >>> edit_link = user_browser.getLink('Change details')
- >>> edit_link is None
- False
- >>> # XXX StuartBishop 20090423 bug=365378: raiseHttpErrors is broken,
- >>> # requiring the try/except dance.
- >>> user_browser.handleErrors = True
- >>> user_browser.raiseHttpErrors = False
- >>> touch_read_only_file()
- >>> try:
- ... edit_link.click()
- ... except:
- ... pass
- >>> print user_browser.headers.get('Status')
- 503 Service Unavailable
- >>> print extract_text(first_tag_by_class(
- ... user_browser.contents, 'exception'))
- Sorry, you can't do this right now
-
-And even if a user manages to get a form and submit it, they get the
-same 503 error page.
-
- >>> remove_read_only_file()
- >>> user_browser.handleErrors = True
- >>> user_browser.open('http://launchpad.dev/people/+me')
- >>> user_browser.getLink('Change details').click()
- >>> user_browser.getControl(name='field.displayname').value = 'Different'
- >>> user_browser.raiseHttpErrors = False
- >>> # XXX StuartBishop 20090423 bug=365378: raiseHttpErrors is broken,
- >>> # requiring the try/except dance.
- >>> touch_read_only_file()
- >>> try:
- ... user_browser.getControl('Save Changes').click()
- ... except:
- ... pass
- >>> print user_browser.headers.get('Status')
- 503 Service Unavailable
- >>> print extract_text(first_tag_by_class(
- ... user_browser.contents, 'exception'))
- Sorry, you can't do this right now
-
-There are actually two exceptions that might trigger this error page.
-
- * Legacy code may trigger the ReadOnlyModeViolation exception by
- attempting to write to an object retrieved from the default Store.
-
- * Code may trigger the ReadOnlyModeDisallowedStore exception by
- requesting a master Store.
-
-Unfortunately it is difficult to ensure the same exception will
-continue to be raised by the above test. Instead, we confirm that both
-exceptions are rendered using the same view ensuring that the observed
-behavior is the same.
-
- >>> from zope.app import zapi
- >>> from lp.services.webapp.interfaces import (
- ... ReadOnlyModeDisallowedStore, ReadOnlyModeViolation)
- >>> from lp.services.webapp.servers import LaunchpadTestRequest
- >>> request = LaunchpadTestRequest()
- >>> view_name = zapi.queryDefaultViewName(
- ... ReadOnlyModeDisallowedStore, request)
- >>> view_name is not None
- True
- >>> disallowed_view = zapi.queryMultiAdapter(
- ... (ReadOnlyModeDisallowedStore, request), name=view_name)
-
- >>> view_name = zapi.queryDefaultViewName(
- ... ReadOnlyModeViolation, request)
- >>> view_name is not None
- True
- >>> violation_view = zapi.queryMultiAdapter(
- ... (ReadOnlyModeViolation, request), name=view_name)
-
- >>> violation_view == disallowed_view
- True
-
-
-== Read-only pages ==
-
-Most Launchpad pages (the ones that don't handle edit forms) can be
-accessed in read-only mode. Here are some examples.
-
-=== Bug page ===
-
- >>> user_browser.open('http://launchpad.dev/bugs/5')
- >>> print user_browser.title
- Bug #5...
- >>> print user_browser.headers['status']
- 200 Ok
-
-
-== Cleanup ==
-
- >>> remove_read_only_file()
=== modified file 'lib/lp/services/config/__init__.py'
--- lib/lp/services/config/__init__.py 2012-06-29 08:40:05 +0000
+++ lib/lp/services/config/__init__.py 2012-08-09 04:24:19 +0000
@@ -433,23 +433,11 @@
@property
def main_master(self):
- # Its a bit silly having ro_main_master and rw_main_master.
- # rw_main_master will never be used, as read-only-mode will
- # fail attempts to access the master database with a
- # ReadOnlyModeDisallowedStore exception.
- from lp.services.database.readonly import is_read_only
- if is_read_only():
- return self.ro_main_master
- else:
- return self.rw_main_master
+ return self.rw_main_master
@property
def main_slave(self):
- from lp.services.database.readonly import is_read_only
- if is_read_only():
- return self.ro_main_slave
- else:
- return self.rw_main_slave
+ return self.rw_main_slave
def override(self, **kwargs):
"""Override one or more config attributes.
=== modified file 'lib/lp/services/config/tests/test_database_config.py'
--- lib/lp/services/config/tests/test_database_config.py 2012-04-06 17:28:25 +0000
+++ lib/lp/services/config/tests/test_database_config.py 2012-08-09 04:24:19 +0000
@@ -4,11 +4,6 @@
__metaclass__ = type
from lp.services.config import DatabaseConfig
-from lp.services.database.readonly import read_only_file_exists
-from lp.services.database.tests.readonly import (
- remove_read_only_file,
- touch_read_only_file,
- )
from lp.testing import TestCase
from lp.testing.layers import DatabaseLayer
@@ -46,23 +41,3 @@
self.assertEqual('not_launchpad', dbc.dbuser)
dbc.reset()
self.assertEqual('launchpad_main', dbc.dbuser)
-
- def test_main_master_and_main_slave(self):
- # DatabaseConfig provides two extra properties: main_master and
- # main_slave, which return the value of either
- # rw_main_master/rw_main_slave or ro_main_master/ro_main_slave,
- # depending on whether or not we're in read-only mode.
- dbc = DatabaseConfig()
- self.assertFalse(read_only_file_exists())
- self.assertEquals(dbc.rw_main_master, dbc.main_master)
- self.assertEquals(dbc.rw_main_slave, dbc.main_slave)
-
- touch_read_only_file()
- try:
- self.assertTrue(read_only_file_exists())
- self.assertEquals(
- dbc.ro_main_master, dbc.main_master)
- self.assertEquals(
- dbc.ro_main_slave, dbc.main_slave)
- finally:
- remove_read_only_file()
=== modified file 'lib/lp/services/database/doc/db-policy.txt'
--- lib/lp/services/database/doc/db-policy.txt 2012-02-02 10:26:54 +0000
+++ lib/lp/services/database/doc/db-policy.txt 2012-08-09 04:24:19 +0000
@@ -123,30 +123,3 @@
>>> from lp.services.database.lpstorm import IMasterObject
>>> IMasterObject(ro_janitor) is writable_janitor
True
-
-Read-Only Mode
---------------
-
-During database outages, we run in read-only mode. In this mode, no
-matter what database policy is currently installed, explicit requests
-for a master store fail and the default store is always the slave.
-
- >>> from lp.services.database.tests.readonly import read_only_mode
- >>> from lp.services.webapp.dbpolicy import MasterDatabasePolicy
- >>> from contextlib import nested
-
- >>> with nested(read_only_mode(), MasterDatabasePolicy()):
- ... default_store = IStore(Person)
- ... IMasterStore.providedBy(default_store)
- False
-
- >>> with nested(read_only_mode(), MasterDatabasePolicy()):
- ... slave_store = ISlaveStore(Person)
- ... IMasterStore.providedBy(slave_store)
- False
-
- >>> with nested(read_only_mode(), MasterDatabasePolicy()):
- ... master_store = IMasterStore(Person)
- Traceback (most recent call last):
- ...
- ReadOnlyModeDisallowedStore: ('main', 'master')
=== removed file 'lib/lp/services/database/readonly.py'
--- lib/lp/services/database/readonly.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/database/readonly.py 1970-01-01 00:00:00 +0000
@@ -1,83 +0,0 @@
-# Copyright 2010 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Helpers for running Launchpad in read-only mode.
-
-To switch an app server to read-only mode, all you need to do is create a file
-named read-only.txt in the root of the Launchpad tree.
-"""
-
-__metaclass__ = type
-__all__ = [
- 'is_read_only',
- 'read_only_file_exists',
- 'read_only_file_path',
- ]
-
-import logging
-import os
-import threading
-
-from lazr.restful.utils import get_current_browser_request
-from zope.security.management import queryInteraction
-
-from lp.services.config import config
-
-
-read_only_file_path = os.path.join(config.root, 'read-only.txt')
-READ_ONLY_MODE_ANNOTATIONS_KEY = 'launchpad.read_only_mode'
-
-
-def read_only_file_exists():
- """Does a read-only.txt file exists in the root of our tree?"""
- return os.path.isfile(read_only_file_path)
-
-
-_lock = threading.Lock()
-_currently_in_read_only = False
-
-
-def is_read_only():
- """Are we in read-only mode?
-
- If called as part of the processing of a request, we'll look in the
- request's annotations for a read-only key
- (READ_ONLY_MODE_ANNOTATIONS_KEY), and if it exists we'll just return its
- value.
-
- If there's no request or the key doesn't exist, we check for the presence
- of a read-only.txt file in the root of our tree, set the read-only key in
- the request's annotations (when there is a request), update
- _currently_in_read_only (in case it changed, also logging the change)
- and return it.
- """
- # pylint: disable-msg=W0603
- global _currently_in_read_only
- request = None
- # XXX: salgado, 2010-01-14, bug=507447: Only call
- # get_current_browser_request() when we have an interaction, or else
- # it will raise an AttributeError.
- if queryInteraction() is not None:
- request = get_current_browser_request()
- if request is not None:
- if READ_ONLY_MODE_ANNOTATIONS_KEY in request.annotations:
- return request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY]
-
- read_only = read_only_file_exists()
- if request is not None:
- request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = read_only
-
- log_change = False
- with _lock:
- if _currently_in_read_only != read_only:
- _currently_in_read_only = read_only
- log_change = True
-
- if log_change:
- logging.warning(
- 'Read-only mode change detected; now read-only is %s' % read_only)
-
- return read_only
-
-
-_currently_in_read_only = is_read_only()
=== removed file 'lib/lp/services/database/tests/readonly.py'
--- lib/lp/services/database/tests/readonly.py 2011-12-24 15:54:55 +0000
+++ lib/lp/services/database/tests/readonly.py 1970-01-01 00:00:00 +0000
@@ -1,67 +0,0 @@
-# Copyright 2010 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Helpers for creating and removing a read-only.txt in the root of our tree.
-"""
-
-__metaclass__ = type
-__all__ = [
- 'touch_read_only_file',
- 'read_only_mode',
- 'remove_read_only_file',
- ]
-
-from contextlib import contextmanager
-import os
-
-from lazr.restful.utils import get_current_browser_request
-
-from lp.services.database.readonly import (
- is_read_only,
- read_only_file_exists,
- read_only_file_path,
- READ_ONLY_MODE_ANNOTATIONS_KEY,
- )
-
-
-def touch_read_only_file():
- """Create an empty file named read-only.txt under the root of the tree.
-
- This function must not be called if a file with that name already exists.
- """
- assert not read_only_file_exists(), (
- "This function must not be called when a read-only.txt file "
- "already exists.")
- f = open(read_only_file_path, 'w')
- f.close()
- # Assert that the switch succeeded and make sure the mode change is
- # logged.
- assert is_read_only(), "Switching to read-only failed."
-
-
-def remove_read_only_file(assert_mode_switch=True):
- """Remove the file named read-only.txt from the root of the tree.
-
- May also assert that the mode switch actually happened (i.e. not
- is_read_only()). This assertion has to be conditional because some tests
- will use this during the processing of a request, when a mode change can't
- happen (i.e. is_read_only() will still return True during that request's
- processing, even though the read-only.txt file has been removed).
- """
- os.remove(read_only_file_path)
- if assert_mode_switch:
- # Assert that the switch succeeded and make sure the mode change is
- # logged.
- assert not is_read_only(), "Switching to read-write failed."
-
-
-@contextmanager
-def read_only_mode(flag=True):
- request = get_current_browser_request()
- current = request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY]
- request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = flag
- try:
- assert is_read_only() == flag, 'Failed to set read-only mode'
- yield
- finally:
- request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = current
=== removed file 'lib/lp/services/database/tests/test_readonly.py'
--- lib/lp/services/database/tests/test_readonly.py 2012-03-14 18:38:28 +0000
+++ lib/lp/services/database/tests/test_readonly.py 1970-01-01 00:00:00 +0000
@@ -1,88 +0,0 @@
-# Copyright 2010 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-
-from lazr.restful.utils import get_current_browser_request
-from zope.security.management import endInteraction
-
-from lp.services.database.readonly import (
- is_read_only,
- read_only_file_exists,
- READ_ONLY_MODE_ANNOTATIONS_KEY,
- )
-from lp.services.database.tests.readonly import (
- remove_read_only_file,
- touch_read_only_file,
- )
-from lp.testing import (
- ANONYMOUS,
- login,
- logout,
- TestCase,
- )
-from lp.testing.layers import FunctionalLayer
-
-
-class TestReadOnlyModeDetection(TestCase):
-
- def test_read_only_file_exists(self):
- # By default we run in read-write mode.
- self.assertFalse(read_only_file_exists())
-
- # When a file named 'read-only.txt' exists under the root of the tree,
- # we run in read-only mode.
- touch_read_only_file()
- try:
- self.assertTrue(read_only_file_exists())
- finally:
- remove_read_only_file()
-
- # Once the file is removed, we're back into read-write mode.
- self.assertFalse(read_only_file_exists())
-
-
-class Test_is_read_only(TestCase):
- layer = FunctionalLayer
-
- def tearDown(self):
- # Safety net just in case a test leaves the read-only.txt file behind.
- if read_only_file_exists():
- remove_read_only_file()
- endInteraction()
- super(Test_is_read_only, self).tearDown()
-
- def test_is_read_only(self):
- # By default we run in read-write mode.
- logout()
- self.assertFalse(is_read_only())
-
- # When a file named 'read-only.txt' exists under the root of the tree,
- # we run in read-only mode.
- touch_read_only_file()
- try:
- self.assertTrue(is_read_only())
- finally:
- remove_read_only_file()
-
- def test_caching_in_request(self):
- # When called as part of a request processing, is_read_only() will
- # stash the read-only flag in the request's annotations.
- login(ANONYMOUS)
- request = get_current_browser_request()
- self.assertIs(
- None,
- request.annotations.get(READ_ONLY_MODE_ANNOTATIONS_KEY))
- self.assertFalse(is_read_only())
- self.assertFalse(
- request.annotations.get(READ_ONLY_MODE_ANNOTATIONS_KEY))
-
- def test_cached_value_takes_precedence(self):
- # Once the request has the read-only flag, we don't check for the
- # presence of the read-only.txt file anymore, so it could be removed
- # and the request would still be in read-only mode.
- login(ANONYMOUS)
- request = get_current_browser_request()
- request.annotations[READ_ONLY_MODE_ANNOTATIONS_KEY] = True
- self.assertTrue(is_read_only())
- self.assertFalse(read_only_file_exists())
=== modified file 'lib/lp/services/feeds/configure.zcml'
--- lib/lp/services/feeds/configure.zcml 2011-12-30 07:32:58 +0000
+++ lib/lp/services/feeds/configure.zcml 2012-08-09 04:24:19 +0000
@@ -46,6 +46,7 @@
name="index.html"
permission="zope.Public"
class="lp.services.webapp.login.FeedsUnauthorizedView"
+ template="../../app/templates/launchpad-forbidden.pt"
attribute="__call__"
layer="lp.layers.FeedsLayer"
/>
=== modified file 'lib/lp/services/webapp/adapter.py'
--- lib/lp/services/webapp/adapter.py 2012-06-29 08:40:05 +0000
+++ lib/lp/services/webapp/adapter.py 2012-08-09 04:24:19 +0000
@@ -21,7 +21,6 @@
get_current_browser_request,
safe_hasattr,
)
-import psycopg2
from psycopg2.extensions import (
ISOLATION_LEVEL_AUTOCOMMIT,
ISOLATION_LEVEL_READ_COMMITTED,
@@ -36,7 +35,6 @@
)
from storm.databases.postgres import (
Postgres,
- PostgresConnection,
PostgresTimeoutTracer,
)
from storm.exceptions import TimeoutError
@@ -65,7 +63,6 @@
IMasterStore,
)
from lp.services.database.postgresql import ConnectionString
-from lp.services.database.readonly import is_read_only
from lp.services.log.loglevels import DEBUG2
from lp.services.stacktrace import (
extract_stack,
@@ -84,8 +81,6 @@
IStoreSelector,
MAIN_STORE,
MASTER_FLAVOR,
- ReadOnlyModeDisallowedStore,
- ReadOnlyModeViolation,
SLAVE_FLAVOR,
)
from lp.services.webapp.opstats import OpStats
@@ -473,23 +468,6 @@
}
-class ReadOnlyModeConnection(PostgresConnection):
- """storm.database.Connection for read-only mode Launchpad."""
-
- def execute(self, statement, params=None, noresult=False):
- """See storm.database.Connection."""
- try:
- return super(ReadOnlyModeConnection, self).execute(
- statement, params, noresult)
- except psycopg2.InternalError as exception:
- # Error 25006 is 'ERROR: transaction is read-only'. This
- # is raised when an attempt is made to make changes when
- # the connection has been put in read-only mode.
- if exception.pgcode == '25006':
- raise ReadOnlyModeViolation(None, sys.exc_info()[2])
- raise
-
-
class LaunchpadDatabase(Postgres):
_dsn_user_re = re.compile('user=[^ ]*')
@@ -572,17 +550,6 @@
flavor, raw_connection.get_backend_pid(), dbuser, self._isolation)
return raw_connection
- @property
- def connection_factory(self):
- """Return the correct connection factory for the current mode.
-
- If we are running in read-only mode, returns a
- ReadOnlyModeConnection. Otherwise it returns the Storm default.
- """
- if is_read_only():
- return ReadOnlyModeConnection
- return super(LaunchpadDatabase, self).connection_factory
-
class LaunchpadSessionDatabase(Postgres):
@@ -828,20 +795,6 @@
@staticmethod
def get(name, flavor):
"""See `IStoreSelector`."""
- if is_read_only():
- # If we are in read-only mode, override the default to the
- # slave no matter what the existing policy says (it might
- # work), and raise an exception if the master was explicitly
- # requested. Most of the time, this doesn't matter as when
- # we are in read-only mode we have a suitable database
- # policy installed. However, code can override the policy so
- # we still need to catch disallowed requests here.
- if flavor == DEFAULT_FLAVOR:
- flavor = SLAVE_FLAVOR
- elif flavor == MASTER_FLAVOR:
- raise ReadOnlyModeDisallowedStore(name, flavor)
- else:
- pass
db_policy = StoreSelector.get_current()
if db_policy is None:
db_policy = MasterDatabasePolicy(None)
=== modified file 'lib/lp/services/webapp/authorization.py'
--- lib/lp/services/webapp/authorization.py 2012-03-31 11:32:15 +0000
+++ lib/lp/services/webapp/authorization.py 2012-08-09 04:24:19 +0000
@@ -40,7 +40,6 @@
from lp.app.interfaces.security import IAuthorization
from lp.registry.interfaces.role import IPersonRoles
-from lp.services.database.readonly import is_read_only
from lp.services.database.sqlbase import block_implicit_flushes
from lp.services.privacy.interfaces import IObjectPrivacy
from lp.services.webapp.canonicalurl import nearest_adapter
@@ -144,15 +143,6 @@
after the permission, use that to check the permission.
- Otherwise, deny.
"""
- # Shortcut in read-only mode. We have to do this now to avoid
- # accidentally using cached results. This will be important when
- # Launchpad automatically fails over to read-only mode when the
- # master database is unavailable.
- if is_read_only():
- lp_permission = getUtility(ILaunchpadPermission, permission)
- if lp_permission.access_level != "read":
- return False
-
# If we have a view, get its context and use that to get an
# authorization adapter.
if IView.providedBy(object):
=== modified file 'lib/lp/services/webapp/dbpolicy.py'
--- lib/lp/services/webapp/dbpolicy.py 2012-06-20 18:48:02 +0000
+++ lib/lp/services/webapp/dbpolicy.py 2012-08-09 04:24:19 +0000
@@ -9,7 +9,6 @@
'DatabaseBlockedPolicy',
'LaunchpadDatabasePolicy',
'MasterDatabasePolicy',
- 'ReadOnlyLaunchpadDatabasePolicy',
'SlaveDatabasePolicy',
'SlaveOnlyDatabasePolicy',
]
@@ -45,7 +44,6 @@
IMasterStore,
ISlaveStore,
)
-from lp.services.database.readonly import is_read_only
from lp.services.database.sqlbase import StupidCache
from lp.services.webapp import LaunchpadView
from lp.services.webapp.interfaces import (
@@ -55,7 +53,6 @@
IStoreSelector,
MAIN_STORE,
MASTER_FLAVOR,
- ReadOnlyModeDisallowedStore,
SLAVE_FLAVOR,
)
@@ -195,8 +192,6 @@
# of test requests in our automated tests.
if request.get('PATH_INFO') in [u'/+opstats', u'/+haproxy']:
return DatabaseBlockedPolicy(request)
- elif is_read_only():
- return ReadOnlyLaunchpadDatabasePolicy(request)
else:
return LaunchpadDatabasePolicy(request)
@@ -357,41 +352,15 @@
def WebServiceDatabasePolicyFactory(request):
"""Return the Launchpad IDatabasePolicy for the current appserver state.
"""
- if is_read_only():
- return ReadOnlyLaunchpadDatabasePolicy(request)
- else:
- # If a session cookie was sent with the request, use the
- # standard Launchpad database policy for load balancing to
- # the slave databases. The javascript web service libraries
- # send the session cookie for authenticated users.
- cookie_name = getUtility(IClientIdManager).namespace
- if cookie_name in request.cookies:
- return LaunchpadDatabasePolicy(request)
- # Otherwise, use the master only web service database policy.
- return MasterDatabasePolicy(request)
-
-
-class ReadOnlyLaunchpadDatabasePolicy(BaseDatabasePolicy):
- """Policy for Launchpad web requests when running in read-only mode.
-
- Access to all master Stores is blocked.
- """
-
- def getStore(self, name, flavor):
- """See `IDatabasePolicy`.
-
- Access to all master Stores is blocked. The default Store is
- the slave.
-
- Note that we even have to block access to the authdb master
- Store, as it allows access to tables replicated from the
- lpmain replication set. These tables will be locked during
- a lpmain replication set database upgrade.
- """
- if flavor == MASTER_FLAVOR:
- raise ReadOnlyModeDisallowedStore(name, flavor)
- return super(ReadOnlyLaunchpadDatabasePolicy, self).getStore(
- name, SLAVE_FLAVOR)
+ # If a session cookie was sent with the request, use the
+ # standard Launchpad database policy for load balancing to
+ # the slave databases. The javascript web service libraries
+ # send the session cookie for authenticated users.
+ cookie_name = getUtility(IClientIdManager).namespace
+ if cookie_name in request.cookies:
+ return LaunchpadDatabasePolicy(request)
+ # Otherwise, use the master only web service database policy.
+ return MasterDatabasePolicy(request)
class WhichDbView(LaunchpadView):
=== modified file 'lib/lp/services/webapp/errorlog.py'
--- lib/lp/services/webapp/errorlog.py 2012-05-15 07:15:17 +0000
+++ lib/lp/services/webapp/errorlog.py 2012-08-09 04:24:19 +0000
@@ -294,9 +294,7 @@
class ErrorReportingUtility:
implements(IErrorReportingUtility)
- _ignored_exceptions = set([
- 'ReadOnlyModeDisallowedStore', 'ReadOnlyModeViolation',
- 'TranslationUnavailable', 'NoReferrerError'])
+ _ignored_exceptions = set(['TranslationUnavailable', 'NoReferrerError'])
_ignored_exceptions_for_offsite_referer = set([
'GoneError', 'InvalidBatchSizeError', 'NotFound'])
_default_config_section = 'error_reports'
=== modified file 'lib/lp/services/webapp/interfaces.py'
--- lib/lp/services/webapp/interfaces.py 2012-02-22 18:51:23 +0000
+++ lib/lp/services/webapp/interfaces.py 2012-08-09 04:24:19 +0000
@@ -805,21 +805,6 @@
"""
-class ReadOnlyModeViolation(Exception):
- """An attempt was made to write to a slave Store in read-only mode.
-
- This can happen in legacy code where writes are being made to an
- object retrieved from the default Store rather than casting the
- object to a writable version using IMasterObject(obj).
- """
-
-
-class ReadOnlyModeDisallowedStore(DisallowedStore, ReadOnlyModeViolation):
- """A request was made to access a Store that cannot be granted
- because we are running in read-only mode.
- """
-
-
class IStoreSelector(Interface):
"""Get a Storm store with a desired flavor.
=== modified file 'lib/lp/services/webapp/login.py'
--- lib/lp/services/webapp/login.py 2012-08-08 14:27:28 +0000
+++ lib/lp/services/webapp/login.py 2012-08-09 04:24:19 +0000
@@ -45,7 +45,6 @@
TeamEmailAddressError,
)
from lp.services.config import config
-from lp.services.database.readonly import is_read_only
from lp.services.identity.interfaces.account import AccountSuspendedError
from lp.services.openid.interfaces.openidconsumer import IOpenIDConsumerStore
from lp.services.propertycache import cachedproperty
@@ -59,7 +58,6 @@
IPlacelessLoginSource,
LoggedOutEvent,
)
-from lp.services.webapp.metazcml import ILaunchpadPermission
from lp.services.webapp.publisher import LaunchpadView
from lp.services.webapp.url import urlappend
from lp.services.webapp.vhosts import allvhosts
@@ -68,33 +66,9 @@
class UnauthorizedView(SystemErrorView):
response_code = None
-
- forbidden_page = ViewPageTemplateFile(
- '../../../lp/app/templates/launchpad-forbidden.pt')
-
- read_only_page = ViewPageTemplateFile(
- '../../../lp/app/templates/launchpad-readonlyfailure.pt')
-
- def page_title(self):
- if is_read_only():
- return super(UnauthorizedView, self).page_title
- else:
- return 'Forbidden'
+ page_title = 'Forbidden'
def __call__(self):
- # In read only mode, Unauthorized exceptions get raised by the
- # security policy when write permissions are requested. We need
- # to render the read-only failure screen so the user knows their
- # request failed for operational reasons rather than a genuine
- # permission problem.
- if is_read_only():
- # Our context is an Unauthorized exception, which acts like
- # a tuple containing (object, attribute_requested, permission).
- lp_permission = getUtility(ILaunchpadPermission, self.context[2])
- if lp_permission.access_level != "read":
- self.request.response.setStatus(503) # Service Unavailable
- return self.read_only_page()
-
if IUnauthenticatedPrincipal.providedBy(self.request.principal):
if 'loggingout' in self.request.form:
target = '%s?loggingout=1' % self.request.URL[-2]
@@ -133,7 +107,7 @@
return ''
else:
self.request.response.setStatus(403) # Forbidden
- return self.forbidden_page()
+ return self.template()
def getRedirectURL(self, current_url, query_string):
"""Get the URL to redirect to.
@@ -569,4 +543,4 @@
assert IUnauthenticatedPrincipal.providedBy(self.request.principal), (
"Feeds user should always be anonymous.")
self.request.response.setStatus(403) # Forbidden
- return self.forbidden_page()
+ return self.template()
=== modified file 'lib/lp/services/webapp/publication.py'
--- lib/lp/services/webapp/publication.py 2012-01-04 05:07:53 +0000
+++ lib/lp/services/webapp/publication.py 2012-08-09 04:24:19 +0000
@@ -64,7 +64,6 @@
)
from lp.services import features
from lp.services.config import config
-from lp.services.database.readonly import is_read_only
from lp.services.features.flags import NullFeatureController
from lp.services.oauth.interfaces import IOAuthSignedRequest
from lp.services.osutils import open_for_writing
@@ -74,7 +73,6 @@
FinishReadOnlyRequestEvent,
IDatabasePolicy,
ILaunchpadRoot,
- INotificationResponse,
IOpenLaunchBag,
IPlacelessAuthUtility,
IPrimaryContext,
@@ -84,7 +82,6 @@
OffsiteFormPostError,
StartRequestEvent,
)
-from lp.services.webapp.menu import structured
from lp.services.webapp.opstats import OpStats
from lp.services.webapp.vhosts import allvhosts
@@ -267,32 +264,9 @@
transaction.begin()
- db_policy = IDatabasePolicy(request)
-
- # If we have switched to or from read-only mode, we need to
- # disconnect all Stores for this thread. We don't want the
- # appserver to leave dangling connections as this will interfere
- # with database maintenance.
- # We don't disconnect Stores for threads currently handling
- # requests. That would generate unreproducable OOPSes. This
- # isn't a problem, as our requests should complete soon or
- # timeout. Unfortunately, there is no way to disconnect Stores
- # for idle threads. This means connections are left dangling
- # until the appserver has processed as many requests as there
- # are worker threads. We will be able to handle this better
- # when we have a connection pool.
- was_read_only = getattr(self.thread_locals, 'was_read_only', None)
- if was_read_only is not None and was_read_only != is_read_only():
- zstorm = getUtility(IZStorm)
- for name, store in list(zstorm.iterstores()):
- zstorm.remove(store)
- store.close()
- # is_read_only() is cached for the entire request, so there
- # is no race condition here.
- self.thread_locals.was_read_only = is_read_only()
-
# Now we are logged in, install the correct IDatabasePolicy for
# this request.
+ db_policy = IDatabasePolicy(request)
getUtility(IStoreSelector).push(db_policy)
getUtility(IOpenLaunchBag).clear()
@@ -308,21 +282,6 @@
request.setPrincipal(principal)
self.maybeRestrictToTeam(request)
maybe_block_offsite_form_post(request)
- self.maybeNotifyReadOnlyMode(request)
-
- def maybeNotifyReadOnlyMode(self, request):
- """Hook to notify about read-only mode."""
- if is_read_only():
- notification_response = INotificationResponse(request, None)
- if notification_response is not None:
- notification_response.addWarningNotification(
- structured("""
- Launchpad is undergoing maintenance and is in
- read-only mode. <i>You cannot make any
- changes.</i> You can find more information on the
- <a href="http://identi.ca/launchpadstatus">Launchpad
- system status</a> page.
- """))
def getPrincipal(self, request):
"""Return the authenticated principal for this request.
=== modified file 'lib/lp/services/webapp/tests/test_dbpolicy.py'
--- lib/lp/services/webapp/tests/test_dbpolicy.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_dbpolicy.py 2012-08-09 04:24:19 +0000
@@ -30,15 +30,10 @@
IMasterStore,
ISlaveStore,
)
-from lp.services.database.tests.readonly import (
- remove_read_only_file,
- touch_read_only_file,
- )
from lp.services.webapp.dbpolicy import (
BaseDatabasePolicy,
LaunchpadDatabasePolicy,
MasterDatabasePolicy,
- ReadOnlyLaunchpadDatabasePolicy,
SlaveDatabasePolicy,
SlaveOnlyDatabasePolicy,
)
@@ -50,7 +45,6 @@
IStoreSelector,
MAIN_STORE,
MASTER_FLAVOR,
- ReadOnlyModeDisallowedStore,
SLAVE_FLAVOR,
)
from lp.services.webapp.servers import LaunchpadTestRequest
@@ -230,63 +224,9 @@
finally:
endInteraction()
- def test_WebServiceRequest_uses_ReadOnlyDatabasePolicy(self):
- """WebService requests should use the read only database
- policy in read only mode.
- """
- touch_read_only_file()
- try:
- api_prefix = getUtility(
- IWebServiceConfiguration).active_versions[0]
- server_url = 'http://api.launchpad.dev/%s' % api_prefix
- request = LaunchpadTestRequest(SERVER_URL=server_url)
- setFirstLayer(request, WebServiceLayer)
- policy = IDatabasePolicy(request)
- self.assertIsInstance(policy, ReadOnlyLaunchpadDatabasePolicy)
- finally:
- remove_read_only_file()
-
- def test_read_only_mode_uses_ReadOnlyLaunchpadDatabasePolicy(self):
- touch_read_only_file()
- try:
- request = LaunchpadTestRequest(
- SERVER_URL='http://launchpad.dev')
- policy = IDatabasePolicy(request)
- self.assertIsInstance(policy, ReadOnlyLaunchpadDatabasePolicy)
- finally:
- remove_read_only_file()
-
def test_other_request_uses_LaunchpadDatabasePolicy(self):
"""By default, requests should use the LaunchpadDatabasePolicy."""
server_url = 'http://launchpad.dev/'
request = LaunchpadTestRequest(SERVER_URL=server_url)
policy = IDatabasePolicy(request)
self.assertIsInstance(policy, LaunchpadDatabasePolicy)
-
-
-class ReadOnlyLaunchpadDatabasePolicyTestCase(BaseDatabasePolicyTestCase):
- """Tests for the `ReadOnlyModeLaunchpadDatabasePolicy`"""
-
- def setUp(self):
- self.policy = ReadOnlyLaunchpadDatabasePolicy()
- super(ReadOnlyLaunchpadDatabasePolicyTestCase, self).setUp()
-
- def test_defaults(self):
- # default Store is the slave.
- for store in ALL_STORES:
- self.assertProvides(
- getUtility(IStoreSelector).get(store, DEFAULT_FLAVOR),
- ISlaveStore)
-
- def test_slave_allowed(self):
- for store in ALL_STORES:
- self.assertProvides(
- getUtility(IStoreSelector).get(store, SLAVE_FLAVOR),
- ISlaveStore)
-
- def test_master_disallowed(self):
- store_selector = getUtility(IStoreSelector)
- for store in ALL_STORES:
- self.assertRaises(
- ReadOnlyModeDisallowedStore,
- store_selector.get, store, MASTER_FLAVOR)
=== modified file 'lib/lp/services/webapp/tests/test_publication.py'
--- lib/lp/services/webapp/tests/test_publication.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_publication.py 2012-08-09 04:24:19 +0000
@@ -5,7 +5,6 @@
__metaclass__ = type
-import logging
import sys
from contrib.oauth import (
@@ -17,7 +16,6 @@
STATE_RECONNECT,
)
from storm.exceptions import DisconnectionError
-from storm.zope.interfaces import IZStorm
from zope.component import getUtility
from zope.interface import directlyProvides
from zope.publisher.interfaces import (
@@ -25,13 +23,7 @@
Retry,
)
-from lp.services.config import dbconfig
from lp.services.database.lpstorm import IMasterStore
-from lp.services.database.readonly import is_read_only
-from lp.services.database.tests.readonly import (
- remove_read_only_file,
- touch_read_only_file,
- )
from lp.services.identity.model.emailaddress import EmailAddress
from lp.services.oauth.interfaces import (
IOAuthConsumerSet,
@@ -39,13 +31,9 @@
)
import lp.services.webapp.adapter as dbadapter
from lp.services.webapp.interfaces import (
- IStoreSelector,
- MAIN_STORE,
- MASTER_FLAVOR,
NoReferrerError,
OAuthPermission,
OffsiteFormPostError,
- SLAVE_FLAVOR,
)
from lp.services.webapp.publication import (
is_browser,
@@ -64,10 +52,7 @@
TestCase,
TestCaseWithFactory,
)
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- FunctionalLayer,
- )
+from lp.testing.layers import DatabaseFunctionalLayer
class TestLaunchpadBrowserPublication(TestCase):
@@ -95,137 +80,6 @@
self.assertEquals(request.traversed_objects, [obj1])
-class TestReadOnlyModeSwitches(TestCase):
- # At the beginning of every request (in publication.beforeTraversal()), we
- # check to see if we've changed from/to read-only/read-write and if there
- # was a change we remove the main_master/slave stores from ZStorm, forcing
- # them to be recreated the next time they're needed, thus causing them to
- # point to the correct databases.
- layer = DatabaseFunctionalLayer
-
- def tearDown(self):
- TestCase.tearDown(self)
- # If a DB policy was installed (e.g. by publication.beforeTraversal),
- # uninstall it.
- try:
- getUtility(IStoreSelector).pop()
- except IndexError:
- pass
- # Cleanup needed so that further tests can start processing other
- # requests (e.g. calling beforeTraversal).
- self.publication.endRequest(self.request, None)
- # Force pending mode switches to actually happen and get logged so
- # that we don't interfere with other tests.
- assert not is_read_only(), (
- "A test failed to clean things up properly, leaving the app "
- "in read-only mode.")
-
- def setUp(self):
- TestCase.setUp(self)
- # Get the main_master/slave stores just to make sure they're added to
- # ZStorm.
- master = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
- slave = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
- self.master_connection = master._connection
- self.slave_connection = slave._connection
- self.zstorm = getUtility(IZStorm)
- self.publication = LaunchpadBrowserPublication(None)
- # Run through once to initialize. beforeTraversal will never
- # disconnect Stores the first run through because there is no
- # need.
- request = LaunchpadTestRequest()
- self.publication.beforeTraversal(request)
- self.publication.endRequest(request, None)
- getUtility(IStoreSelector).pop()
-
- self.request = LaunchpadTestRequest()
-
- @property
- def zstorm_stores(self):
- return [name for (name, store) in self.zstorm.iterstores()]
-
- def test_no_mode_changes(self):
- # Make sure the master/slave stores are present in zstorm.
- self.assertIn('main-master', self.zstorm_stores)
- self.assertIn('main-slave', self.zstorm_stores)
-
- self.publication.beforeTraversal(self.request)
-
- # Since the mode didn't change, the stores were left in zstorm.
- self.assertIn('main-master', self.zstorm_stores)
- self.assertIn('main-slave', self.zstorm_stores)
-
- # With the store's connection being the same as before.
- master = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
- self.assertIs(self.master_connection, master._connection)
-
- # And they still point to the read-write databases.
- self.assertEquals(
- dbconfig.rw_main_master.strip(),
- # XXX: 2009-01-12, salgado, bug=506536: We shouldn't need to go
- # through private attributes to get to the store's database.
- master._connection._database.dsn_without_user.strip())
-
- def test_changing_modes(self):
- # Make sure the master/slave stores are present in zstorm.
- self.assertIn('main-master', self.zstorm_stores)
- self.assertIn('main-slave', self.zstorm_stores)
-
- try:
- touch_read_only_file()
- self.publication.beforeTraversal(self.request)
- finally:
- # Tell remove_read_only_file() to not assert that the mode switch
- # actually happened, as we know it won't happen until this request
- # is finished.
- remove_read_only_file(assert_mode_switch=False)
-
- # Here the mode has changed to read-only, so the stores were removed
- # from zstorm.
- self.assertNotIn('main-master', self.zstorm_stores)
- self.assertNotIn('main-slave', self.zstorm_stores)
-
- # If they're needed again, they'll be re-created by ZStorm, and when
- # that happens they will point to the read-only databases.
- master = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
- self.assertEquals(
- dbconfig.ro_main_master.strip(),
- # XXX: 2009-01-12, salgado, bug=506536: We shouldn't need to go
- # through private attributes to get to the store's database.
- master._connection._database.dsn_without_user.strip())
-
-
-class TestReadOnlyNotifications(TestCase):
- """Tests for `LaunchpadBrowserPublication.maybeNotifyReadOnlyMode`."""
-
- layer = FunctionalLayer
-
- def setUp(self):
- TestCase.setUp(self)
- touch_read_only_file()
- self.addCleanup(remove_read_only_file, assert_mode_switch=False)
-
- def test_notification(self):
- # In read-only mode, maybeNotifyReadOnlyMode adds a warning that
- # changes cannot be made to every request that supports notifications.
- publication = LaunchpadBrowserPublication(None)
- request = LaunchpadTestRequest()
- publication.maybeNotifyReadOnlyMode(request)
- self.assertEqual(1, len(request.notifications))
- notification = request.notifications[0]
- self.assertEqual(logging.WARNING, notification.level)
- self.assertTrue('read-only mode' in notification.message)
-
- def test_notification_xmlrpc(self):
- # Even in read-only mode, maybeNotifyReadOnlyMode doesn't try to add a
- # notification to a request that doesn't support notifications.
- from lp.services.webapp.servers import PublicXMLRPCRequest
- publication = LaunchpadBrowserPublication(None)
- request = PublicXMLRPCRequest(None, {})
- # This is just assertNotRaises
- publication.maybeNotifyReadOnlyMode(request)
-
-
class TestWebServicePublication(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
=== modified file 'lib/lp/translations/browser/serieslanguage.py'
--- lib/lp/translations/browser/serieslanguage.py 2012-01-01 02:58:52 +0000
+++ lib/lp/translations/browser/serieslanguage.py 2012-08-09 04:24:19 +0000
@@ -15,7 +15,6 @@
from lp.app.browser.tales import PersonFormatterAPI
from lp.registry.model.sourcepackagename import SourcePackageName
from lp.services.database.bulk import load_related
-from lp.services.database.readonly import is_read_only
from lp.services.propertycache import cachedproperty
from lp.services.webapp import LaunchpadView
from lp.services.webapp.batching import BatchNavigator
@@ -85,12 +84,6 @@
@property
def access_level_description(self):
"""Must not be called when there's no translation group."""
-
- if is_read_only():
- return (
- "No work can be done on these translations while Launchpad "
- "is in read-only mode.")
-
if self.user is None:
return ("You are not logged in. Please log in to work "
"on translations.")
=== modified file 'lib/lp/translations/browser/tests/test_distroserieslanguage_views.py'
--- lib/lp/translations/browser/tests/test_distroserieslanguage_views.py 2012-01-01 02:58:52 +0000
+++ lib/lp/translations/browser/tests/test_distroserieslanguage_views.py 2012-08-09 04:24:19 +0000
@@ -3,7 +3,6 @@
__metaclass__ = type
-from lazr.restful.utils import get_current_browser_request
from testtools.matchers import Equals
import transaction
from zope.component import getUtility
@@ -40,11 +39,6 @@
self.view = DistroSeriesLanguageView(
self.dsl, LaunchpadTestRequest())
- def _simulateReadOnlyMode(self):
- """Pretend to be in read-only mode for this test."""
- request = get_current_browser_request()
- request.annotations['launchpad.read_only_mode'] = True
-
def test_empty_view(self):
self.assertEquals(self.view.translation_group, None)
self.assertEquals(self.view.translation_team, None)
@@ -78,13 +72,6 @@
self.view.initialize()
self.assertEquals(self.view.translation_team, translator)
- def test_access_level_description_handles_readonly(self):
- self._simulateReadOnlyMode()
- notice = (
- "No work can be done on these translations while Launchpad "
- "is in read-only mode.")
- self.assertEqual(notice, self.view.access_level_description)
-
def test_sourcepackagenames_bulk_loaded(self):
# SourcePackageName records referenced by POTemplates
# are bulk loaded. Accessing the sourcepackagename attribute
=== modified file 'lib/lp/translations/browser/translationmessage.py'
--- lib/lp/translations/browser/translationmessage.py 2012-06-29 08:40:05 +0000
+++ lib/lp/translations/browser/translationmessage.py 2012-08-09 04:24:19 +0000
@@ -39,7 +39,6 @@
from zope.schema.vocabulary import getVocabularyRegistry
from lp.app.errors import UnexpectedFormData
-from lp.services.database.readonly import is_read_only
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
ApplicationMenu,
@@ -382,11 +381,6 @@
principle the user should not have been given the option to
submit the current request.
"""
- if is_read_only():
- raise UnexpectedFormData(
- "Launchpad is currently in read-only mode for maintenance. "
- "Please try again later.")
-
if self.user is None:
raise UnexpectedFormData("You are not logged in.")
=== modified file 'lib/lp/translations/model/pofile.py'
--- lib/lp/translations/model/pofile.py 2012-06-29 08:40:05 +0000
+++ lib/lp/translations/model/pofile.py 2012-08-09 04:24:19 +0000
@@ -52,7 +52,6 @@
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.lpstorm import IStore
-from lp.services.database.readonly import is_read_only
from lp.services.database.sqlbase import (
flush_database_updates,
quote,
@@ -153,17 +152,11 @@
def canEditTranslations(self, person):
"""See `IPOFile`."""
- if is_read_only():
- # Nothing can be edited in read-only mode.
- return False
policy = self.potemplate.getTranslationPolicy()
return policy.allowsTranslationEdits(person, self.language)
def canAddSuggestions(self, person):
"""See `IPOFile`."""
- if is_read_only():
- # No data can be entered in read-only mode.
- return False
policy = self.potemplate.getTranslationPolicy()
return policy.allowsTranslationSuggestions(person, self.language)
Follow ups