diff --git a/Makefile-docker b/Makefile-docker index bd029018b25..452370b88e1 100644 --- a/Makefile-docker +++ b/Makefile-docker @@ -8,9 +8,6 @@ export PYTHON_COMMAND=python3 export PIP_COMMAND=$(PYTHON_COMMAND) -m pip APP=src/olympia/ -NUM_ADDONS=10 -NUM_THEMES=$(NUM_ADDONS) - NODE_MODULES := $(NPM_CONFIG_PREFIX)node_modules/ REQUIRED_FILES := \ @@ -52,37 +49,26 @@ check_django: ## check if the django app is configured properly .PHONY: check check: check_files check_olympia_user check_debian_packages check_pip_packages check_django +.PHONY: data_dump +data_dump: + ./manage.py data_dump $(ARGS) + +.PHONY: data_load +data_load: + ./manage.py data_load $(ARGS) + .PHONY: initialize_db initialize_db: ## create a new database rm -rf ./user-media/* ./tmp/* - $(PYTHON_COMMAND) manage.py create_db --force - $(PYTHON_COMMAND) manage.py migrate --noinput - $(PYTHON_COMMAND) manage.py loaddata initial.json - $(PYTHON_COMMAND) manage.py import_prod_versions - # The superuser needs to have a mozilla.com address for admin tools access - $(PYTHON_COMMAND) manage.py createsuperuser \ - --no-input \ - --username "local_admin" \ - --email "local_admin@mozilla.com" - $(PYTHON_COMMAND) manage.py loaddata zadmin/users + ./manage.py create_db --force + ./manage.py migrate --noinput + # Seed the database with initial data + ./manage.py data_seed .PHONY: reindex_data reindex_data: ## reindex the data in elasticsearch $(PYTHON_COMMAND) manage.py reindex --force --noinput -.PHONY: populate_data -populate_data: ## populate a new database - # reindex --wipe will force the ES mapping to be re-installed. Useful to - # make sure the mapping is correct before adding a bunch of add-ons. - $(PYTHON_COMMAND) manage.py reindex --wipe --force --noinput - $(PYTHON_COMMAND) manage.py generate_addons --app firefox $(NUM_ADDONS) - $(PYTHON_COMMAND) manage.py generate_addons --app android $(NUM_ADDONS) - $(PYTHON_COMMAND) manage.py generate_themes $(NUM_THEMES) - # These add-ons are specifically useful for the addons-frontend - # homepage. You may have to re-run this, in case the data there - # changes. - $(PYTHON_COMMAND) manage.py generate_default_addons_for_frontend - .PHONY: update_db update_db: ## run the database migrations $(PYTHON_COMMAND) manage.py migrate --noinput @@ -157,7 +143,7 @@ dbshell: ## connect to a database shell $(PYTHON_COMMAND) ./manage.py dbshell .PHONY: initialize -initialize: initialize_db update_assets populate_data reindex_data ## init the dependencies, the database, and assets +initialize: initialize_db update_assets reindex_data ## init the dependencies, the database, and assets PYTEST_SRC := src/olympia/ diff --git a/Makefile-os b/Makefile-os index 071ee9e541b..20c85dd0432 100644 --- a/Makefile-os +++ b/Makefile-os @@ -77,26 +77,6 @@ shell: ## connect to a running addons-server docker shell rootshell: ## connect to a running addons-server docker shell with root user docker compose exec --user root web bash -.PHONY: data_export -data_export: - @ mkdir -p $(EXPORT_DIR) - - # Extracting mysql database - docker compose exec mysqld /usr/bin/mysqldump olympia > $(EXPORT_DIR)/data_mysqld.sql - -.PHONY: data_restore -data_restore: - @[ -d $(RESTORE_DIR) ] || (echo "Directory $(RESTORE_DIR) does not exist" && exit 1) - - # Wait for MySQL server to be ready - docker compose exec mysqld bash \ - -c 'while ! mysqladmin ping --silent; do echo "waiting"; sleep 1; done' - - # Restoring mysql database - docker compose exec -T mysqld /usr/bin/mysql olympia < $(RESTORE_DIR)/data_mysqld.sql - - $(MAKE) reindex - .PHONY: docker_compose_config docker_compose_config: ## Show the docker compose configuration @docker compose config web --format json @@ -168,7 +148,7 @@ initialize_docker: up docker compose exec --user olympia web make initialize %: ## This directs any other recipe (command) to the web container's make. - docker compose exec --user olympia web make $(MAKECMDGOALS) ARGS=$(ARGS) + docker compose exec --user olympia web make $(MAKECMDGOALS) ARGS="$(shell echo $(ARGS))" # You probably want to put new commands in Makefile-docker, unless they operate # on multiple containers or are host-os specific. diff --git a/docs/topics/development/data_management.md b/docs/topics/development/data_management.md index 25d97eccd2e..e39f263b0da 100644 --- a/docs/topics/development/data_management.md +++ b/docs/topics/development/data_management.md @@ -35,30 +35,46 @@ The `make initialize` command, executed as part of `make initialize_docker`, per ## Exporting and Loading Data Snapshots -You can export and load data snapshots to manage data states across different environments or for backup purposes. The Makefile provides commands to facilitate this. +You can export and load data snapshots to manage data states across different environments or for backup purposes. +The Makefile provides commands to facilitate this. +These commands rely internally on [django-dbbackup](https://django-dbbackup.readthedocs.io/en/stable/) -- **Exporting Data**: +- **Data dump**: ```sh - make data_export [EXPORT_DIR=] + make data_dump [ARGS="--name --force"] ``` - This command creates a dump of the current MySQL database. The optional `EXPORT_DIR` argument allows you to specify a custom path for the export directory. - The default value is a timestamp in the `backups` directory. + This command creates a dump of the current MySQL database. The command accepts an optional `name` argument which will determine + the name of the directory created in the backup directory. By default it uses a timestamp to ensure uniqueness. - The data exported will be a .sql dump of the current state of the database including any data that has been added or modified. + You can also specify the `--force` argument to overwrite an existing backup with the same name. - **Loading Data**: ```sh - make data_restore [RESTORE_DIR=] + make data_load [ARGS="--name "] ``` - This command restores a MySQL database from a previously exported snapshot. The optional `RESTORE_DIR` argument allows you to specify the path of the import file. - This must be an absolute path. It defaults to the latest stored snapshot in the `backups` directory. + This command will load data from an existing backup directory, synchronize the storage directory and reindex elasticsearch. + The name is required and must match a directory in the backup directory. -Refer to the Makefile for detailed instructions on these commands. +## Hard Reset Database -This comprehensive setup ensures that the development environment is fully prepared with the necessary data. +The actual mysql database is created and managed by the `mysqld` container. The database is created on container start +and the actual data is stored in a persistent data volume. This enables data to persist across container restarts. -By following these practices, developers can manage data effectively in the **addons-server** project. The use of persistent volumes, external mounts, data snapshots, and automated data population ensures a robust and flexible data management strategy. For more detailed instructions, refer to the project's Makefile and Docker Compose configuration in the repository. +`addons-server` assumes that a database named `olympia` already exists and most data management commands will fail +if it does not. + +If you need to hard reset the database (for example, to start with a fresh state), you can use the following command: + +```bash +make down && make docker_mysqld_volume_remove +``` + +This will stop the containers and remove the `mysqld` data volume from docker. The next time you run `make up` it will +create a new empty volume for you and mysql will recreate the database. + +> NOTE: removing the data volume will remove the actual data! You can and should save a backup before doing this +> if you want to keep the data. diff --git a/requirements/dev.txt b/requirements/dev.txt index 10357445fea..e27c3970559 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -308,3 +308,6 @@ pytest-split==0.9.0 \ pytest-reportlog==0.4.0 \ --hash=sha256:5db4d00586546d8c6b95c66466629f1e913440c36d97795a673d2e19c5cedd5c \ --hash=sha256:c9f2079504ee51f776d3118dcf5e4730f163d3dcf26ebc8f600c1fa307bf638c +django-dbbackup==4.2.1 \ + --hash=sha256:157a2ec10d482345cd75092e510ac40d6e2ee6084604a1d17abe178c2f06bc69 \ + --hash=sha256:b23265600ead0780ca781b1b4b594949aaa8a20d74f08701f91ee9d7eb1f08cd diff --git a/settings.py b/settings.py index dbb5be584c9..e25c2738301 100644 --- a/settings.py +++ b/settings.py @@ -17,7 +17,16 @@ INTERNAL_ROUTES_ALLOWED = True # These apps are great during development. -INSTALLED_APPS += ('olympia.landfill',) +INSTALLED_APPS += ( + 'olympia.landfill', + 'dbbackup', +) + +DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage' + +DBBACKUP_CONNECTOR_MAPPING = { + 'olympia.core.db.mysql': 'dbbackup.db.mysql.MysqlDumpConnector', +} # Override logging config to enable DEBUG logs for (almost) everything. LOGGING['root']['level'] = logging.DEBUG diff --git a/src/olympia/amo/management/__init__.py b/src/olympia/amo/management/__init__.py index fbc67de52e7..86ac1a9b049 100644 --- a/src/olympia/amo/management/__init__.py +++ b/src/olympia/amo/management/__init__.py @@ -1,3 +1,8 @@ +import logging +import os +import shutil + +from django.conf import settings from django.core.management.base import BaseCommand, CommandError from celery import chord, group @@ -143,3 +148,45 @@ def handle(self, *args, **options): else: ts = group(grouping) ts.apply_async() + + +class BaseDataCommand(BaseCommand): + # Settings for django-dbbackup + data_backup_dirname = os.path.abspath(os.path.join(settings.ROOT, 'backups')) + data_backup_init = '_init' + data_backup_db_filename = 'db.sql' + data_backup_storage_filename = 'storage.tar' + + logger = logging + + def backup_dir_path(self, name): + return os.path.abspath(os.path.join(self.data_backup_dirname, name)) + + def backup_db_path(self, name): + return os.path.abspath( + os.path.join(self.backup_dir_path(name), self.data_backup_db_filename) + ) + + def backup_storage_path(self, name): + return os.path.abspath( + os.path.join(self.backup_dir_path(name), self.data_backup_storage_filename) + ) + + def clean_dir(self, name: str) -> None: + path = self.backup_dir_path(name) + logging.info(f'Clearing {path}') + shutil.rmtree(path, ignore_errors=True) + + def make_dir(self, name: str, force: bool = False) -> None: + path = self.backup_dir_path(name) + path_exists = os.path.exists(path) + + if path_exists: + if force: + self.clean_dir(name) + else: + raise CommandError( + f'path {path} already exists.' 'Use --force to overwrite.' + ) + + os.makedirs(path, exist_ok=True) diff --git a/src/olympia/amo/management/commands/data_dump.py b/src/olympia/amo/management/commands/data_dump.py new file mode 100644 index 00000000000..129628fc7f7 --- /dev/null +++ b/src/olympia/amo/management/commands/data_dump.py @@ -0,0 +1,48 @@ +from datetime import datetime + +from django.core.management import call_command + +from .. import BaseDataCommand + + +class Command(BaseDataCommand): + help = 'Dump data with a specified name' + + def add_arguments(self, parser): + parser.add_argument( + '--name', + type=str, + default=datetime.now().strftime('%Y%m%d%H%M%S'), + help='Name of the data dump', + ) + parser.add_argument( + '--force', action='store_true', help='Force overwrite of existing dump' + ) + + def handle(self, *args, **options): + name = options.get('name') + force = options.get('force') + + dump_path = self.backup_dir_path(name) + db_path = self.backup_db_path(name) + storage_path = self.backup_storage_path(name) + + try: + self.make_dir(dump_path, force=force) + + call_command( + 'dbbackup', + output_path=db_path, + interactive=False, + compress=True, + ) + + call_command( + 'mediabackup', + output_path=storage_path, + interactive=False, + compress=True, + ) + except Exception as e: + self.clean_dir(dump_path) + raise e diff --git a/src/olympia/amo/management/commands/data_load.py b/src/olympia/amo/management/commands/data_load.py new file mode 100644 index 00000000000..663ff652d80 --- /dev/null +++ b/src/olympia/amo/management/commands/data_load.py @@ -0,0 +1,50 @@ +import os + +from django.core.management import call_command +from django.core.management.base import CommandError + +from .. import BaseDataCommand + + +class Command(BaseDataCommand): + help = 'Load data from a specified name' + + def add_arguments(self, parser): + parser.add_argument( + '--name', + type=str, + required=True, + help='Name of the data dump', + ) + + def handle(self, *args, **options): + name = options.get('name') + db_path = self.backup_db_path(name) + storage_path = self.backup_storage_path(name) + + if not os.path.exists(db_path): + print('DB backup not found: {db_path}') + raise CommandError(f'DB backup not found: {db_path}') + + call_command( + 'dbrestore', + input_path=db_path, + interactive=False, + uncompress=True, + ) + + if not os.path.exists(storage_path): + raise CommandError(f'Storage backup not found: {storage_path}') + + call_command( + 'mediarestore', + input_path=storage_path, + interactive=False, + uncompress=True, + replace=True, + ) + + # reindex --wipe will force the ES mapping to be re-installed. + # After loading data from a backup, we should always reindex + # to make sure the mapping is correct. + call_command('reindex', '--wipe', '--force', '--noinput') diff --git a/src/olympia/amo/management/commands/data_seed.py b/src/olympia/amo/management/commands/data_seed.py new file mode 100644 index 00000000000..a5478811bef --- /dev/null +++ b/src/olympia/amo/management/commands/data_seed.py @@ -0,0 +1,45 @@ +from django.conf import settings +from django.core.management import call_command + +from .. import BaseDataCommand + + +class Command(BaseDataCommand): + help = ( + 'Reset and seed the database with initial data, ' + 'generated add-ons, and data from AMO production.' + ) + + def handle(self, *args, **options): + num_addons = 10 + num_themes = 5 + + self.clean_dir(self.data_backup_init) + + self.logger.info('Resetting database...') + call_command('flush', '--noinput') + # reindex --wipe will force the ES mapping to be re-installed. + call_command('reindex', '--wipe', '--force', '--noinput') + call_command('migrate', '--noinput') + + self.logger.info('Loading initial data...') + call_command('loaddata', 'initial.json') + call_command('import_prod_versions') + call_command( + 'createsuperuser', + '--no-input', + '--username', + settings.LOCAL_ADMIN_USERNAME, + '--email', + settings.LOCAL_ADMIN_EMAIL, + ) + call_command('loaddata', 'zadmin/users') + + self.logger.info('Generating add-ons...') + call_command('generate_addons', '--app', 'firefox', num_addons) + call_command('generate_addons', '--app', 'android', num_addons) + call_command('generate_themes', num_themes) + + call_command('generate_default_addons_for_frontend') + + call_command('data_dump', '--name', self.data_backup_init) diff --git a/src/olympia/amo/tests/test_commands.py b/src/olympia/amo/tests/test_commands.py index 27a620b4131..9b5cf35b42c 100644 --- a/src/olympia/amo/tests/test_commands.py +++ b/src/olympia/amo/tests/test_commands.py @@ -10,8 +10,10 @@ from django.test.utils import override_settings import pytest +from freezegun import freeze_time from olympia.addons.models import Preview +from olympia.amo.management import BaseDataCommand from olympia.amo.management.commands.get_changed_files import ( collect_addon_icons, collect_addon_previews, @@ -332,3 +334,312 @@ def path(self): assert collect_blocklist(self.yesterday) == [ f'foo/{datetime_to_ts(newerer)}' ] + + +class BaseTestDataCommand(TestCase): + class Commands: + flush = mock.call('flush', '--noinput') + migrate = mock.call('migrate', '--noinput') + data_seed = mock.call('data_seed') + + flush = mock.call('flush', '--noinput') + reindex = mock.call('reindex', '--wipe', '--force', '--noinput') + load_initial_data = mock.call('loaddata', 'initial.json') + import_prod_versions = mock.call('import_prod_versions') + createsuperuser = mock.call( + 'createsuperuser', + '--no-input', + '--username', + settings.LOCAL_ADMIN_USERNAME, + '--email', + settings.LOCAL_ADMIN_EMAIL, + ) + load_zadmin_users = mock.call('loaddata', 'zadmin/users') + generate_default_addons_for_frontend = mock.call( + 'generate_default_addons_for_frontend' + ) + + def data_dump(self, name='_init'): + return mock.call('data_dump', '--name', name) + + def generate_addons(self, app, num_addons): + return mock.call('generate_addons', '--app', app, num_addons) + + def generate_themes(self, num_themes): + return mock.call('generate_themes', num_themes) + + def data_load(self, name='_init'): + return mock.call('data_load', '--name', name) + + def db_backup(self, output_path): + return mock.call( + 'dbbackup', output_path=output_path, interactive=False, compress=True + ) + + def db_restore(self, input_path): + return mock.call( + 'dbrestore', input_path=input_path, interactive=False, uncompress=True + ) + + def media_backup(self, output_path): + return mock.call( + 'mediabackup', output_path=output_path, interactive=False, compress=True + ) + + def media_restore(self, input_path): + return mock.call( + 'mediarestore', + input_path=input_path, + interactive=False, + uncompress=True, + replace=True, + ) + + base_data_command = BaseDataCommand() + backup_dir = '/data/olympia/backups' + db_file = 'db.sql' + storage_file = 'storage.tar' + + def setUp(self): + self.mock_commands = self.Commands() + + def _assert_commands_called_in_order(self, mock_call_command, expected_commands): + actual_commands = mock_call_command.mock_calls + assert actual_commands == expected_commands, ( + f'Commands were not called in the expected order. ' + f'Expected: {expected_commands}, Actual: {actual_commands}' + ) + + +class TestBaseDataCommand(BaseTestDataCommand): + def setUp(self): + super().setUp() + + def test_backup_dir_path(self): + name = 'test_backup' + expected_path = os.path.join(self.backup_dir, name) + + actual_path = self.base_data_command.backup_dir_path(name) + assert ( + actual_path == expected_path + ), f'Expected {expected_path}, got {actual_path}' + + def test_backup_db_path(self): + name = 'db_backup' + expected_path = os.path.join(self.backup_dir, name, self.db_file) + actual_path = self.base_data_command.backup_db_path(name) + assert ( + actual_path == expected_path + ), f'Expected {expected_path}, got {actual_path}' + + def test_backup_storage_path(self): + name = 'storage_backup' + expected_path = os.path.join(self.backup_dir, name, self.storage_file) + actual_path = self.base_data_command.backup_storage_path(name) + assert ( + actual_path == expected_path + ), f'Expected {expected_path}, got {actual_path}' + + @mock.patch('olympia.amo.management.shutil.rmtree') + @mock.patch('olympia.amo.management.logging') + def test_clean_dir(self, mock_logging, mock_rmtree): + name = 'cleanup_test' + backup_path = self.base_data_command.backup_dir_path(name) + + self.base_data_command.clean_dir(name) + + mock_logging.info.assert_called_with(f'Clearing {backup_path}') + mock_rmtree.assert_called_with(backup_path, ignore_errors=True) + + @mock.patch('olympia.amo.management.os.path.exists') + @mock.patch('olympia.amo.management.shutil.rmtree') + @mock.patch('olympia.amo.management.os.makedirs') + def test_make_dir_existing_path_no_force( + self, mock_makedirs, mock_rmtree, mock_exists + ): + name = 'existing_dir' + mock_exists.return_value = True + + with self.assertRaises(CommandError) as context: + self.base_data_command.make_dir(name, force=False) + assert 'Directory already exists' in str(context.exception) + + @mock.patch('olympia.amo.management.os.path.exists') + @mock.patch('olympia.amo.management.shutil.rmtree') + @mock.patch('olympia.amo.management.os.makedirs') + def test_make_dir_existing_path_with_force( + self, mock_makedirs, mock_rmtree, mock_exists + ): + name = 'existing_dir_force' + backup_path = self.base_data_command.backup_dir_path(name) + mock_exists.return_value = True + + self.base_data_command.make_dir(name, force=True) + + mock_exists.assert_called_with(backup_path) + mock_rmtree.assert_called_with(backup_path, ignore_errors=True) + mock_makedirs.assert_called_with(backup_path, exist_ok=True) + + @mock.patch('olympia.amo.management.os.path.exists') + @mock.patch('olympia.amo.management.os.makedirs') + def test_make_dir_non_existing_path(self, mock_makedirs, mock_exists): + name = 'new_dir' + backup_path = self.base_data_command.backup_dir_path(name) + mock_exists.return_value = False + + self.base_data_command.make_dir(name, force=False) + + mock_exists.assert_called_with(backup_path) + mock_makedirs.assert_called_with(backup_path, exist_ok=True) + + +class TestDumpDataCommand(BaseTestDataCommand): + def setUp(self): + super().setUp() + patches = ( + ( + 'mock_make_dir', + 'olympia.amo.management.commands.data_dump.BaseDataCommand.make_dir', + ), + ( + 'mock_call_command', + 'olympia.amo.management.commands.data_dump.call_command', + ), + ( + 'mock_clean_dir', + 'olympia.amo.management.commands.data_dump.BaseDataCommand.clean_dir', + ), + ) + self.mocks = {} + + for mock_name, mock_path in patches: + patcher = mock.patch(mock_path) + self.addCleanup(patcher.stop) + self.mocks[mock_name] = patcher.start() + + @freeze_time('2023-06-26 11:00:44') + def test_default_name(self): + print('backup', self.backup_dir) + backup_dir = os.path.join(self.backup_dir, '20230626110044') + db_path = os.path.join(backup_dir, self.db_file) + storage_path = os.path.join(backup_dir, self.storage_file) + + call_command('data_dump') + self.mocks['mock_make_dir'].assert_called_with(backup_dir, force=False) + self._assert_commands_called_in_order( + self.mocks['mock_call_command'], + [ + self.mock_commands.db_backup(db_path), + self.mock_commands.media_backup(storage_path), + ], + ) + + def test_custom_name(self): + name = 'test' + backup_dir = os.path.join(self.backup_dir, name) + + call_command('data_dump', name=name) + self.mocks['mock_make_dir'].assert_called_with(backup_dir, force=False) + + def test_failure(self): + name = 'test' + backup_dir = os.path.join(self.backup_dir, name) + self.mocks['mock_call_command'].side_effect = Exception('banana') + + with pytest.raises(Exception) as context: + call_command('data_dump', name=name) + assert 'banana' in str(context.value) + self.mocks['mock_clean_dir'].assert_called_with(backup_dir) + + +class TestLoadDataCommand(BaseTestDataCommand): + def setUp(self): + super().setUp() + + patcher = mock.patch('olympia.amo.management.commands.data_load.call_command') + self.addCleanup(patcher.stop) + self.mock_call_command = patcher.start() + + def test_missing_name(self): + with pytest.raises(CommandError): + call_command('data_load') + + @mock.patch('olympia.amo.management.commands.data_load.os.path.exists') + def test_loads_correct_path(self, mock_exists): + mock_exists.return_value = True + name = 'test_backup' + backup_dir = os.path.join(self.backup_dir, name) + db_path = os.path.join(backup_dir, self.db_file) + storage_path = os.path.join(backup_dir, self.storage_file) + + call_command('data_load', name=name) + + self._assert_commands_called_in_order( + self.mock_call_command, + [ + self.mock_commands.db_restore(db_path), + self.mock_commands.media_restore(storage_path), + self.mock_commands.reindex, + ], + ) + + @mock.patch('olympia.amo.management.commands.data_load.os.path.exists') + def test_data_load_with_missing_db(self, mock_exists): + mock_exists.return_value = False + with pytest.raises(CommandError) as context: + call_command('data_load', name='test_backup') + assert 'DB backup not found' in str(context.value) + + @mock.patch('olympia.amo.management.commands.data_load.os.path.exists') + def test_data_load_with_missing_storage(self, mock_exists): + storage_path = os.path.join(self.backup_dir, 'test_backup', self.storage_file) + + mock_exists.side_effect = lambda path: path != storage_path + + with pytest.raises(CommandError) as context: + call_command('data_load', name='test_backup') + assert 'Storage backup not found' in str(context.value) + + +class TestSeedDataCommand(BaseTestDataCommand): + def setUp(self): + super().setUp() + + patches = ( + ( + 'mock_call_command', + 'olympia.amo.management.commands.data_seed.call_command', + ), + ( + 'mock_clean_dir', + 'olympia.amo.management.commands.data_seed.BaseDataCommand.clean_dir', + ), + ) + + self.mocks = {} + + for mock_name, mock_path in patches: + patcher = mock.patch(mock_path) + self.addCleanup(patcher.stop) + self.mocks[mock_name] = patcher.start() + + def test_default(self): + call_command('data_seed') + + self._assert_commands_called_in_order( + self.mocks['mock_call_command'], + [ + self.mock_commands.flush, + self.mock_commands.reindex, + self.mock_commands.migrate, + self.mock_commands.load_initial_data, + self.mock_commands.import_prod_versions, + self.mock_commands.createsuperuser, + self.mock_commands.load_zadmin_users, + self.mock_commands.generate_addons('firefox', 10), + self.mock_commands.generate_addons('android', 10), + self.mock_commands.generate_themes(5), + self.mock_commands.generate_default_addons_for_frontend, + self.mock_commands.data_dump(self.base_data_command.data_backup_init), + ], + ) diff --git a/src/olympia/blocklist/tests/test_admin.py b/src/olympia/blocklist/tests/test_admin.py index 6b2ebcff60d..b1d3a7a15e0 100644 --- a/src/olympia/blocklist/tests/test_admin.py +++ b/src/olympia/blocklist/tests/test_admin.py @@ -20,6 +20,7 @@ TestCase, addon_factory, block_factory, + collection_factory, # Added collection_factory user_factory, version_factory, ) @@ -1361,14 +1362,14 @@ def test_signoff_approve(self): log_entry = LogEntry.objects.last() assert log_entry.user == user assert log_entry.object_id == str(mbs.id) - other_obj = addon_factory(id=mbs.id) + other_obj = collection_factory(id=mbs.id, name='not a Block!') LogEntry.objects.log_action( user_factory().id, ContentType.objects.get_for_model(other_obj).pk, other_obj.id, repr(other_obj), ADDITION, - 'not a Block!', + str(other_obj), ) response = self.client.get(multi_url, follow=True) @@ -1437,14 +1438,14 @@ def test_signoff_reject(self): log_entry = LogEntry.objects.last() assert log_entry.user == user assert log_entry.object_id == str(mbs.id) - other_obj = addon_factory(id=mbs.id) + other_obj = collection_factory(id=mbs.id, name='not a Block!') LogEntry.objects.log_action( user_factory().id, ContentType.objects.get_for_model(other_obj).pk, other_obj.id, repr(other_obj), ADDITION, - 'not a Block!', + str(other_obj), ) response = self.client.get(multi_url, follow=True) diff --git a/src/olympia/lib/settings_base.py b/src/olympia/lib/settings_base.py index 648b268fcc2..0b570faf15a 100644 --- a/src/olympia/lib/settings_base.py +++ b/src/olympia/lib/settings_base.py @@ -151,6 +151,9 @@ def get_db_config(environ_var, atomic_requests=True): # Put the aliases for your slave databases in this list. REPLICA_DATABASES = [] +LOCAL_ADMIN_EMAIL = 'local_admin@mozilla.com' +LOCAL_ADMIN_USERNAME = 'local_admin' + # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems.