Lightweight Django field utilities and extensions that reduce boilerplate and enforce sensible defaults.
pip install django-nxRequires Python >= 3.9, Django >= 3.2, and Django REST Framework >= 3.12.4.
from nx import nx
class Product(nx.Model):
name = nx.CharField('Name') # Default max_length=128
price = nx.MoneyField('Price')
status = nx.IntChoiceField('Status', choices=ProductStatus)
tags = nx.ArrayField('Tags')
metadata = nx.ObjectField('Metadata')All fields automatically use verbose_name as help_text when help_text is not explicitly provided.
| Field | Default | Description |
|---|---|---|
CharField |
default="", blank=True, max_length=128 |
String field with empty string default |
TextField |
default="", blank=True |
Long text field with empty string default |
TextChoiceField |
max_length=64, defaults to first choice |
CharField backed by choices enum |
name = nx.CharField('Name', max_length=255)
description = nx.TextField('Description')
priority = nx.TextChoiceField('Priority', choices=PriorityLevel)| Field | Default | Description |
|---|---|---|
IntegerField |
— | Standard integer with auto help_text |
MoneyField |
max_digits=18, decimal_places=2, default=Decimal('0') |
Decimal field for monetary values |
IntChoiceField |
default=first_choice |
SmallIntegerField backed by choices enum |
quantity = nx.IntegerField('Quantity')
price = nx.MoneyField('Price') # DECIMAL(18,2)
discount = nx.MoneyField('Discount', max_digits=5, decimal_places=4)
status = nx.IntChoiceField('Status', choices=OrderStatus)| Field | Default | Description |
|---|---|---|
BooleanField |
default=False, blank=True |
Boolean flag |
DateField |
null=True, blank=True |
Date picker |
DateTimeField |
null=True, blank=True |
DateTime picker |
is_active = nx.BooleanField('Is Active')
published_at = nx.DateTimeField('Published At')
birth_date = nx.DateField('Birth Date')| Field | Default | Description |
|---|---|---|
ForeignKey |
null=True, blank=True, on_delete=CASCADE |
Standard FK with nullable defaults |
OneToOne |
null=True, blank=True, on_delete=CASCADE |
One-to-one with nullable defaults |
ManyToMany |
blank=True |
Many-to-many relation |
ShadowForeignKey |
db_constraint=False |
FK without database constraint |
ShadowOneToOne |
db_constraint=False |
One-to-one without DB constraint |
ShadowManyToMany |
db_constraint=False |
Many-to-many without DB constraint |
user = nx.ForeignKey('auth.User', 'User')
profile = nx.OneToOne('accounts.Profile', 'Profile')
tags = nx.ManyToMany('products.Tag', 'Tags')
# Soft / logical foreign key (no DB-level constraint)
legacy_id = nx.ShadowForeignKey('legacy.Model', 'Legacy Ref')| Field | Default | Description |
|---|---|---|
ObjectField |
default=dict, blank=True |
JSONField defaulting to {} |
ArrayField |
default=list, blank=True |
JSONField defaulting to [] |
ShortUUIDField |
max_length=22, auto-generated |
URL-safe concise UUID |
config = nx.ObjectField('Config')
items = nx.ArrayField('Items')
code = nx.ShortUUIDField('Code')Auto-numbered integer choices starting from 1 (configurable via inner Meta.start).
import nx
class Priority(nx.IntegerChoices):
class Meta:
start = 1
LOW = "Low Priority" # 1
MEDIUM = "Medium Priority" # 2
HIGH = "High Priority" # 3
# Explicit values still work
class Status(nx.IntegerChoices):
PENDING = "Pending" # 1
APPROVED = 10 # 10
REJECTED = "Rejected" # 11
# Custom tuples are preserved
class Custom(nx.IntegerChoices):
NORMAL = "Normal" # 1
SPECIAL = (99, "Special") # 99Same as IntegerChoices, but defaults to starting from 0.
class Priority(nx.ZeroBasedChoices):
LOW = "Low" # 0
MEDIUM = "Medium" # 1
HIGH = "High" # 2Auto-lowercased values for consistent API keys.
class TriggerType(nx.TextChoices):
SALE_AMOUNT = "Sale Amount" # value = "sale_amount"
ORDER_COUNT = "Order Count" # value = "order_count"
# Custom tuples are preserved
class Status(nx.TextChoices):
ACTIVE = "Active" # value = "active"
CUSTOM = ("custom_key", "Custom") # value = "custom_key"nx.Model is an abstract base model that provides:
created_at– auto_now_add timestampupdated_at– auto_now timestampis_deleted– soft-delete flag (defaultFalse)- Auto-generated
db_table–{app_label}_{snake_case_model_name}viahumps.decamelize
import nx
class Product(nx.Model):
name = nx.CharField('Name', max_length=255)
class Meta:
app_label = 'shop'
# db_table = 'shop_product' # auto-generated if omittedExplicitly set
Meta.db_tableto skip auto-naming.
nx.QuerySet adds soft-delete aware query methods. It reads deleted_field from Model.Meta (defaults to is_deleted).
from nx.models.querysets import QuerySet
class ProductQuerySet(QuerySet):
pass
class Product(nx.Model):
...
class Meta:
deleted_field = 'is_deleted'
Product.objects.valid() # is_deleted=False / 0
Product.objects.invalid() # is_deleted=True / 1| Class | Description |
|---|---|
nx.drf.MoneyField |
DecimalField(max_digits=18, decimal_places=2) |
nx.drf.QuantityField |
IntegerField(min_value=0) |
nx.drf.MethodField |
Alias for SerializerMethodField |
nx.drf.AutoInstanceLookupMixin |
Mixin that auto-looks up instance by id on save |
from rest_framework import serializers
import nx
class ProductSerializer(nx.drf.AutoInstanceLookupMixin, serializers.ModelSerializer):
price = nx.drf.MoneyField()
stock = nx.drf.QuantityField()
category_name = nx.drf.MethodField()
class Meta:
model = Product
fields = ['id', 'price', 'stock', 'category_name']
def get_category_name(self, obj):
return obj.category.name if obj.category else NoneInject a top-level meta object (or any custom root key) into list responses.
from rest_framework import viewsets
import nx
class ProductViewSet(nx.drf.ListMetadataMixin, viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
list_metadata_root = "meta" # optional; omit to merge at top-level
def get_list_metadata(self, request, queryset, response):
return {
"total": queryset.count(),
"timestamp": timezone.now().isoformat(),
}Response shape with list_metadata_root = "meta":
{
"count": 100,
"results": [...],
"meta": {
"total": 100,
"timestamp": "2024-01-15T09:30:00Z"
}
}Response shape without list_metadata_root:
{
"count": 100,
"results": [...],
"total": 100,
"timestamp": "2024-01-15T09:30:00Z"
}Returns today, week, month, and year datetime ranges respecting USE_TZ.
from nx.utils import get_stat_datetime_range
ranges = get_stat_datetime_range()
# ranges.today -> (2024-01-15 00:00:00, 2024-01-15 23:59:59.999999)
# ranges.week -> (Mon 00:00:00, Sun 23:59:59.999999)
# ranges.month -> (1st 00:00:00, last_day 23:59:59.999999)
# ranges.year -> (Jan 1 00:00:00, Dec 31 23:59:59.999999)# Install dependencies
uv sync
# Run tests
pytest
# Lint
ruff check .
# Build
uv buildMIT License — see LICENSE for details.