fast Django app development


Keywords
django
License
MIT
Install
pip install SmartDjango==2.0.0.beta

Documentation

smartdjango v4

Introduction

smartdjango is a high-level packed developing tools for Django.

It provides the following features:

  • Smart middleware for auto packing json-format response
  • Smart fields for restricting value, such as min_length of a CharField, or max_value/min_value of an IntegerField
  • Smart param for value validating and processing, which can be generated by a field quickly.
  • Smart analysis tool for analysing parameters of any method's input, especially the Request object. So the parameters can be formatted/detected before methods running.
  • Smart error for identifying, recording and response.
  • Smart pager for partly response objects the model filtered.

Installation

pip install smartdjango

The latest version is 4.2.1, building on Aug, 2025. It supports the latest django 5.

Hints

Language Limited

Current Version is based on Chinese, as you can see in some readable errors.

error.py

from smartdjango import Error, Code

@Error.register
class BaseErrors:
    OK = Error("没有错误", code=Code.OK)
    FIELD_VALIDATOR = Error("字段校验器错误", code=Hc.InternalServerError)
    FIELD_PROCESSOR = Error("字段处理器错误", code=Code.InternalServerError)
    FIELD_FORMAT = Error("字段格式错误", code=Code.BadRequest)
    RET_FORMAT = Error("函数返回格式错误", code=Code.InternalServerError)
    MISS_PARAM = Error("缺少参数{0}({1})", code=Code.BadRequest)
    STRANGE = Error("未知错误", code=Code.InternalServerError)

We will later support more languages.

Django familiarity

You need to be familiar with Django development, including models, views as well as basic server development.

Tutorial

We assume you have setup Django environment, with database correctly configured.

No hundreds times' return JsonResponse!

JSON is a popular format for data transferring. The following code is usually used to pack dict data to HTTPResponse object:

import json

from django.http import HttpResponse

def handler(request):
    data = get_data()  # type: dict
    return HttpResponse(
                json.dumps(data, ensure_ascii=False),  # for showing Chinese characters
                content_type="application/json; encoding=utf-8",
            )

or an easier way:

from django.http import JsonResponse

def handler(request):
    data = get_data()  # type: dict
    return JsonResponse(data)

Now it's the easiest way:

def handler(request):
    data = get_data()  # type: dict
    return data

You may have dozens of handlers, but you don't need to write dozens of JsonResponse. What's all you need is to append a single line in MIDDLEWARES in setting file like this:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    ...
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'smartdjango.middleware.APIPacker',
]

The middleware will directly pass HttpResponse object, handle errors which error module creates and importantly pack dict data.

Model value detect

It's a common requirement to limit the length of username to 3 to 16, and limit its characters to a to z. As CharField only has max_length attribute, what we usually do is:

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=16)
    password = models.CharField(max_length=16)
    ...
    
    @classmethod
    def new(cls, username, password):
        if not cls.check_username_is_alpha(username):
            raise Exception
        if not cls.check_username_max_min_length(username):
            raise Exception
        if not cls.check_password_max_min_length(password):
            raise Exception
        user = cls(username=username, password=password)
        user.save()
        return user

Now we have a more elegant and magical solution:

from smartdjango import Error, Code

from django.db import models

@Error.register
class UserErrors:
    USERNAME_ALPHA = Error("Username should only be consist of alphas.", code=Code.Forbidden)
    USERNAME_TOO_SHORT = Error("Username should be at least {length} characters.", code=Code.BadRequest)
    PASSWORD_TOO_SHORT = Error("Password should be at least {length} characters.", code=Code.BadRequest)
    
    
    
class UserValidator:
    MAX_USERNAME_LENGTH = 16
    MIN_USERNAME_LENGTH = 6
    MAX_PASSWORD_LENGTH = 16
    MIN_PASSWORD_LENGTH = 6
    
    @classmethod
    def username(cls, username):
        if len(username) < cls.MIN_USERNAME_LENGTH:
          raise UserErrors.USERNAME_TOO_SHORT(length=cls.MIN_USERNAME_LENGTH)
        
    @classmethod
    def password(cls, password):
        if len(password) < cls.MIN_PASSWORD_LENGTH:
          raise UserErrors.PASSWORD_TOO_SHORT(length=cls.MIN_PASSWORD_LENGTH)
        
        
class User(models.Model):
    vldt = UserValidator
    username = models.CharField(max_length=vldt.MAX_USERNAME_LENGTH, validators=[vldt.username])
    password = models.CharField(max_length=vldt.MAX_PASSWORD_LENGTH, validators=[vldt.password])
    ...
    
    @classmethod
    def new(cls, username, password):
        user = cls(username=username, password=password)
        user.save()
        return user

We firstly create an error class, for storing all errors would faced in user model. Here USERNAME_ALPHA is the identifier of this error, with description and http code.

This error is used in User._valid_username method. SmartDjango will detect if there is a attribute-level validator named _valid_ATTRIBUTE in this class when calling cls.validator.

If the validator, called in new method, returns/raises an error, new method will bubble this error to upper method. As it arrives in middleware, it will be handled and shown as json response.

Another thing is max length and min length. cls.validator will also detects values if fitting the configuration of fields.

Avoid bad parameters before entering the handler!

In fact, if the username posted is not valid, we could stop it at the beginning.

Here's the code we usually write in views.py to get register information, create a user and return something.

from django.views import View
from User.models import User

import json

class UserView(View):
    def post(self, request):
        data = json.loads(request)
        username = data['username']
        password = data['password']
        user = User.new(username, password)  # assuming we adopt the code above
        
        return dict(id=user.pk, username=user.username)  # middleware will handle it!

Try this easier way:

models.py

from diq import Dictify

from smartdjango import Error, Params, Validator

from django.db import models

@Error.register
class UserErrors: ...

class User(models.Model, Dictify):
    def _dictify_id(self):
        return self.pk
        
    def d(self):
        return self.dictify('username', 'id')
...

class UserParams(metaclass=Params):
    model_class = User
    
    username: Validator
    password: Validator

views.py

from django.views import View
from smartdjango import analyse
from User.models import User
from User.params import UserParams


class UserView(View):
    @analyse.json(UserParams.username, UserParams.password)  
    def post(self, request):
        username = request.json.username
        password = request.json.password
        user = User.new(username, password)  # or User.new(**request.json())
        return user.d()

As you can see, we support many new features.

Analyse parameters which can be custom or generated magically by model fields

Firstly, UserParams means Parameters created by User. UserParams.username and UserParams.passowrd succeed features like max/min length and custom validator of username and password field.

In views.py, we use analyse.json to analyse parameters in request body, analyse.query for query, and analyse.argument for argument. For example, the API:

POST /api/zoo/insect/search?count=5&last=0

{
    "name": "fly",
    "color": null
}

with the url pattern: path('/api/zoo/<str: kind>/search', some_handler), the some handler should be written as:

from smartdjango import analyse, Validator

@analyse.json(
    Validator('name', 'name of insert'),
    Validator('color').null().default(None)
)
@analyse.query(
    Validator('count').to(int).to(lambda x: min(1, x)),
    Validator('last').to(int)
)
@analyse.argument('kind')
def some_handler(request, kind):
    pass

OK now we back to the UserView. The analyse decorator will check these two parameters if valid. Only if they passed the analysis, they will be stored in request.data or request.query/json/argument object. It's obviously more convenient to use dot-username than quote-username-quote.

Also the dict method is provided to fetch all/specific parameters.

Simple way to get information of a model instance

The next magic is models.dictify.

It would be very boring to write something like this:

def d(self):
    return dict(
        id=self.pk,
        birthday=self.birthday.strftime('%Y-%m-%d'),  # datetime -> str
        username=self.username,
        description=self.description,
        male=self.male,
        school=self.school,
        ...
    )

Now dictify is born, and the bore is gone.

dictify will detect firstly _dictify_ATTRIBUTE for each attributes. If detected, it will record dict(attribute=self._dictify_ATTRIBUTE()), or else it will find if the model has this attribute.

What we make easy is 2 schools at beginning but now only once. It can be simplified like this:

def _dictify_id(self):
    return self.pk
    
def _dictify_birthday(self):
    return self.birthday.timestamp()
        
def d(self):
    return self.dictor('id', 'birthday', 'username', 'description', 'male', 'school', ...) 

More custom for Validator

final_name and to_python

Assuming now you want to get some user's information with its user_id.

Former way:

from smartdjango import analyse

@analyse.query('user_id')
def handler(request):
    user_id = request.query.user_id
    user = User.get(user_id)
    return user.d()

Magic way:

from smartdjango import analyse, Validator

@analyse.query(Validator('user_id', final_name='user').to(User.get))
def handler(r):
    return r.d.user.d()

validator

Validator is a simpler limitation compared with Processor. It's a Processor, but the simple version. It will be stored with other processors.

Validator only returns error or None. If None, the value doesn't change. Processor will change value and is able to change parameter's name (final_name).

If some parameters have more than one validators or processors, the order of those makes a significant effect. Take a timestamp string as an example, if we want to extract datetime information of the string "1572451778", we firstly need to transfer it as the integer 1572451778, then use datetime methods to process this integer to a datetime object. The right way to handle the problem above should be one of the following codes.

import datetime

from smartdjango import Validator

Validator('time').to(int).to(datetime.datetime.fromtimestamp)

You might think the second method is stupid in some kind, but it's an usual phenomenon. Think about it:

models.py

import datetime

from django.db import models
from smartdjango import Params, Validator

class SomeModel(models.Model):
    time = models.DateTimeField(...)
    
class SomeModelParams(metaclass=Params):
    time: Validator
    
SomeModelParams.time.to(int).to(datetime.datetime.fromtimestamp)

SomeModelParams.time is created as a Validator, and its build-in validator will check if the value is instance of Datetime. As mentioned above, validator is stored with other processors (in one processor list), we need to process the string-format timestamp to datetime type. So what we always do is add two processors.

default and null

Sometimes parameter may have default value or can be null. If some parameter null and it posted as null, it will only change it's final_name and the value will stay null. You can use null(null=False) to change its state.

If some parameter would have default value, default will show its function. When a Validator doesn't set null but set default, it will get a default value. For example, Validator('count').default(10).

rename and clone

If we want to custom some Validator based on it, we can use copy method to get a new Validator with all features.

Rename would be a huge demand, to change parameter's name, read name and yield name.

One more thing

better searching

Django provides convenient filter method to select items we want based on fields we limited. But when we do some search things, like get students whose name contains Jyonn, we would code like this:

SomeClass.objects.filter(name__contains='Jyonn')

Search takes the secondary place cause name='Jyonn' means the name is exactly Jyonn. Now we want to take search as important as filter. And the dream has come true:

SomeClass.objects.search(name='Jyonn')

We use name__full='Jyonn' in search to replace name='Jyonn' in filter. We also support custom design. For example, we want to get all apps with user number above 500(some digit) and created time in 1 year(some day). Try this:

models.py

import datetime

from SmartDjango import models

class App(models.Model):
    user_num = models.IntegerField(...)
    create_time = models.DateTimeField(...)
    
    def _search_user_num(self, v):
        return dict(user_num__gte=v)
        
    def _search_create_time(self, v):
        return dict(create_time__gte=v)
        
class AppP:
    user_num, create_time = App.get_params('user_num', 'create_time')
    create_time.processor(datetime.datetime.fromtimestamp, begin=True).processor(int, begin=True)

views.py

from django.views import View
from SmartDjango import Analyse

class SearchView(View):
    @staticmethod
    @Analyse.r(q=[AppP.user_num, AppP.create_time])
    def get(r):
        user_num = r.d.user_num
        create_time = r.d.create_time
        objects = App.search(user_num=user_num, create_time=create_time)
        # or
        objects = App.search(**r.d.dict()) 
        ...

We pack _gte or some other things into the model, so the code in views could be quite simple and elegant.

You will find more colorful usage. Try SmartDjango!