Skip to content

Commit

Permalink
v11.4.0 (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
GitRon authored Sep 24, 2024
1 parent b87adf4 commit 82eab41
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

**11.4.0** (2024-09-24)
* Added system check to enforce naming conventions for DateFields and DateTimeFields

**11.3.0** (2024-09-17)
* Added date util functions `get_current_year` and `check_date_is_weekend`
* Improved date utils docs
Expand Down
2 changes: 1 addition & 1 deletion ambient_toolbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Python toolbox of Ambient Digital containing an abundance of useful tools and gadgets."""

__version__ = "11.3.0"
__version__ = "11.4.0"
Empty file.
56 changes: 56 additions & 0 deletions ambient_toolbox/system_checks/model_field_name_conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.apps import apps
from django.conf import settings
from django.core import checks
from django.db.models import DateField, DateTimeField


def check_model_time_based_fields(*args, **kwargs):
"""
Checks all model time fields ('DateField', 'DateTimeField') for a "correct" ending in their name.
Inspired by: https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/
"""

project_apps = [
app.split(".")[-1] for app in settings.INSTALLED_APPS if app.startswith(settings.ROOT_URLCONF.split(".")[0])
]
issue_list = []

# Allowlists
allowed_datetime_field_endings = getattr(settings, "ALLOWED_MODEL_DATETIME_FIELD_ENDINGS", ["_at"])
allowed_date_field_endings = getattr(settings, "ALLOWED_MODEL_DATE_FIELD_ENDINGS", ["_date"])

str_allowed_datetime_endings = ", ".join(allowed_datetime_field_endings)
str_allowed_date_endings = ", ".join(allowed_date_field_endings)

# Iterate all registered models...
for model in apps.get_models():
# Check if the model is from your project...
if model._meta.app_label in project_apps:
# Iterate over all fields...
for field in model._meta.get_fields():
# Case: DateTimeField, noqa: ERA001
if isinstance(field, DateTimeField):
# Check field name ending against allowlist
if not field.name.lower().endswith(tuple(allowed_datetime_field_endings)):
issue_list.append(
checks.Warning(
f"DateTimeField '{model.__name__}.{field.name}' doesn't end with: "
f"{str_allowed_datetime_endings}.",
obj=field,
id="ambient_toolbox.W001",
)
)
# Case: Date field, noqa: ERA001
elif isinstance(field, DateField):
# Check field name ending against allowlist
if not field.name.lower().endswith(tuple(allowed_date_field_endings)):
issue_list.append(
checks.Warning(
f"DateField '{model.__name__}.{field.name}' doesn't end with: "
f"{str_allowed_date_endings}.",
obj=field,
id="ambient_toolbox.W002",
)
)

return issue_list
37 changes: 37 additions & 0 deletions docs/features/system_checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# System checks

## Model field naming conventions

Inspired
by [Luke Plants article](https://lukeplant.me.uk/blog/posts/enforcing-conventions-in-django-projects-with-introspection/),
this package implements a system check to ensure that all custom DateField and DateTimeField are named in a uniform
manner.

By default, it requires for DateFields to end on `_date` and DateTimeFields on `_at`.

It's straightforward to register this system check in your project.

````python
# apps/common/apps.py
from ambient_toolbox.system_checks.model_field_name_conventions import check_model_time_based_fields

from django.apps import AppConfig
from django.core.checks import register


class CommonConfig(AppConfig):
name = "apps.common"
verbose_name = "Common"

def ready(self):
register(check_model_time_based_fields)
````

You can configure which field name endings are allowed by setting these variables in your global Django settings file.

````python
# apps/config/settings.py

ALLOWED_MODEL_DATETIME_FIELD_ENDINGS = ["_at"]
ALLOWED_MODEL_DATE_FIELD_ENDINGS = ["_date"]
````
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The package is published at pypi under the following link: `https://pypi.org/pro
features/sentry.md
features/services.md
features/static_role_permissions.md
features/system_checks.md
features/tests.md
features/translations.md
features/utils.rst
Expand Down
20 changes: 19 additions & 1 deletion testapp/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.7 on 2024-07-15 14:19
# Generated by Django 5.0.7 on 2024-09-24 03:59

import ambient_toolbox.mixins.bleacher
import ambient_toolbox.mixins.models
Expand Down Expand Up @@ -52,6 +52,24 @@ class Migration(migrations.Migration):
],
bases=(ambient_toolbox.mixins.bleacher.BleacherMixin, models.Model),
),
migrations.CreateModel(
name="ModelNameTimeBasedFieldTest",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("wrongly_named_date_field", models.DateField()),
("wrongly_named_datetime_field", models.DateTimeField()),
("timestamp_date", models.DateField()),
("timestamped_at", models.DateTimeField()),
],
),
migrations.CreateModel(
name="ModelWithCleanMixin",
fields=[
Expand Down
10 changes: 10 additions & 0 deletions testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,13 @@ def __str__(self):
@receiver(pre_save, sender=ModelWithSaveWithoutSignalsMixin)
def increase_value_on_pre_save(sender, instance, **kwargs):
instance.value += 1


class ModelNameTimeBasedFieldTest(models.Model):
wrongly_named_date_field = models.DateField()
wrongly_named_datetime_field = models.DateTimeField()
timestamp_date = models.DateField()
timestamped_at = models.DateTimeField()

def __str__(self):
return self.id
Empty file added tests/system_checks/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions tests/system_checks/test_model_field_name_conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.core import checks
from django.test import SimpleTestCase, override_settings

from ambient_toolbox.system_checks.model_field_name_conventions import check_model_time_based_fields
from testapp.models import ModelNameTimeBasedFieldTest


class CheckModelTimeBasedFieldsTest(SimpleTestCase):
def test_check_regular(self):
# Create expected warnings
datetime_warning = checks.Warning(
"DateTimeField 'ModelNameTimeBasedFieldTest.wrongly_named_datetime_field' doesn't end with: _at.",
obj=ModelNameTimeBasedFieldTest.wrongly_named_datetime_field.field,
id="ambient_toolbox.W001",
)
date_warning = checks.Warning(
"DateField 'ModelNameTimeBasedFieldTest.wrongly_named_date_field' doesn't end with: _date.",
obj=ModelNameTimeBasedFieldTest.wrongly_named_date_field.field,
id="ambient_toolbox.W002",
)

# Call system check
error_list = check_model_time_based_fields()

# Assert warngins
self.assertEqual(len(error_list), 2)
self.assertIn(datetime_warning, error_list)
self.assertIn(date_warning, error_list)

@override_settings(ALLOWED_MODEL_DATETIME_FIELD_ENDINGS=["wrongly_named_datetime_field", "_at"])
def test_check_allowlist_works_datetime_field(self):
# Call system check
error_list = check_model_time_based_fields()

# Assert warngins
self.assertEqual(len(error_list), 1)
self.assertEqual(error_list[0].id, "ambient_toolbox.W002")

@override_settings(ALLOWED_MODEL_DATE_FIELD_ENDINGS=["wrongly_named_date_field", "_date"])
def test_check_allowlist_works_date_field(self):
# Call system check
error_list = check_model_time_based_fields()

# Assert warngins
self.assertEqual(len(error_list), 1)
self.assertEqual(error_list[0].id, "ambient_toolbox.W001")

0 comments on commit 82eab41

Please sign in to comment.