From 708d9601afe43199e1bb63d8a35b9092d88972ee Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Wed, 9 Aug 2023 16:14:37 -0600 Subject: [PATCH 1/8] chore: Fix up readme. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99d4f6d..10aefb6 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Refer to folder and file structure, and usage, for more detailed information. ![Python%203.7%20-%20Django%203.2](https://docs.arrai-dev.com/django-view-manager/artifacts/main/python%203.11%20-%20django%204.1.svg) [![Coverage](https://docs.arrai-dev.com/django-view-manager/artifacts/main/python%203.11%20-%20django%204.1.coverage.svg)](https://docs.arrai-dev.com/django-view-manager/artifacts/main/htmlcov_python%203.11%20-%20django%204.1/) +**Table of Contents** + @@ -112,8 +114,8 @@ The results will be: ```shell $ python manage.py makeviewmigration [-h] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] -                                   [--skip-checks] -                                   {animals_pets,employees_employeelikes,food_sweets} migration_name +                                     [--skip-checks] +                                     {animals_pets,employees_employeelikes,food_sweets} migration_name manage.py makeviewmigration: error: the following arguments are required: db_table_name, migration_name ``` From fd8f0d1c53a61c2046eac8d1c2b3c5ddd005a64d Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Wed, 9 Aug 2023 16:19:12 -0600 Subject: [PATCH 2/8] feat: Add code to modify latest sql filenames in other django-view-manager migrations. --- .../management/commands/makeviewmigration.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/django_view_manager/utils/management/commands/makeviewmigration.py b/django_view_manager/utils/management/commands/makeviewmigration.py index 8ee1cc3..3d1f5ee 100644 --- a/django_view_manager/utils/management/commands/makeviewmigration.py +++ b/django_view_manager/utils/management/commands/makeviewmigration.py @@ -163,7 +163,7 @@ def _parse_migration_number_from_show_migrations(line): @staticmethod def _is_migration_modified(db_table_name, migrations_path, migration_name, num): - with open(os.path.join(migrations_path, f"{migration_name}.py"), "r") as f: + with open(os.path.join(migrations_path, f"{migration_name}.py"), "r", encoding="utf-8") as f: # Did we modify this migration? Check the first 10 lines for our modified comment. found_modified_comment = False for migration_line_no, migration_line in enumerate(f.readlines()): @@ -289,6 +289,37 @@ def _create_latest_sql_file(self, sql_path, db_table_name): self.stdout.write(self.style.SUCCESS(f"\nCreated new SQL view file - '{latest_sql_filename}'.")) + def _find_and_rewrite_migrations_containing_latest( + self, + migration_numbers_and_names, + migrations_path, + latest_sql_filename, + historical_sql_filename, + ): + for migration_name in migration_numbers_and_names.values(): + with open(os.path.join(migrations_path, f"{migration_name}.py"), "r+", encoding="utf-8") as f: + lines = f.readlines() + modified_migration = False + sql_line_no = 0 + for line_no, line in enumerate(lines): + if line.find(latest_sql_filename) != -1: + sql_line_no = line_no + break + + if sql_line_no: + lines[sql_line_no] = lines[sql_line_no].replace(latest_sql_filename, historical_sql_filename) + modified_migration = True + + if modified_migration: + self.stdout.write( + self.style.SUCCESS( + f"\nModified migration '{migration_name}' to read from '{historical_sql_filename}'." + ) + ) + f.seek(0) + f.truncate(0) + f.writelines(lines) + def _rewrite_latest_migration(self, migrations_path, migration_name, latest_sql_filename, historical_sql_filename): with open(os.path.join(migrations_path, migration_name + ".py"), "r+", encoding="utf-8") as f: lines = f.readlines() @@ -436,6 +467,14 @@ def handle(self, *args, **options): ) ) + # Find any additional migrations which should be switched to use the historical sql view filename. + self._find_and_rewrite_migrations_containing_latest( + migration_numbers_and_names, + migrations_path, + latest_sql_filename, + historical_sql_filename, + ) + # Update the empty migration to use the `latest` sql view filename. self._rewrite_migration( migrations_path, From c70a0264e36eaac38425bce9c440872c0b4313bb Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Wed, 9 Aug 2023 16:23:21 -0600 Subject: [PATCH 3/8] chore: Bump the version number. --- VERSION | 2 +- .../utils/management/commands/makeviewmigration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 21e8796..ee90284 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 +1.0.4 diff --git a/django_view_manager/utils/management/commands/makeviewmigration.py b/django_view_manager/utils/management/commands/makeviewmigration.py index 3d1f5ee..da3d03f 100644 --- a/django_view_manager/utils/management/commands/makeviewmigration.py +++ b/django_view_manager/utils/management/commands/makeviewmigration.py @@ -11,7 +11,7 @@ from django.db.transaction import atomic -VERSION = "1.0.3" +VERSION = "1.0.4" COPIED_SQL_VIEW_CONTENT = f"""/* From a771428e56b24c17aa26bfcd584e37a4f3f2a1a8 Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Wed, 9 Aug 2023 16:26:41 -0600 Subject: [PATCH 4/8] test: Create an app and test for multiple migrations that contain latest. --- config/settings/test.py | 1 + tests/employees/tests.py | 9 ++- tests/store/__init__.py | 0 tests/store/apps.py | 6 ++ tests/store/migrations/0001_initial.py | 68 +++++++++++++++++++ .../0002_create_product_calculations_view.py | 24 +++++++ ...3_create_purchased_product_calculations.py | 24 +++++++ ...d_markup_amount_to_product_calculations.py | 37 ++++++++++ tests/store/migrations/__init__.py | 0 tests/store/models.py | 54 +++++++++++++++ .../view-store_productcalculations-0002.sql | 10 +++ .../view-store_productcalculations-latest.sql | 11 +++ ...re_purchasedproductcalculations-latest.sql | 19 ++++++ tests/store/tests.py | 56 +++++++++++++++ tests/test_case.py | 14 ++++ ...3_create_purchased_product_calculations.py | 24 +++++++ ...d_markup_amount_to_product_calculations.py | 37 ++++++++++ tests/test_runner.py | 31 +++++++++ 18 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 tests/store/__init__.py create mode 100644 tests/store/apps.py create mode 100644 tests/store/migrations/0001_initial.py create mode 100644 tests/store/migrations/0002_create_product_calculations_view.py create mode 100644 tests/store/migrations/0003_create_purchased_product_calculations.py create mode 100644 tests/store/migrations/0004_add_markup_amount_to_product_calculations.py create mode 100644 tests/store/migrations/__init__.py create mode 100644 tests/store/models.py create mode 100644 tests/store/sql/view-store_productcalculations-0002.sql create mode 100644 tests/store/sql/view-store_productcalculations-latest.sql create mode 100644 tests/store/sql/view-store_purchasedproductcalculations-latest.sql create mode 100644 tests/store/tests.py create mode 100644 tests/test_data/store/migrations/0003_create_purchased_product_calculations.py create mode 100644 tests/test_data/store/migrations/0004_add_markup_amount_to_product_calculations.py diff --git a/config/settings/test.py b/config/settings/test.py index ece2b6d..012a138 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -36,6 +36,7 @@ "tests.animals", "tests.employees", "tests.food", + "tests.store", "django_view_manager.utils", ] diff --git a/tests/employees/tests.py b/tests/employees/tests.py index 14a060a..cd554bf 100644 --- a/tests/employees/tests.py +++ b/tests/employees/tests.py @@ -15,13 +15,18 @@ def test_no_args(self): ) # Depending on the width of your console, migration_name may be on the # same line as the db_table_name, or it may wrap it onto the next line. - self.assertIn("{animals_pets,employees_employeelikes,food_sweets} migration_name", " ".join(err)) + self.assertIn( + "{animals_pets,employees_employeelikes,food_sweets," + "store_productcalculations,store_purchasedproductcalculations} migration_name", + " ".join(err), + ) def test_bad_db_table_name(self): out, err = self.call_command(["manage.py", "makeviewmigration", "employees_employeehates", "create_view"]) self.assertIn( "manage.py makeviewmigration: error: argument db_table_name: invalid choice: 'employees_" - "employeehates' (choose from 'animals_pets', 'employees_employeelikes', 'food_sweets')", + "employeehates' (choose from 'animals_pets', 'employees_employeelikes', 'food_sweets', " + "'store_productcalculations', 'store_purchasedproductcalculations')", err, ) diff --git a/tests/store/__init__.py b/tests/store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/store/apps.py b/tests/store/apps.py new file mode 100644 index 0000000..a8e9c86 --- /dev/null +++ b/tests/store/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StoreConfig(AppConfig): + name = "tests.store" + verbose_name = "Store" diff --git a/tests/store/migrations/0001_initial.py b/tests/store/migrations/0001_initial.py new file mode 100644 index 0000000..dcce654 --- /dev/null +++ b/tests/store/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.17 on 2023-08-09 15:08 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ProductCalculations", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("markup_price", models.DecimalField(decimal_places=2, max_digits=9)), + ], + options={ + "db_table": "store_productcalculations", + "managed": False, + "default_related_name": "calculations", + }, + ), + migrations.CreateModel( + name="PurchasedProductCalculations", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("profit", models.DecimalField(decimal_places=2, max_digits=9)), + ], + options={ + "db_table": "store_purchasedproductcalculations", + "managed": False, + "default_related_name": "calculations", + }, + ), + migrations.CreateModel( + name="Products", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=1024)), + ("price", models.DecimalField(decimal_places=2, max_digits=9)), + ("markup_percentage", models.IntegerField()), + ], + options={ + "ordering": ("name",), + "default_related_name": "products", + }, + ), + migrations.CreateModel( + name="PurchasedProducts", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("quantity", models.IntegerField()), + ( + "product", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, related_name="order_items", to="store.products" + ), + ), + ], + options={ + "ordering": ("product__name",), + "default_related_name": "order_items", + }, + ), + ] diff --git a/tests/store/migrations/0002_create_product_calculations_view.py b/tests/store/migrations/0002_create_product_calculations_view.py new file mode 100644 index 0000000..e3018d5 --- /dev/null +++ b/tests/store/migrations/0002_create_product_calculations_view.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-08-09 15:08 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/store/sql" +forward_sql_filename = "view-store_productcalculations-0002.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0001_initial"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql="DROP VIEW IF EXISTS store_productcalculations;", + ), + ] diff --git a/tests/store/migrations/0003_create_purchased_product_calculations.py b/tests/store/migrations/0003_create_purchased_product_calculations.py new file mode 100644 index 0000000..a8f64dd --- /dev/null +++ b/tests/store/migrations/0003_create_purchased_product_calculations.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-08-09 15:08 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/store/sql" +forward_sql_filename = "view-store_purchasedproductcalculations-latest.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0002_create_product_calculations_view"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql="DROP VIEW IF EXISTS store_purchasedproductcalculations;", + ), + ] diff --git a/tests/store/migrations/0004_add_markup_amount_to_product_calculations.py b/tests/store/migrations/0004_add_markup_amount_to_product_calculations.py new file mode 100644 index 0000000..5ba8c6e --- /dev/null +++ b/tests/store/migrations/0004_add_markup_amount_to_product_calculations.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.17 on 2023-08-09 15:10 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/store/sql" +forward_sql_filename = "view-store_productcalculations-latest.sql" +reverse_sql_filename = "view-store_productcalculations-0002.sql" +sql_filename_which_uses_this_view = "view-store_purchasedproductcalculations-latest.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + +with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f: + reverse_sql = f.read() + +with open(os.path.join(sql_path, sql_filename_which_uses_this_view), mode="r") as f: + sql_which_uses_this_view = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0003_create_purchased_product_calculations"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql=reverse_sql, + ), + # We need to set back up the view that was dropped. + migrations.RunSQL( + sql=sql_which_uses_this_view, + reverse_sql=sql_which_uses_this_view, + ), + ] diff --git a/tests/store/migrations/__init__.py b/tests/store/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/store/models.py b/tests/store/models.py new file mode 100644 index 0000000..7ffde41 --- /dev/null +++ b/tests/store/models.py @@ -0,0 +1,54 @@ +from django.db import models +from django.db.models.fields import related + + +class Products(models.Model): + name = models.CharField(max_length=1024) + price = models.DecimalField(decimal_places=2, max_digits=9) + markup_percentage = models.IntegerField() + + class Meta: + ordering = ("name",) + default_related_name = "products" + + def __str__(self): + return f"{self.name} is a product." + + +class ProductCalculations(models.Model): + product = related.OneToOneField("store.Products", on_delete=models.DO_NOTHING) + markup_price = models.DecimalField(decimal_places=2, max_digits=9) + markup_amount = models.DecimalField(decimal_places=2, max_digits=9) + + class Meta: + managed = False + db_table = "store_productcalculations" + default_related_name = "calculations" + + def __str__(self): + return f"{self.sale_price} is the sale price." + + +class PurchasedProducts(models.Model): + product = models.OneToOneField("store.Products", on_delete=models.PROTECT) + quantity = models.IntegerField() + + class Meta: + ordering = ("product__name",) + default_related_name = "order_items" + + def __str__(self): + return f"{self.order_number} is an order." + + +class PurchasedProductCalculations(models.Model): + purchased_product = models.OneToOneField("store.PurchasedProducts", on_delete=models.PROTECT) + profit = models.DecimalField(decimal_places=2, max_digits=9) + + class Meta: + managed = False + db_table = "store_purchasedproductcalculations" + default_related_name = "calculations" + + def __str__(self): + return f"{self.profit} is the profit." diff --git a/tests/store/sql/view-store_productcalculations-0002.sql b/tests/store/sql/view-store_productcalculations-0002.sql new file mode 100644 index 0000000..4ea2cfd --- /dev/null +++ b/tests/store/sql/view-store_productcalculations-0002.sql @@ -0,0 +1,10 @@ +DROP VIEW IF EXISTS store_purchasedproductcalculations; +DROP VIEW IF EXISTS store_productcalculations; +CREATE VIEW store_productcalculations AS +SELECT + P.id, + P.id AS product_id, + P.price * (1 + (P.markup_percentage / 100)) AS markup_price +FROM + store_products P +; diff --git a/tests/store/sql/view-store_productcalculations-latest.sql b/tests/store/sql/view-store_productcalculations-latest.sql new file mode 100644 index 0000000..efb9f52 --- /dev/null +++ b/tests/store/sql/view-store_productcalculations-latest.sql @@ -0,0 +1,11 @@ +DROP VIEW IF EXISTS store_purchasedproductcalculations; +DROP VIEW IF EXISTS store_productcalculations; +CREATE VIEW store_productcalculations AS +SELECT + P.id, + P.id AS product_id, + P.price * (1 + (P.markup_percentage / 100)) AS markup_price, + P.price * (P.markup_percentage / 100) AS markup_amount +FROM + store_products P +; diff --git a/tests/store/sql/view-store_purchasedproductcalculations-latest.sql b/tests/store/sql/view-store_purchasedproductcalculations-latest.sql new file mode 100644 index 0000000..90b805b --- /dev/null +++ b/tests/store/sql/view-store_purchasedproductcalculations-latest.sql @@ -0,0 +1,19 @@ +/* + This file was generated using django-view-manager 1.0.4. + Modify the SQL for this view and then commit the changes. + You can remove this comment before committing. + + When you have changes to make to this sql, you need to run the makeviewmigration command + before altering the sql, so the historical sql file is created with the correct contents. +*/ +DROP VIEW IF EXISTS store_purchasedproductcalculations; +CREATE VIEW store_purchasedproductcalculations AS +SELECT + PP.id, + PP.id AS order_item_id, + (PC.markup_price - P.price) * PP.quantity AS profit +FROM + store_purchasedproducts PP + JOIN store_productcalculations PC ON PP.purchased_product_id = PC.product_id + JOIN store_products P ON P.id = PP.product_id +; diff --git a/tests/store/tests.py b/tests/store/tests.py new file mode 100644 index 0000000..21b737b --- /dev/null +++ b/tests/store/tests.py @@ -0,0 +1,56 @@ +import os + +from tests.test_case import ManagementCommandTestCase + + +# Tests cannot be run in parallel. +class StoreTestCase(ManagementCommandTestCase): + maxDiff = None + + def test_multiple_files_associated_to_latest(self): + out, err = self.call_command( + ["manage.py", "makeviewmigration", "store_purchasedproductcalculations", "modify_view"] + ) + self.assertTupleEqual(err, ()) + self.assertTupleEqual( + out, + ( + "", + "Creating empty migration for the SQL changes.", + "Migrations for 'store':", + "tests/store/migrations/0005_modify_view.py", + "- Raw SQL operation", + "", + "Created historical SQL view file - 'view-store_purchasedproductcalculations-0004.sql'.", + "", + "Modified migration '0004_add_markup_amount_to_product_calculations' to read from " + "'view-store_purchasedproductcalculations-0004.sql'.", + "", + "Modified migration '0003_create_purchased_product_calculations' to read from " + "'view-store_purchasedproductcalculations-0004.sql'.", + "", + "Modified migration '0005_modify_view' to read from 'view-store_purchasedproductcalculations-" + "latest.sql' and 'view-store_purchasedproductcalculations-0004.sql'.", + "", + "Done - You can now edit 'view-store_purchasedproductcalculations-latest.sql'.", + "", + ), + ) + self.assertTrue(os.path.exists("tests/store/migrations/0005_modify_view.py")) + self.assertTrue(os.path.exists("tests/store/sql/view-store_purchasedproductcalculations-0004.sql")) + + with open( + os.path.join("tests", "store", "migrations", "0003_create_purchased_product_calculations.py"), + "r", + encoding="utf-8", + ) as f: + lines = f.readlines() + self.assertIn('forward_sql_filename = "view-store_purchasedproductcalculations-0004.sql"\n', lines) + + with open( + os.path.join("tests", "store", "migrations", "0004_add_markup_amount_to_product_calculations.py"), + "r", + encoding="utf-8", + ) as f: + lines = f.readlines() + self.assertIn('sql_filename_which_uses_this_view = "view-store_purchasedproductcalculations-0004.sql"\n', lines) diff --git a/tests/test_case.py b/tests/test_case.py index 3cb0beb..8d7dce4 100644 --- a/tests/test_case.py +++ b/tests/test_case.py @@ -11,6 +11,20 @@ # Tests cannot be run in parallel. class ManagementCommandTestCase(TransactionTestCase): + def tearDown(self): + super().tearDown() + + # Clean up sys.modules between tests, because some tests delete migrations or the migration folder. + keys_to_delete = set() + keys_to_find = ("tests.animals", "tests.employees", "tests.food", "tests.store") + for key in sys.modules.keys(): + for key_to_find in keys_to_find: + if key_to_find in key: + keys_to_delete.add(key) + + for key in keys_to_delete: + del sys.modules[key] + def call_command(self, command): self.stderr = err = io.StringIO() self.stdout = out = io.StringIO() diff --git a/tests/test_data/store/migrations/0003_create_purchased_product_calculations.py b/tests/test_data/store/migrations/0003_create_purchased_product_calculations.py new file mode 100644 index 0000000..a8f64dd --- /dev/null +++ b/tests/test_data/store/migrations/0003_create_purchased_product_calculations.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-08-09 15:08 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/store/sql" +forward_sql_filename = "view-store_purchasedproductcalculations-latest.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0002_create_product_calculations_view"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql="DROP VIEW IF EXISTS store_purchasedproductcalculations;", + ), + ] diff --git a/tests/test_data/store/migrations/0004_add_markup_amount_to_product_calculations.py b/tests/test_data/store/migrations/0004_add_markup_amount_to_product_calculations.py new file mode 100644 index 0000000..5ba8c6e --- /dev/null +++ b/tests/test_data/store/migrations/0004_add_markup_amount_to_product_calculations.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.17 on 2023-08-09 15:10 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/store/sql" +forward_sql_filename = "view-store_productcalculations-latest.sql" +reverse_sql_filename = "view-store_productcalculations-0002.sql" +sql_filename_which_uses_this_view = "view-store_purchasedproductcalculations-latest.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + +with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f: + reverse_sql = f.read() + +with open(os.path.join(sql_path, sql_filename_which_uses_this_view), mode="r") as f: + sql_which_uses_this_view = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0003_create_purchased_product_calculations"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql=reverse_sql, + ), + # We need to set back up the view that was dropped. + migrations.RunSQL( + sql=sql_which_uses_this_view, + reverse_sql=sql_which_uses_this_view, + ), + ] diff --git a/tests/test_runner.py b/tests/test_runner.py index 4b55dc8..7e3eda5 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -25,6 +25,8 @@ def clean_up_files(): "tests/food/migrations/0003_change_something.py", "tests/food/sql/view-food_sweets-0002.sql", "tests/food/sql/view-food_sweets-latest.sql", + "tests/store/migrations/0005_modify_view.py", + "tests/store/sql/view-store_purchasedproductcalculations-0004.sql", ) for path in files_to_delete: @@ -47,6 +49,34 @@ def clean_up_folders(): shutil.rmtree(path) +def replace_files(): + with open( + os.path.join("tests", "test_data", "store", "migrations", "0003_create_purchased_product_calculations.py"), + "r", + encoding="utf-8", + ) as f_in: + content = f_in.read() + with open( + os.path.join("tests", "store", "migrations", "0003_create_purchased_product_calculations.py"), + "w", + encoding="utf-8", + ) as f_out: + f_out.write(content) + + with open( + os.path.join("tests", "test_data", "store", "migrations", "0004_add_markup_amount_to_product_calculations.py"), + "r", + encoding="utf-8", + ) as f_in: + content = f_in.read() + with open( + os.path.join("tests", "store", "migrations", "0004_add_markup_amount_to_product_calculations.py"), + "w", + encoding="utf-8", + ) as f_out: + f_out.write(content) + + class TestRunner(DiscoverRunner): parallel = 0 interactive = False @@ -60,3 +90,4 @@ def teardown_databases(self, old_config, **kwargs): # We do this cleanup, so you can locally run tests. clean_up_files() clean_up_folders() + replace_files() From a57f45733de0d1a2b3a40d4244d5b11e90726306 Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Fri, 11 Aug 2023 16:40:32 -0600 Subject: [PATCH 5/8] feat: Add code to handle migrations with the same number. Closes #5. --- .../management/commands/makeviewmigration.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/django_view_manager/utils/management/commands/makeviewmigration.py b/django_view_manager/utils/management/commands/makeviewmigration.py index da3d03f..7cfd8d3 100644 --- a/django_view_manager/utils/management/commands/makeviewmigration.py +++ b/django_view_manager/utils/management/commands/makeviewmigration.py @@ -221,29 +221,32 @@ def _get_sql_numbers_and_names(sql_path, db_table_name): if filename.startswith(view_name_start) and filename.endswith(view_name_end): sql_file_num = filename.replace(view_name_start, "").replace(view_name_end, "") + sql_file_name = None + if "-" in sql_file_num: + sql_file_num, sql_file_name = sql_file_num.split("-") + # Convert the number to an int, so we can max them. if sql_file_num == LATEST_VIEW_NAME: - sql_numbers_and_names[LATEST_VIEW_NUMBER] = filename + sql_numbers_and_names[(LATEST_VIEW_NUMBER, sql_file_name)] = filename elif sql_file_num.isdigit(): - sql_numbers_and_names[decimal.Decimal(sql_file_num)] = filename + sql_numbers_and_names[(decimal.Decimal(sql_file_num), sql_file_name)] = filename return sql_numbers_and_names @staticmethod def _get_latest_migration_number_and_name(migration_numbers_and_names, sql_numbers_and_names): - largest_migration_number = decimal.Decimal(0) - latest_sql_number = None + largest_migration_number = latest_sql_number = None - for migration_number in migration_numbers_and_names: - largest_migration_number = migration_number + for migration_number, migration_name in migration_numbers_and_names.items(): + largest_migration_number = (migration_number, migration_name) - for sql_number in sql_numbers_and_names: + for sql_number, sql_name in sql_numbers_and_names: if sql_number is LATEST_VIEW_NUMBER: - latest_sql_number = sql_number + latest_sql_number = (sql_number, sql_name) if largest_migration_number and latest_sql_number is not None: - return largest_migration_number, migration_numbers_and_names[largest_migration_number] + return largest_migration_number return None, None @@ -398,6 +401,20 @@ def _rewrite_migration( f.seek(0) f.writelines(lines) + def _get_historical_sql_filename( + self, db_table_name, latest_migration_number, latest_migration_name, sql_numbers_and_names + ): + historical_sql_filename = f"view-{db_table_name}-{str(latest_migration_number).zfill(4)}.sql" + # Do multiple migrations with the same number exist? + # If so, we need to include the migration name in the sql view name. + if historical_sql_filename in sql_numbers_and_names.values(): + latest_migration_name = latest_migration_name.split("_", 1)[1] # Remove the migration number. + historical_sql_filename = ( + f"view-{db_table_name}-{str(latest_migration_number).zfill(4)}-{latest_migration_name}.sql" + ) + + return historical_sql_filename + @atomic def handle(self, *args, **options): # Get passed in args. @@ -452,7 +469,9 @@ def handle(self, *args, **options): # Is there a `latest` SQL view and migration? if latest_migration_number is not None and latest_migration_name is not None: latest_sql_filename = f"view-{db_table_name}-{LATEST_VIEW_NAME}.sql" - historical_sql_filename = f"view-{db_table_name}-{str(latest_migration_number).zfill(4)}.sql" + historical_sql_filename = self._get_historical_sql_filename( + db_table_name, latest_migration_number, latest_migration_name, sql_numbers_and_names + ) # Copy the `latest` SQL view to match the latest migration number. self._copy_latest_sql_view(sql_path, latest_sql_filename, historical_sql_filename) From 501b3801faa90d7a73959c67b90257f8e74da117 Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Fri, 11 Aug 2023 16:42:03 -0600 Subject: [PATCH 6/8] test: Add test for migrations with the same number. --- config/settings/test.py | 1 + tests/employees/tests.py | 4 +- tests/musicians/__init__.py | 0 tests/musicians/apps.py | 6 ++ tests/musicians/migrations/0001_initial.py | 61 +++++++++++++++++++ .../migrations/0002_create_band_info_view.py | 24 ++++++++ .../migrations/0003_add_founded_date.py | 28 +++++++++ .../migrations/0003_add_member_count.py | 28 +++++++++ ..._add_founded_date_0003_add_member_count.py | 12 ++++ tests/musicians/migrations/__init__.py | 0 tests/musicians/models.py | 42 +++++++++++++ tests/musicians/sql/view-band_info-0002.sql | 8 +++ tests/musicians/sql/view-band_info-0003.sql | 11 ++++ tests/musicians/sql/view-band_info-latest.sql | 12 ++++ tests/musicians/tests.py | 54 ++++++++++++++++ .../migrations/0003_add_founded_date.py | 28 +++++++++ .../musicians/sql/view-band_info-latest.sql | 12 ++++ tests/test_runner.py | 41 ++++++------- 18 files changed, 347 insertions(+), 25 deletions(-) create mode 100644 tests/musicians/__init__.py create mode 100644 tests/musicians/apps.py create mode 100644 tests/musicians/migrations/0001_initial.py create mode 100644 tests/musicians/migrations/0002_create_band_info_view.py create mode 100644 tests/musicians/migrations/0003_add_founded_date.py create mode 100644 tests/musicians/migrations/0003_add_member_count.py create mode 100644 tests/musicians/migrations/0004_merge_0003_add_founded_date_0003_add_member_count.py create mode 100644 tests/musicians/migrations/__init__.py create mode 100644 tests/musicians/models.py create mode 100644 tests/musicians/sql/view-band_info-0002.sql create mode 100644 tests/musicians/sql/view-band_info-0003.sql create mode 100644 tests/musicians/sql/view-band_info-latest.sql create mode 100644 tests/musicians/tests.py create mode 100644 tests/test_data/musicians/migrations/0003_add_founded_date.py create mode 100644 tests/test_data/musicians/sql/view-band_info-latest.sql diff --git a/config/settings/test.py b/config/settings/test.py index 012a138..000fbe3 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -36,6 +36,7 @@ "tests.animals", "tests.employees", "tests.food", + "tests.musicians", "tests.store", "django_view_manager.utils", ] diff --git a/tests/employees/tests.py b/tests/employees/tests.py index cd554bf..9075ccc 100644 --- a/tests/employees/tests.py +++ b/tests/employees/tests.py @@ -16,7 +16,7 @@ def test_no_args(self): # Depending on the width of your console, migration_name may be on the # same line as the db_table_name, or it may wrap it onto the next line. self.assertIn( - "{animals_pets,employees_employeelikes,food_sweets," + "{animals_pets,band_info,employees_employeelikes,food_sweets," "store_productcalculations,store_purchasedproductcalculations} migration_name", " ".join(err), ) @@ -25,7 +25,7 @@ def test_bad_db_table_name(self): out, err = self.call_command(["manage.py", "makeviewmigration", "employees_employeehates", "create_view"]) self.assertIn( "manage.py makeviewmigration: error: argument db_table_name: invalid choice: 'employees_" - "employeehates' (choose from 'animals_pets', 'employees_employeelikes', 'food_sweets', " + "employeehates' (choose from 'animals_pets', 'band_info', 'employees_employeelikes', 'food_sweets', " "'store_productcalculations', 'store_purchasedproductcalculations')", err, ) diff --git a/tests/musicians/__init__.py b/tests/musicians/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/musicians/apps.py b/tests/musicians/apps.py new file mode 100644 index 0000000..583db18 --- /dev/null +++ b/tests/musicians/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FoodConfig(AppConfig): + name = "tests.musicians" + verbose_name = "Musicians" diff --git a/tests/musicians/migrations/0001_initial.py b/tests/musicians/migrations/0001_initial.py new file mode 100644 index 0000000..f366162 --- /dev/null +++ b/tests/musicians/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 3.2.17 on 2023-08-11 16:16 + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="BandInfo", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=1024)), + ("genre", models.CharField(max_length=1024)), + ], + options={ + "db_table": "band_info", + "ordering": ("name",), + "managed": False, + "default_related_name": "musicians_bandinfo", + }, + ), + migrations.CreateModel( + name="Bands", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=1024)), + ("genre", models.CharField(max_length=1024)), + ("formed", models.DateField()), + ], + options={ + "ordering": ("name",), + "default_related_name": "bands", + }, + ), + migrations.CreateModel( + name="BandMembers", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=1024)), + ("active_member", models.BooleanField(default=True)), + ( + "band", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="band_members", + to="musicians.bands", + ), + ), + ], + options={ + "ordering": ("name",), + "default_related_name": "band_members", + }, + ), + ] diff --git a/tests/musicians/migrations/0002_create_band_info_view.py b/tests/musicians/migrations/0002_create_band_info_view.py new file mode 100644 index 0000000..4e5ee1d --- /dev/null +++ b/tests/musicians/migrations/0002_create_band_info_view.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-08-11 16:17 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/musicians/sql" +forward_sql_filename = "view-band_info-0002.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("musicians", "0001_initial"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql="DROP VIEW IF EXISTS band_info;", + ), + ] diff --git a/tests/musicians/migrations/0003_add_founded_date.py b/tests/musicians/migrations/0003_add_founded_date.py new file mode 100644 index 0000000..560b035 --- /dev/null +++ b/tests/musicians/migrations/0003_add_founded_date.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.17 on 2023-08-11 16:30 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/musicians/sql" +forward_sql_filename = "view-band_info-latest.sql" +reverse_sql_filename = "view-band_info-0003.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + +with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f: + reverse_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("musicians", "0002_create_band_info_view"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql=reverse_sql, + ), + ] diff --git a/tests/musicians/migrations/0003_add_member_count.py b/tests/musicians/migrations/0003_add_member_count.py new file mode 100644 index 0000000..05f04b4 --- /dev/null +++ b/tests/musicians/migrations/0003_add_member_count.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.17 on 2023-08-11 16:21 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/musicians/sql" +forward_sql_filename = "view-band_info-0003.sql" +reverse_sql_filename = "view-band_info-0002.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + +with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f: + reverse_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("musicians", "0002_create_band_info_view"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql=reverse_sql, + ), + ] diff --git a/tests/musicians/migrations/0004_merge_0003_add_founded_date_0003_add_member_count.py b/tests/musicians/migrations/0004_merge_0003_add_founded_date_0003_add_member_count.py new file mode 100644 index 0000000..079faa8 --- /dev/null +++ b/tests/musicians/migrations/0004_merge_0003_add_founded_date_0003_add_member_count.py @@ -0,0 +1,12 @@ +# Generated by Django 3.2.17 on 2023-08-11 16:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("musicians", "0003_add_founded_date"), + ("musicians", "0003_add_member_count"), + ] + + operations = [] diff --git a/tests/musicians/migrations/__init__.py b/tests/musicians/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/musicians/models.py b/tests/musicians/models.py new file mode 100644 index 0000000..a5a8eaa --- /dev/null +++ b/tests/musicians/models.py @@ -0,0 +1,42 @@ +from django.db import models +from django.db.models.fields import related + + +class Bands(models.Model): + name = models.CharField(max_length=1024) + genre = models.CharField(max_length=1024) + formed = models.DateField() + + class Meta: + ordering = ("name",) + default_related_name = "bands" + + def __str__(self): + return f"{self.name} is a band." + + +class BandMembers(models.Model): + band = related.ForeignKey("musicians.Bands", on_delete=models.DO_NOTHING) + name = models.CharField(max_length=1024) + active_member = models.BooleanField(default=True) + + class Meta: + ordering = ("name",) + default_related_name = "band_members" + + def __str__(self): + return f"{self.name} is in a band." + + +class BandInfo(models.Model): + name = models.CharField(max_length=1024) + genre = models.CharField(max_length=1024) + + class Meta: + managed = False + db_table = "band_info" + ordering = ("name",) + default_related_name = "musicians_bandinfo" + + def __str__(self): + return f"{self.name} is info about a band." diff --git a/tests/musicians/sql/view-band_info-0002.sql b/tests/musicians/sql/view-band_info-0002.sql new file mode 100644 index 0000000..466b84b --- /dev/null +++ b/tests/musicians/sql/view-band_info-0002.sql @@ -0,0 +1,8 @@ +DROP VIEW IF EXISTS musicians_bandinfo; +CREATE VIEW musicians_bandinfo AS +SELECT + B.id, + B.name +FROM + musicians_bands B +; diff --git a/tests/musicians/sql/view-band_info-0003.sql b/tests/musicians/sql/view-band_info-0003.sql new file mode 100644 index 0000000..b022ca9 --- /dev/null +++ b/tests/musicians/sql/view-band_info-0003.sql @@ -0,0 +1,11 @@ +DROP VIEW IF EXISTS musicians_bandinfo; +CREATE VIEW musicians_bandinfo AS +SELECT + B.id, + B.name, + count(M.name) as member_count +FROM + musicians_bands B + LEFT JOIN musicians_bandmembers M + ON M.band_id = B.id +; diff --git a/tests/musicians/sql/view-band_info-latest.sql b/tests/musicians/sql/view-band_info-latest.sql new file mode 100644 index 0000000..8f7ceaa --- /dev/null +++ b/tests/musicians/sql/view-band_info-latest.sql @@ -0,0 +1,12 @@ +DROP VIEW IF EXISTS musicians_bandinfo; +CREATE VIEW musicians_bandinfo AS +SELECT + B.id, + B.name, + B.formed, + count(M.name) as member_count +FROM + musicians_bands B + LEFT JOIN musicians_bandmembers M + ON M.band_id = B.id +; diff --git a/tests/musicians/tests.py b/tests/musicians/tests.py new file mode 100644 index 0000000..96df709 --- /dev/null +++ b/tests/musicians/tests.py @@ -0,0 +1,54 @@ +import os + +from tests.test_case import ManagementCommandTestCase + + +# Tests cannot be run in parallel. +class StoreTestCase(ManagementCommandTestCase): + maxDiff = None + + def test_migrations_with_the_same_number(self): + out, err = self.call_command(["manage.py", "makeviewmigration", "band_info", "add_album_count"]) + + self.assertTupleEqual(err, ()) + + print(out) + + self.assertTupleEqual( + out, + ( + "", + "Creating empty migration for the SQL changes.", + "Migrations for 'musicians':", + "tests/musicians/migrations/0005_add_album_count.py", + "- Raw SQL operation", + "", + "Created historical SQL view file - 'view-band_info-0003-add_founded_date.sql'.", + "", + "Modified migration '0003_add_founded_date' to read from 'view-band_info-0003-add_founded_date.sql'.", + "", + "Modified migration '0005_add_album_count' to read from" + " 'view-band_info-latest.sql' and 'view-band_info-0003-add_founded_date.sql'.", + "", + "Done - You can now edit 'view-band_info-latest.sql'.", + "", + ), + ) + self.assertTrue(os.path.exists("tests/musicians/migrations/0005_add_album_count.py")) + self.assertTrue(os.path.exists("tests/musicians/sql/view-band_info-0003-add_founded_date.sql")) + + with open( + os.path.join("tests", "musicians", "migrations", "0003_add_founded_date.py"), + "r", + encoding="utf-8", + ) as f: + lines = f.readlines() + self.assertIn('forward_sql_filename = "view-band_info-0003-add_founded_date.sql"\n', lines) + + with open( + os.path.join("tests", "musicians", "migrations", "0005_add_album_count.py"), + "r", + encoding="utf-8", + ) as f: + lines = f.readlines() + self.assertIn('reverse_sql_filename = "view-band_info-0003-add_founded_date.sql"\n', lines) diff --git a/tests/test_data/musicians/migrations/0003_add_founded_date.py b/tests/test_data/musicians/migrations/0003_add_founded_date.py new file mode 100644 index 0000000..560b035 --- /dev/null +++ b/tests/test_data/musicians/migrations/0003_add_founded_date.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.17 on 2023-08-11 16:30 +# Modified using django-view-manager 1.0.4. Please do not delete this comment. +import os + +from django.db import migrations + +sql_path = "tests/musicians/sql" +forward_sql_filename = "view-band_info-latest.sql" +reverse_sql_filename = "view-band_info-0003.sql" + +with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f: + forwards_sql = f.read() + +with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f: + reverse_sql = f.read() + + +class Migration(migrations.Migration): + dependencies = [ + ("musicians", "0002_create_band_info_view"), + ] + + operations = [ + migrations.RunSQL( + sql=forwards_sql, + reverse_sql=reverse_sql, + ), + ] diff --git a/tests/test_data/musicians/sql/view-band_info-latest.sql b/tests/test_data/musicians/sql/view-band_info-latest.sql new file mode 100644 index 0000000..8f7ceaa --- /dev/null +++ b/tests/test_data/musicians/sql/view-band_info-latest.sql @@ -0,0 +1,12 @@ +DROP VIEW IF EXISTS musicians_bandinfo; +CREATE VIEW musicians_bandinfo AS +SELECT + B.id, + B.name, + B.formed, + count(M.name) as member_count +FROM + musicians_bands B + LEFT JOIN musicians_bandmembers M + ON M.band_id = B.id +; diff --git a/tests/test_runner.py b/tests/test_runner.py index 7e3eda5..5cc0dff 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -27,6 +27,8 @@ def clean_up_files(): "tests/food/sql/view-food_sweets-latest.sql", "tests/store/migrations/0005_modify_view.py", "tests/store/sql/view-store_purchasedproductcalculations-0004.sql", + "tests/musicians/migrations/0005_add_album_count.py", + "tests/musicians/sql/view-band_info-0003-add_founded_date.sql", ) for path in files_to_delete: @@ -50,31 +52,24 @@ def clean_up_folders(): def replace_files(): - with open( - os.path.join("tests", "test_data", "store", "migrations", "0003_create_purchased_product_calculations.py"), - "r", - encoding="utf-8", - ) as f_in: - content = f_in.read() + for filename in ( + os.path.join("store", "migrations", "0003_create_purchased_product_calculations.py"), + os.path.join("store", "migrations", "0004_add_markup_amount_to_product_calculations.py"), + os.path.join("musicians", "migrations", "0003_add_founded_date.py"), + os.path.join("musicians", "sql", "view-band_info-latest.sql"), + ): with open( - os.path.join("tests", "store", "migrations", "0003_create_purchased_product_calculations.py"), - "w", + os.path.join("tests", "test_data", filename), + "r", encoding="utf-8", - ) as f_out: - f_out.write(content) - - with open( - os.path.join("tests", "test_data", "store", "migrations", "0004_add_markup_amount_to_product_calculations.py"), - "r", - encoding="utf-8", - ) as f_in: - content = f_in.read() - with open( - os.path.join("tests", "store", "migrations", "0004_add_markup_amount_to_product_calculations.py"), - "w", - encoding="utf-8", - ) as f_out: - f_out.write(content) + ) as f_in: + content = f_in.read() + with open( + os.path.join("tests", filename), + "w", + encoding="utf-8", + ) as f_out: + f_out.write(content) class TestRunner(DiscoverRunner): From f6284e2a3c5a62b24289809f7ae95de8d2ce480c Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Fri, 11 Aug 2023 17:02:16 -0600 Subject: [PATCH 7/8] docs: Updated docs to mention the new capabilities. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 10aefb6..13bc5d7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A management command for django, designed to provide a way in pull requests, to see a diff of the sql (`CREATE VIEW ...`) for unmanaged models. +Capable of handling migrations with the same number (refer to the musicians app and test) and partially capable of handing views that use other views. Views using views requires some manual additions to migrations, so it can drop required views and set them back up (refer to migration 0004 in the store app for how this can be accomplished). Once set up, it is capable of updating the sql view name in any migration that uses it. + The management command creates an `sql` folder inside an app, along with files like `view-animals_pets-latest.sql` (live) and `view-animals_pets-0002.sql` (historical), where you write your sql. Migrations are also created in the process, which read these files, so you don't need to create them yourself. Refer to folder and file structure, and usage, for more detailed information. From a2dc1f80330892273a65277e1f22b63aaca724ac Mon Sep 17 00:00:00 2001 From: Jayden Kneller Date: Mon, 14 Aug 2023 09:37:35 -0600 Subject: [PATCH 8/8] test: Add to a test so we verify parsing filenames with a number dash name. --- tests/musicians/tests.py | 46 +++++++++++++++++++++++++++++++++++++--- tests/test_runner.py | 2 ++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/musicians/tests.py b/tests/musicians/tests.py index 96df709..593af15 100644 --- a/tests/musicians/tests.py +++ b/tests/musicians/tests.py @@ -11,9 +11,6 @@ def test_migrations_with_the_same_number(self): out, err = self.call_command(["manage.py", "makeviewmigration", "band_info", "add_album_count"]) self.assertTupleEqual(err, ()) - - print(out) - self.assertTupleEqual( out, ( @@ -52,3 +49,46 @@ def test_migrations_with_the_same_number(self): ) as f: lines = f.readlines() self.assertIn('reverse_sql_filename = "view-band_info-0003-add_founded_date.sql"\n', lines) + self.assertIn('forward_sql_filename = "view-band_info-latest.sql"\n', lines) + + # Make another view migration, so we can test that it parses the sql view filename correctly. + out, err = self.call_command(["manage.py", "makeviewmigration", "band_info", "add_member_list"]) + self.assertTupleEqual(err, ()) + self.assertTupleEqual( + out, + ( + "", + "Creating empty migration for the SQL changes.", + "Migrations for 'musicians':", + "tests/musicians/migrations/0006_add_member_list.py", + "- Raw SQL operation", + "", + "Created historical SQL view file - 'view-band_info-0005.sql'.", + "", + "Modified migration '0005_add_album_count' to read from 'view-band_info-0005.sql'.", + "", + "Modified migration '0006_add_member_list' to read from" + " 'view-band_info-latest.sql' and 'view-band_info-0005.sql'.", + "", + "Done - You can now edit 'view-band_info-latest.sql'.", + "", + ), + ) + self.assertTrue(os.path.exists("tests/musicians/migrations/0006_add_member_list.py")) + self.assertTrue(os.path.exists("tests/musicians/sql/view-band_info-0005.sql")) + + with open( + os.path.join("tests", "musicians", "migrations", "0005_add_album_count.py"), + "r", + encoding="utf-8", + ) as f: + lines = f.readlines() + self.assertIn('forward_sql_filename = "view-band_info-0005.sql"\n', lines) + + with open( + os.path.join("tests", "musicians", "migrations", "0006_add_member_list.py"), + "r", + encoding="utf-8", + ) as f: + lines = f.readlines() + self.assertIn('reverse_sql_filename = "view-band_info-0005.sql"\n', lines) diff --git a/tests/test_runner.py b/tests/test_runner.py index 5cc0dff..8b7c631 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -29,6 +29,8 @@ def clean_up_files(): "tests/store/sql/view-store_purchasedproductcalculations-0004.sql", "tests/musicians/migrations/0005_add_album_count.py", "tests/musicians/sql/view-band_info-0003-add_founded_date.sql", + "tests/musicians/migrations/0006_add_member_list.py", + "tests/musicians/sql/view-band_info-0005.sql", ) for path in files_to_delete: