Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v11.4.0 #34

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")