From ca8ad4cd1ed5cd916b1a26fa4e87ad96cc0cb713 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 09:37:31 +0000 Subject: [PATCH 01/11] UpdateFields type --- codeforlife/models/__init__.py | 0 codeforlife/models/signals.py | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 codeforlife/models/__init__.py create mode 100644 codeforlife/models/signals.py diff --git a/codeforlife/models/__init__.py b/codeforlife/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/codeforlife/models/signals.py b/codeforlife/models/signals.py new file mode 100644 index 00000000..53e234e0 --- /dev/null +++ b/codeforlife/models/signals.py @@ -0,0 +1,3 @@ +import typing as t + +UpdateFields = t.Optional[t.FrozenSet[str]] From 084f9bac678cc77712705f3516b1e43bf2b1ccf4 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 12:34:19 +0000 Subject: [PATCH 02/11] feat: model signal helpers --- codeforlife/models/__init__.py | 17 +++++++++ codeforlife/models/signals.py | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/codeforlife/models/__init__.py b/codeforlife/models/__init__.py index e69de29b..551957ba 100644 --- a/codeforlife/models/__init__.py +++ b/codeforlife/models/__init__.py @@ -0,0 +1,17 @@ +import typing as t + +from django.db.models import Model as _Model + + +class Model(_Model): + """A base class for all Django models. + + Args: + _Model (django.db.models.Model): Django's model class. + """ + + id: int + pk: int + + +AnyModel = t.TypeVar("AnyModel", bound=Model) diff --git a/codeforlife/models/signals.py b/codeforlife/models/signals.py index 53e234e0..a0e0b8d9 100644 --- a/codeforlife/models/signals.py +++ b/codeforlife/models/signals.py @@ -1,3 +1,69 @@ import typing as t +from . import AnyModel + UpdateFields = t.Optional[t.FrozenSet[str]] + + +def check_post_save( + update_fields: UpdateFields, + expected_update_fields: UpdateFields, +): + """https://docs.djangoproject.com/en/3.2/ref/signals/#post-save + + Check if a model was updated with the expected fields. + + Args: + update_fields: The fields that were updated. + expected_update_fields: A subset of the fields that were expected to be + updated. If no fields were expected to be updated, set to None. + + Returns: + If the model instance was updated as expected. + """ + + if expected_update_fields is None: + return update_fields is None + elif update_fields is None: + return False + + return all( + update_field in update_fields for update_field in expected_update_fields + ) + + +def check_pre_save( + update_fields: UpdateFields = None, + expected_update_fields: UpdateFields = None, + instance: t.Optional[AnyModel] = None, + created: bool = False, + created_only: bool = False, +): + """https://docs.djangoproject.com/en/3.2/ref/signals/#pre-save + + Check if a model was created or updated with the expected fields. + + Args: + update_fields: The fields that were updated. + expected_update_fields: A subset of the fields that were expected to be + updated. If no fields were expected to be updated, set to None. + instance: Any model instance. + created: Check if the model was created. + created_only: Only check if the model was created. + + Raises: + ValueError: If arg 'created' is True and arg 'instance' is None. + + Returns: + If the model instance was created or updated as expected. + """ + + if created: + if instance is None: + raise ValueError("Arg 'instance' cannot be None.") + if created_only: + return instance.pk is None + if instance.pk is None: + return True + + return check_post_save(update_fields, expected_update_fields) From a3a3bddc2f7f50bba7f81a205038b0984df36c84 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 12:44:34 +0000 Subject: [PATCH 03/11] fix path ignore --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d073f995..00ee3288 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" - - "**/.*" + - "**/\\.*" workflow_dispatch: env: From acd9e2253aaa774e806b1a0d942df81c74ab4cbb Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 12:45:23 +0000 Subject: [PATCH 04/11] remove --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00ee3288..17d8636b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,6 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" - - "**/\\.*" workflow_dispatch: env: From 965cb47767be5aec5b87158ecbf3ce6b41941015 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 12:50:31 +0000 Subject: [PATCH 05/11] ignore . files --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 17d8636b..d073f995 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,7 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" + - "**/.*" workflow_dispatch: env: From fa9c17028de5f49e7624d7e4094ba4e21a3d696c Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 12:52:03 +0000 Subject: [PATCH 06/11] escape . --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d073f995..04e531d5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" - - "**/.*" + - "**/\.*" workflow_dispatch: env: From 35274848cf43b7a63ed0630c909a8bdbfc4d77cd Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 12:52:45 +0000 Subject: [PATCH 07/11] ignore . 2 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 04e531d5..00ee3288 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" - - "**/\.*" + - "**/\\.*" workflow_dispatch: env: From ffc4070b95908b88a6203b7f78b587774660b03e Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 13:00:59 +0000 Subject: [PATCH 08/11] test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00ee3288..16b68507 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" - - "**/\\.*" + - ".*" workflow_dispatch: env: From 78f3953fca33b95791d6e232930c0f52adfefbaf Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 13:02:14 +0000 Subject: [PATCH 09/11] test2 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ff08be71..3d17022e 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ + +# test From d26b818da22d7df3641cc5c2865fe43c61a18704 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 9 Nov 2023 13:05:46 +0000 Subject: [PATCH 10/11] final --- .github/workflows/main.yml | 1 + .gitignore | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16b68507..9e152cdd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,7 @@ on: paths-ignore: - "codeforlife/version.py" - "**/*.md" + - ".vscode/**" - ".*" workflow_dispatch: diff --git a/.gitignore b/.gitignore index 3d17022e..ff08be71 100644 --- a/.gitignore +++ b/.gitignore @@ -167,5 +167,3 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ - -# test From a9a87ca5d5a8306c4988e399732b5c353516d12d Mon Sep 17 00:00:00 2001 From: SKairinos Date: Fri, 10 Nov 2023 19:02:28 +0000 Subject: [PATCH 11/11] models helpers --- codeforlife/models/__init__.py | 4 ++ codeforlife/models/signals.py | 69 ------------------------- codeforlife/models/signals/__init__.py | 16 ++++++ codeforlife/models/signals/post_save.py | 21 ++++++++ codeforlife/models/signals/pre_save.py | 66 +++++++++++++++++++++++ 5 files changed, 107 insertions(+), 69 deletions(-) delete mode 100644 codeforlife/models/signals.py create mode 100644 codeforlife/models/signals/__init__.py create mode 100644 codeforlife/models/signals/post_save.py create mode 100644 codeforlife/models/signals/pre_save.py diff --git a/codeforlife/models/__init__.py b/codeforlife/models/__init__.py index 551957ba..9322030d 100644 --- a/codeforlife/models/__init__.py +++ b/codeforlife/models/__init__.py @@ -1,3 +1,7 @@ +"""Helpers for module "django.db.models". +https://docs.djangoproject.com/en/3.2/ref/models/ +""" + import typing as t from django.db.models import Model as _Model diff --git a/codeforlife/models/signals.py b/codeforlife/models/signals.py deleted file mode 100644 index a0e0b8d9..00000000 --- a/codeforlife/models/signals.py +++ /dev/null @@ -1,69 +0,0 @@ -import typing as t - -from . import AnyModel - -UpdateFields = t.Optional[t.FrozenSet[str]] - - -def check_post_save( - update_fields: UpdateFields, - expected_update_fields: UpdateFields, -): - """https://docs.djangoproject.com/en/3.2/ref/signals/#post-save - - Check if a model was updated with the expected fields. - - Args: - update_fields: The fields that were updated. - expected_update_fields: A subset of the fields that were expected to be - updated. If no fields were expected to be updated, set to None. - - Returns: - If the model instance was updated as expected. - """ - - if expected_update_fields is None: - return update_fields is None - elif update_fields is None: - return False - - return all( - update_field in update_fields for update_field in expected_update_fields - ) - - -def check_pre_save( - update_fields: UpdateFields = None, - expected_update_fields: UpdateFields = None, - instance: t.Optional[AnyModel] = None, - created: bool = False, - created_only: bool = False, -): - """https://docs.djangoproject.com/en/3.2/ref/signals/#pre-save - - Check if a model was created or updated with the expected fields. - - Args: - update_fields: The fields that were updated. - expected_update_fields: A subset of the fields that were expected to be - updated. If no fields were expected to be updated, set to None. - instance: Any model instance. - created: Check if the model was created. - created_only: Only check if the model was created. - - Raises: - ValueError: If arg 'created' is True and arg 'instance' is None. - - Returns: - If the model instance was created or updated as expected. - """ - - if created: - if instance is None: - raise ValueError("Arg 'instance' cannot be None.") - if created_only: - return instance.pk is None - if instance.pk is None: - return True - - return check_post_save(update_fields, expected_update_fields) diff --git a/codeforlife/models/signals/__init__.py b/codeforlife/models/signals/__init__.py new file mode 100644 index 00000000..1be564c9 --- /dev/null +++ b/codeforlife/models/signals/__init__.py @@ -0,0 +1,16 @@ +"""Helpers for module "django.db.models.signals". +https://docs.djangoproject.com/en/3.2/ref/signals/#module-django.db.models.signals +""" + +import typing as t + +UpdateFields = t.Optional[t.FrozenSet[str]] + + +def _has_update_fields(actual: UpdateFields, expected: UpdateFields): + if expected is None: + return actual is None + if actual is None: + return False + + return all(update_field in actual for update_field in expected) diff --git a/codeforlife/models/signals/post_save.py b/codeforlife/models/signals/post_save.py new file mode 100644 index 00000000..df7989ca --- /dev/null +++ b/codeforlife/models/signals/post_save.py @@ -0,0 +1,21 @@ +"""Helpers for module "django.db.models.signals.post_save". +https://docs.djangoproject.com/en/3.2/ref/signals/#post-save +""" + +from . import UpdateFields, _has_update_fields + + +def has_update_fields(actual: UpdateFields, expected: UpdateFields): + """Check if the expected fields were updated. + + Args: + actual: The fields that were updated. + expected: A subset of the fields that were expected to be updated. If no + fields were expected to be updated, set to None. + + Returns: + If the fields that were expected to be updated are a subset of the + fields that were updated. + """ + + return _has_update_fields(actual, expected) diff --git a/codeforlife/models/signals/pre_save.py b/codeforlife/models/signals/pre_save.py new file mode 100644 index 00000000..75d43a9d --- /dev/null +++ b/codeforlife/models/signals/pre_save.py @@ -0,0 +1,66 @@ +"""Helpers for module "django.db.models.signals.pre_save". +https://docs.djangoproject.com/en/3.2/ref/signals/#pre-save +""" + +import typing as t + +from .. import AnyModel +from . import UpdateFields, _has_update_fields + + +def was_created(instance: AnyModel): + """Check if the instance was created. + + Args: + instance: The current instance. + + Returns: + If the instance was created. + """ + + return instance.pk is not None + + +def has_update_fields(actual: UpdateFields, expected: UpdateFields): + """Check if the expected fields are going to be updated. + + Args: + actual: The fields that are going to be updated. + expected: A subset of the fields that are expected to be updated. If no + fields are expected to be updated, set to None. + + Returns: + If the fields that are expected to be updated are a subset of the + fields that are going to be updated. + """ + + return _has_update_fields(actual, expected) + + +def has_previous_values( + instance: AnyModel, + predicates: t.Dict[str, t.Callable[[t.Any, t.Any], bool]], +): + """Check if the previous values are as expected. + + Args: + instance: The current instance. + predicates: A predicate for each field. It accepts the arguments + (previous_value, value) and returns True if the values are as expected. + + Raises: + ValueError: If arg 'instance' has not been created yet. + + Returns: + If all the previous values are as expected. + """ + + if not was_created(instance): + raise ValueError("Arg 'instance' has not been created yet.") + + previous_instance = instance.__class__.objects.get(pk=instance.pk) + + return all( + predicate(previous_instance[field], instance[field]) + for field, predicate in predicates.items() + )