klen.github.io

in Blog

Несколько полезных сниппетов для джанго

Настройка HTTPS для чайников Ctrl→
←Ctrl Настройка сервера. Создаем и разворачиваем django-проект


Ниже находится несколько полезных Django сниппетов, которые я использую в своей работе.

Автоматическая генерация имени файла в полях типа FileField, ImageField

В Django поля типа FileField и ImageField позволяют определять функцию для генерации имени сохраняемого файла. Она указывается в аргументе upload_to при создании поля.

Когда мне надоело придумывать уникальные имена файлов, при сохранении их в Django-проектах я написал нижеприведенный сниппет.

  • Генерируемые пути файлов содержат только латинские и цифровые символы;
  • Файлы с одинаковыми названиями, загружаемые в разные инстансы не перезаписываются;
  • Файлы удобно хранятся отсортированные по приложениям и моделям;
  • Путь файла однозначно говорит нам о том, что за инстанс его использует;
  • Система не позволяет тысячам файлов скапливаться в одной папке;
from hashlib import md5
from os import path as op
from time import time


def upload_to(instance, filename, prefix=None, unique=False):
    """ Auto generate name for File and Image fields.
    """
    ext = op.splitext(filename)[1]
    name = str(instance.pk or '') + filename + (str(time()) if unique else '')

    # We think that we use utf8 based OS file system
    filename = md5(name.encode('utf8')).hexdigest() + ext
    basedir = op.join(instance._meta.app_label, instance._meta.module_name)
    if prefix:
        basedir = op.join(basedir, prefix)
    return op.join(basedir, filename[:2], filename[2:4], filename)

Пример работы

Например у нас есть модель Archive в приложении storage, storage/models.py:

from django.db import models

class Archive(model.Model):
    title = models.CharField(max_length=100)
    file = models.FileField(upload_to=upload_to)

В результате сохраненные файлы будут иметь пути вида: {MEDIA_URL}/storage/archive/21/40/2140e8fe8678bc08ff6e9b10c9639068.zip

Здесь присутствует имя приложения, имя модели, созданное новое имя файла и его расширение. При этом путь дробится на папки согласно четырем первым символам в имени файла 21/40/2140e8.... Это позволяет значительно отсрочить ситуацию с сотнями тысяч объектов в папке и тормоза файловой системы.

Параметры сниппета

Функция upload_to может принимать до 4-х параметров. Аргументы instance и filename передаются Django.

Аргумент prefix принимает строковые значения и позволяет дополнительно настраивать пути файлов. Например при определении префикса prefix="cart" вышеприведенный код будет сохранять файлы с путями вида: {MEDIA_URL}/storage/archive/cart/21/40/2140e8fe8678bc08ff6e9b10c9639068.zip. Префикс был добавлен после имени модели.

Аргумент unique принимает булевы значения и позволяет гарантированно получить уникальное имя файла. По-умолчанию для instance с одинаковым ключом, для одинаковых имен файла, будет создан идентичный путь. Это сделано намеренно, чтобы при обновлении файлов пути в конкретной модели не изменялись. Параметр unique меняет это поведение и при каждом изменении файла, путь будет другим.

Но как использовать все эти параметры если Django передает в указанную функцию только первые два? Здесь нам поможет Каррирование. Мы используем функцию curry из django.utils.functional, которая имитирует это поведение в python.

Например в нижеприведенном коде:

from django.db import models
from django.utils.functional import curry

class Archive(model.Model):
    title = models.CharField(max_length=100)
    cover = models.ImageField(upload_to=curry(upload_to, prefix='cover'))
    file = models.FileField(upload_to=curry(upload_to, prefix='file'))
    other = models.FileField(upload_to=curry(upload_to, unique=True))

Файлы из поля cover будут сохраняться с путями вида: {MEDIA_URL}/storage/archive/cover/21/40/2140e8fe8678bc08ff6e9b10c9639068.jpg

Файлы из поля file будут сохраняться с путями вида: {MEDIA_URL}/storage/archive/file/21/40/2140e8fe8678bc08ff6e9b10c9639068.zip

Файлы из поля other всегда будут иметь уникальные пути, вида: {MEDIA_URL}/storage/archive/11/11/1111e8fe8678bc08ff6e9b10c9639068.mp3

Работа с кешем

В работе с кешированием проекта нужна четкая стратегия и обозначенные соглашения. Например наименования ключей кэша. SomeModel.object.filter(active=True) с каким ключом хранить результат выполнения этого запроса? Как параллельный разработчик узнает этот ключ? Когда производить инвалидацию? Для Django существуют приложения помогающие решать эту проблему, но зачастую они слишком перегружены и сложны в использовании.

Для простых проектов я написал несколько полезных функций:

import hashlib
import re

from django.core.cache import cache
from django.db.models import Model, get_model
from django.db.models.base import ModelBase
from django.db.models.query import QuerySet
from django.utils.encoding import smart_str

def cached_instance(model, **filters):
    """ Auto cached model instance.
    """
    if isinstance(model, basestring):
        assert '.' in model, ("'model_class' must be either a model"
                                " or a model name in the format"
                                " app_label.model_name")
        app_label, model_name = model.split(".")
        model = get_model(app_label, model_name)

    cache_key = generate_cache_key(model, **filters)
    return get_cached(cache_key, model.objects.select_related().get, kwargs=filters)


def cached_query(qs, timeout=None):
    """ Auto cached queryset and generate results.
    """
    cache_key = generate_cache_key(qs)
    return get_cached(cache_key, list, args=(qs,), timeout=None)


def clean_cache(*args, **kwargs):
    """ Generate cache key and clean cached value.
    """
    cache_key = generate_cache_key(*args, **kwargs)
    cache.delete(cache_key)


def generate_cache_key(cached, **kwargs):
    """ Auto generate cache key for model or queryset
    """
    if isinstance(cached, QuerySet):
        key = str(cached.query)

    elif isinstance(cached, (Model, ModelBase)):
        key = '%s.%s:%s' % (cached._meta.app_label,
                cached._meta.module_name,
                ','.join('%s=%s' % item for item in kwargs.iteritems()))

    else:
        raise AttributeError("Objects must be queryset or model.")

    if not key:
        raise Exception('Cache key cannot be empty.')

    key = clean_cache_key(key)
    return key


def clean_cache_key(key):
    """ Replace spaces with '-' and hash if length is greater than 250.
    """
    cache_key = re.sub(r'\s+', '-', key)
    cache_key = smart_str(cache_key)

    if len(cache_key) > 200:
        cache_key = cache_key[:150] + '-' + hashlib.md5(cache_key).hexdigest()

    return cache_key


def get_cached(cache_key, func, timeout=None, args=None, kwargs=None):
    args = args or list()
    kwargs = kwargs or dict()
    result = cache.get(cache_key)

    if result is None:

        if timeout is None:
            timeout = cache.default_timeout

        result = func(*args, **kwargs)
        cache.set(cache_key, result, timeout=timeout)

    return result

    #**

Примеры работы

# Генерация ключей для Queryset (разные ключи для разных запросов)
print generate_cache_key(TaxiStation.objects.all())
print generate_cache_key(TaxiStation.objects.all())
print generate_cache_key(TaxiStation.objects.filter(active=True))
print generate_cache_key(TaxiStation.objects.filter(city=1, title="intaxi"))
print generate_cache_key(TaxiStation.objects.filter(active=True))

# Output: 'SELECT-"main_taxistation"."id",-"main_taxistation"."active",-"main_taxistation"."title",-"main_taxistation"."city_id",-"main_taxistation"."agent_id",--873aa3b2fbd81cdaa9fce75e60706579'
# Output: 'SELECT-"main_taxistation"."id",-"main_taxistation"."active",-"main_taxistation"."title",-"main_taxistation"."city_id",-"main_taxistation"."agent_id",--873aa3b2fbd81cdaa9fce75e60706579'
# Output: 'SELECT-"main_taxistation"."id",-"main_taxistation"."active",-"main_taxistation"."title",-"main_taxistation"."city_id",-"main_taxistation"."agent_id",--043271300c8db6cc9152ef3119e6195c'
# Output: 'SELECT-"main_taxistation"."id",-"main_taxistation"."active",-"main_taxistation"."title",-"main_taxistation"."city_id",-"main_taxistation"."agent_id",--98960ebe77c30e08fe6b4a4fd2b1ab57'
# Output: 'SELECT-"main_taxistation"."id",-"main_taxistation"."active",-"main_taxistation"."title",-"main_taxistation"."city_id",-"main_taxistation"."agent_id",--043271300c8db6cc9152ef3119e6195c'

# Генерация ключей для Model
print generate_cache_key(TaxiStation, pk=100, active=True)
print generate_cache_key(TaxiStation, pk=50)

# Output: main.taxistation:pk=50

# Кеширование Queryset (получение данных из кеша или из БД с сохранением в кеш)
all = cached_query(SomeModel.objects.all())
some_results =  cached_query(SomeModel.objects.filter(param=True))

# Принудительная очистка кеша Queryset
clean_cache(SomeModel.objects.all())
clean_cache(SomeModel.objects.filter(param=True))

# Кеширование instance
order = cached_instance(Order, pk=20, title="Some title")

# можно и по имени
order = cached_instance('app.order', pk=20, title="Some title")

# Очистка кеша
clean_cache(Order, pk=20, title="Some title")

Атомарные обновления

Часто возникают ситуации когда нам необходимо обновить одно или несколько полей объекта. В Django по-умолчанию это можно сделать при помощи следующего кода:

# Допустим у нас есть объект order (instance of model)
order.custom_field = custom_value
order.full_clean()
order.save()

При этом ORM Django создает и выполняет запрос содержащий обновление всех полей объекта, что может быть довольно медленной операцией.

Значительно быстрее сработает следующая конструкция:

order.custom_field = custom_value
order.full_clean()
Order.objects.filter(pk=order.pk).update(custom_field = custom_value)

Но она несколько неудобна. Следующая функция решает эту проблему.

import operator

from django.db import models
from django.db.models.expressions import F, ExpressionNode


EXPRESSION_NODE_CALLBACKS = {
    ExpressionNode.ADD: operator.add,
    ExpressionNode.SUB: operator.sub,
    ExpressionNode.MUL: operator.mul,
    ExpressionNode.DIV: operator.div,
    ExpressionNode.MOD: operator.mod,
    ExpressionNode.AND: operator.and_,
    ExpressionNode.OR: operator.or_,
    }

class CannotResolve(Exception):
    pass

def _resolve(instance, node):
    if isinstance(node, F):
        return getattr(instance, node.name)
    elif isinstance(node, ExpressionNode):
        return _resolve(instance, node)
    return node

def resolve_expression_node(instance, node):
    op = EXPRESSION_NODE_CALLBACKS.get(node.connector, None)
    if not op:
        raise CannotResolve
    runner = _resolve(instance, node.children[0])
    for n in node.children[1:]:
        runner = op(runner, _resolve(instance, n))
    return runner

def update(instance, full_clean=False, **kwargs):
    "Atomically update instance, setting field/value pairs from kwargs"

    # apply the updated args to the instance to mimic the change
    # note that these might slightly differ from the true database values
    # as the DB could have been updated by another thread. callers should
    # retrieve a new copy of the object if up-to-date values are required
    for k, v in kwargs.iteritems():
        if isinstance(v, ExpressionNode):
            v = resolve_expression_node(instance, v)
        setattr(instance, k, v)

    # clean instance before update
    if full_clean:
        instance.full_clean()

    # fields that use auto_now=True should be updated corrected, too!
    for field in instance._meta.fields:
        if hasattr(field, 'auto_now') and field.auto_now and field.name not in kwargs:
            kwargs[field.name] = field.pre_save(instance, False)

    rows_affected = instance.__class__._default_manager.filter(pk=instance.pk).update(**kwargs)
    return rows_affected

#**

Теперь мы можем делать так:

# Обновили объект и сохранили его в базу, через
update(order, custom_field=custom_value)

print order.custom_field
# Output: custom_value

Или даже так:

update(order, custom_field=custom_value, other_field=other_value, more_field=more_value)

Обходясь при этом без тяжелых запросов к базе.

SELECT (1) AS "a" FROM "main_order" WHERE "main_order"."id" = 22387  LIMIT 1; args=(22387,)
UPDATE "main_order" SET "created_at" = E'2011-12-02 19:39:00.969044', "city_id" = 1, "when" = E'2011-12-02 19:50:00', "price" = E'100.00', "expected_price" = E'350.00', "car_class" = 10, "conditioner" = false, "smoke" = false, "no_smoke" = false, "child_seat" = false, "long_length" = false, "meet_sign" = false, "from_address_id" = 17130, "to_address_id" = 17131, "route_id" = NULL, "from_node_id" = 501, "to_node_id" = 501, "from_address_entrance_no" = E'', "from_comment" = E'', "to_comment" = E'', "flight_number" = E'', "meet_sign_text" = E'', "auto_id" = NULL, "fare_id" = 42, "taxistation_id" = 1, "passenger_id" = 71111111111, "device_id" = E'200774696189910', "device_agent" = E'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 ', "device_name" = E'Web', "device_token" = E'', "device_sms" = true, "current_status" = E'WaitForCarAssigment', "updated" = E'2011-12-02 19:40:09.405462', "credit" = false WHERE "main_order"."id" = 22387 ; args=(u'2011-12-02 19:39:00.969044', 1, u'2011-12-02 19:50:00', u'100.00', u'350.00', 10, False, False, False, False, False, False, 17130, 17131, 501, 501, u'', u'', u'', u'', u'', 42, 1, 71111111111L, u'200774696189910', u'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 ', u'Web', u'', True, 'WaitForCarAssigment', u'2011-12-02 19:40:09.405462', False, 22387)

VS

UPDATE "main_order" SET "price" = E'100.00' WHERE "main_order"."id" = 22387 ; args=(u'100.00', 22387)

Надеюсь эти несколько простых функций, будут также полезны вам, как и мне.

Настройка HTTPS для чайников Ctrl→
←Ctrl Настройка сервера. Создаем и разворачиваем django-проект
alt