Package: doc-versions
Обработка и хранение данных с временными параметрамиАвторы:В. А. Фёдоров, К. А. ЛопухинАннотация:Предлагается модель представления и методы обработки временных данных
в реляционных СУБД с примерами для Django.ContentsОбработка и хранение данных с временными параметрамиПростейший способИнтервалыСвязи между таблицамиДокументы и версииСвязи между таблицами документаСвязи между документамиРазрешение ссылокРетроспекцияВерсии документов в DjangoAPIdocuments.modelsdocuments.fieldsdocuments.retrospectiondocuments.adminMiddleware для ретроспекцииПримерПример работыРедактор вопросов и ответовСуществует множество видов данных, содержащих в качестве одного из
компонентов момент времени:результат замера какой-нибудь величины (уровень воды по часам)план работысостояние документа (версии)состояние процесса (workflow)Простейший способdrop table if exists data ;
create table data (
      vtime timestamp DEFAULT CURRENT_TIMESTAMP ,
      vid integer ,    -- идентификатор
      v text           -- значение
      ) ;Предполагается что в таблице хранится множество измерений (v) от
разных источников (vid) в разные моменты времени (vtime). Для
примера поместим в таблицу несколько значений:insert into data (vtime,vid,v) values ('2000-01',1,'2000 - 1') ;
insert into data (vtime,vid,v) values ('2001-01',1,'2001 - 1') ;
insert into data (vtime,vid,v) values ('2002-01',1,'2002 - 1') ;
insert into data (vtime,vid,v) values ('2000-01',2,'2000 - 2') ;
insert into data (vtime,vid,v) values ('2001-01',2,'2001 - 2') ;
insert into data (vtime,vid,v) values ('2001-01',3,'2001 - 3') ;
insert into data (vtime,vid,v) values ('2002-01',3,'2002 - 3') ;Если точно знать момент времени, то легко найти значения:select v from data where vtime='2001-01' ;

2001 - 1
2001 - 2
2001 - 3Проблема возникает если момент времени задан не точно. Тогда имеет
смысл запрос на ближайшее известное значение (например снизу). При
этом, если нужно показание конкретного источника, то задачу можно
решить с помощью сортировки и выбора первого значения:select v from data where vid=2 and vtime<='2001-05'
  order by vtime DESC limit 1 ;

2001 - 2В случае, когда нужны все источники, задача решается еще более сложным
способом с использованием агрегирующих функций:select vid,v from data
where vtime=(select max(d.vtime) from data as d where
d.vtime<='2001-05' and d.vid=data.vid) ;

1      2001 - 1
2      2001 - 2
3      2001 - 3Или более эффективно (при большом наборе данных и индексировании):select data.vid,v from data ,
( select max(vtime) as t,vid
  from data where vtime<='2001-05' group by vid) as d
where vtime=t and data.vid=d.vid ;

1      2001 - 1
2      2001 - 2
3      2001 - 3ИнтервалыNoteВ последних постгресах есть with, который позволяет упростить решения!Предложенные выше конструкции довольно громоздки и неэффективны
(selectвстречается дважды), особенно если выбор значений
должен рассматриваться как подзадача более сложной обработки
полученных данных.Упростить решение можно с помощью добавления избыточных данных,
т.е. вместо даты регистрации хранитьинтервалдействия данного
значения:drop table if exists ndata ;
create table ndata (
      vstart timestamp DEFAULT CURRENT_TIMESTAMP , -- начало действия (вкл.)
      vend timestamp DEFAULT '9999-01' ,      -- конец действия (искл.)
      vid integer ,    -- идентификатор
      v text           -- значение
      ) ;Предыдущую таблицу можно преобразовать к новому виду:insert into ndata (vstart,vid,v) select vtime,vid,v from data ;

update ndata set vend = ( -- минимум из всех начал после данного
 select min(d.vstart) from ndata as d
 where d.vstart>ndata.vstart and d.vid=ndata.vid )
where exists ( -- существуют данные после текущего
 select * from ndata as d
 where d.vstart>ndata.vstart and d.vid=ndata.vid ) ;Результат:vstart         vend    vid     v
2000-01        2001-01 1       2000 - 1
2001-01        2002-01 1       2001 - 1
2002-01        9999-01 1       2002 - 1
2000-01        2001-01 2       2000 - 2
2001-01        9999-01 2       2001 - 2
2001-01        2002-01 3       2001 - 3
2002-01        9999-01 3       2002 - 3Интервалы не пересекаются (считаем нижнюю границу включительно, а
верхнюю исключительно). Задачи из предыдущего пункта решаются легко и
одинаково:select v from ndata where vid=2 and vstart<='2001-05' and vend>'2001-05' ;для поиска из конкретного источника (независимо от совпадения с
границей) и:select vid,v from ndata where vstart<='2001-05' and vend>'2001-05' ;для поиска всех источников.Усложняются процедуры вставки:insert into ndata (vstart,vend,vid,v) values ('2000-05',
    -- значение для правого конца интервала:
    case when exists ( -- попали в имеющийся
           select * from ndata
           where vid=2 and ndata.vstart<'2000-05' and ndata.vend>'2000-05' )
         then ( select vend from ndata
                where vid=2 and ndata.vstart<'2000-05' and
                                ndata.vend>'2000-05' )
         when exists ( -- в начало
           select * from ndata where vid=2 )
         then ( select min(vstart) from ndata where vid=2 )
         else '9999-01' end,
 2,'2000-05 2') ;
-- исправляем конец интервала, в который попали:
update ndata set vend='2000-05'
where vid=2 and vstart<'2000-05' and vend>'2000-05' ;и удаления:-- изменяем правый конец предыдущего интервала
update ndata set vend=(select vend from ndata
                      where vid=2 and vstart='2000-05')
where vid=2 and vend='2000-05' ;
delete from ndata where vid=2 and vstart='2000-05' ;Вставка может быть более простой если известен контекст:-- первое значение для vid=4:
insert into ndata (vstart,vid,v) values ('2000-05',4,'2000-05 4') ;

 -- вставка в начало:
insert into ndata
select '1999-01',min(vstart),2,'1999-01 2' from ndata where vid=2 ;

-- вставка в конец или внутрь
insert into ndata
select '2004-05',vend,2,'2004-05 2' from ndata
where vid=2 and vstart<'2004-05' and vend>'2004-05' ;
update ndata set vend='2004-05'
where vid=2 and vstart<'2004-05' and vend=>'2004-05' ;Массовую вставку значений можно сделать эффективно если отложить
вычисление правых границ на потом, как при импорте изdata.Связи между таблицамиИдентификатор и момент времени однозначно определяют значение (если
оно есть). То есть пару (момент времени,идентификатор) можно
использовать в качестве внешнего ключа таблицыndata. Пусть есть
произвольный список моментов времени и идентификаторов:drop table if exists idvtime ;
create table idvtime ( id integer , t timestamp ) ;
insert into idvtime values (3,'2000-01') ;
insert into idvtime values (2,'2000-05') ;
insert into idvtime values (1,'2001-01') ;
insert into idvtime values (3,'2001-05') ;
insert into idvtime values (2,'2002-01') ;Тогда список значений изndataможно присоединить запросом:select id,t,v
from idvtime left join ndata on (id=vid and vstart<=t and vend>t) ;

id     t       v
3      2000-01 NULL
2      2000-05 2000 - 2
1      2001-01 2001 - 1
3      2001-05 2001 - 3
2      2002-01 2001 - 2Документы и версииСуществует множество систем управления версиями: cvs, svn, git, darcs
и т.д. Их основная функция - хранение версий документов, запоминание
состояний документов в какие-то моменты времени с возможностью
просмотра прошлых состояний или даже возврата в какое-то предыдущее
состояние. Все системы работают с документами произвольного вида (в
основном текстовыми) и предназначены в большинстве своем для
сопровождения процесса разработки программ. Применим аналогичный
подход к данным, хранящимся в базе данных. Цели:возможность вернуться к предыдущему состоянию документа;возможность проследить динамику изменения документа;возможность исследовать состояние всей системы (получить отчеты) в
произвольный момент в прошлом (ретроспекция).Документсвязанный набор данных, возможно из нескольких таблиц,
воспринимаемых пользователем как единое целое. Документ имеет тип,
и уникальный (для данного типа) идентификатор экземпляра.Документ может изменяться во времени (редактироваться пользователем
или системой). Состояния, которые проходит документ со временем
называютсяверсиямидокумента.Версии документов не изменяются. У каждой версии есть временной
интервал ее действия. Последняя версия имеет правой границей плюс
бесконечность -datetime.max. Документ считается удаленным из
системы если в правую границу последней версии записать время
удаления.В базе храним версии документа. Одна из таблиц хранящих версию
документа (возможно единственная) содержит интервал действия и
идентификатор -главная таблица документа. Для каждой версии
каждого документа в главной таблице одна строка.Связи между таблицами документаГлавная таблица может быть связана с другими таблицами документа
отношениями:Один к одномуглавная таблица содержит внешний ключ другой - ссылка на
дополнительные данные, которые почему-то не хотим иметь в главной
(например редко используются, а занимают много места).Ссылка в обратную сторону имеет смысл если данные в другой таблице
существуют для редко встречающихся версий (экономится место).Многие к одномупоскольку версии не изменяются - при создании новой версии документа
можно повторно использовать имеющиеся данные другой таблицы (если
они не изменились) скопировав ссылку. Это еще одна причина хранения
данных в другой таблице - редко изменяемые данные.Один ко многимдругая таблица содержит ссылки на данную строку главной. Способ
представления списка объектов относящихся к данной версии.Многие ко многимдля такого отношения делается отдельная таблица содержащая ссылки в
обе стороны, т. е. предыдущий вариант.Другие таблицы могут быть связаны с третьими и т. д. Важно чтобы
соблюдался принцип неизменяемости версии документа (и всех относящихся
к документу данной версии компонент).Для выполнения массовых операций (например для построения отчетов)
важно простым способом получать временной срез произвольных частей
документа, т.е. рассматривать только те записи, которые действуют на
заданный момент времени. Сложность в том, что в таблице данных об
интервале действия нет (она не главная).  В большинстве случаев связи
между компонентами документа можно представить в виде дерева, где
главная таблица - его корень. Для каждого некорневого узла есть
единственный путь наверх, к корню. Есть два вида связи между данным
узлом и узлом верхнего уровня:Узел верхнего уровня содержит ссылку на данный.Данный узел содержит ссылку на узел верхнего уровня.В обоих случаях можно автоматически вычислять префикс доступа к
главной таблице и включать в запрос ограничения по времени.Связи между документамиДокументы могут быть связаны друг с другом, для чего в одном документе
помещается ссылка на другой. Существует несколько вариантов связей по
отношению к версиям связываемых документов:Ссылка на конкретную версию документа, заданную конкретным моментом
времени в прошлом.Ссылка на версию документа, заданную моментом создания версии
ссылающегося документа.Ссылка на последнюю версию документа.Ссылка на версию документа, заданную некоторым моментом в будущем
(после создания версии ссылающегося документа).Ссылка на документ общего вида задается парой (момент,идентификатор),
и в общем случае необходимо отвести два поля в ссылающемся
документе. В качестве момента времени для ссылки на последнюю версию
можно взять произвольную дату, близкую к нашей бесконечности, например9998-01-01.В случаях, когда при построении модели заранее известно, что ссылка
будет конкретного вида 2 или 3 - можно сэкономить, и хранить только
идентификатор документа, на который ссылаются. Момент времени в случае
2 задается моментом создания версии ссылающегося документа, а в случае
3 - константа.Разрешение ссылокЕсли заранее известно, что ссылка будет вида 1 или 2, то ссылки можноразрешать, то есть вычислять первичный ключ таблицы, хранящей версии
документа, на который ссылаемся, и хранить его значение. Идентификатор
документа, на который ссылаемся можно найти пройдя по ссылке. В случае
1 момент времени определяется также проходом по ссылке, а во втором
уже есть в ссылающемся документе.Вариант 2 - наиболее часто встречающийся вид ссылок между документами,
и разрешение ссылок может дать большой прирост производительности при
выполнении массовых операций.РетроспекцияВ процессе создания документов и установки связей участвует глобальное
значение момента времени -сейчас. Обычно этот момент получается
запросом к системным часам. Если это значение сделать параметром
(который по умолчанию берется из системных часов), то все приложение
приобретает новое качество - возможность просмотра состояния всей
системы в произвольный момент времени в прошлом. Для этого необходимо
во все процедуры выборки данных, построения отчетов и т.п. добавить
фильтрацию документов - версии послесейчасне существуют. При
этом ссылка на последнюю версию документа должна рассматриваться как
ссылка в моментсейчас. Если параметрсейчасзадан вручную, то
приложение должно работать в режиметолько для чтения.Версии документов в DjangoДля использования библиотеки необходимо включить в список приложений
приложениеdocuments:documents.modelsсодержит абстрактные
базовые классы для таблиц составляющих документ, аdocuments.fieldsтип поля для ссылки на документы.APIdocuments.modelsDocumentPart– абстрактный базовый класс для всех таблиц документа. Задает:now– дополнительный менеджер, задающий фильтр “сейчас”, где
момент времени берется из глобальной переменной (локальной для
треда). См. модульretrospection, функцииnowиset_now.
Глобальное (для треда) время устанавливается при первом
обращении иостается фиксированнымдо изменения с помощьюset_now.ConfigurationError– исключение, вызываемое в случае некорректной конфигурации
документа (например, невозможность определить главную таблицу)document_get(cls, datetime, **kwargs)– метод класса
возвращает объект (часть документа) определяемую ключевыми
параметрами и моментом времени. Если объектов нет или
несколько - вызывает исключения так же как иgetдляQuerySet.at(cls, datetime, **kwargs)– возвращаетQuerySetдля части документа определяемый
ключевыми значениями и заданным моментом времени (определяемым
по главной таблице документа). Путь к главной таблице
определяется с помощью вызова метода классаto_masterзадающего префикс доступа к главной таблице.history(self, **kwargs)– QuerySet для истории части документа в обратном
хронологическом порядке.Document(DocumentPart)– абстрактный базовый класс для главной таблицы документа. Задает:document_start– DateTimeField– начальный момент действия данной версии документа (включительно).document_end– DateTimeField– конечный момент действия (исключительно).document_id– IntegerField– уникальный идентификатор данного документа. Если не задан -
вычисляется автоматически.ChangedAlready– исключение– вызывается методомdocument_saveесли заданныйidне
является ключом последней версии документа.document_save(self, document_start=None)– записывает в базу данных новую версию объекта главной таблицы
документа. В последней версии документа (если она есть)
устанавливаетсяdocument_end(равныйdocument_startдля
новой версии). Если задан document_start, то он становится
началом действия новой версии, в противном случае берется
datetime.now().ЕслиidнеNone, то предполагается что он
задает ключ последней версии документа (находящейся в
базе). Если это не так (не последняя), вызывается исключение
(позволяет обнаружить параллельное изменение документа
несколькими пользователями). Если не задан (нулевой)
идентификатор документа, после записи в базу вызываетсяnew_document_idдля данного объекта и производится повторная
запись в базу уже с новым идентификатором. Все действия
выполняются в одной транзакции.save_now(self)– аналогично document_save, но в момент
заданный глобально (для треда).new_document_id(self)– выдает новый идентификатор для данного документа.document_get_or_404(cls, datetime, **kwargs)– аналогget_object_or_404с указанием момента времени в качестве
первого параметра.at(cls, datetime, **kwargs)– переопределяет метод базового класса для главной таблицы.history(self, **kwargs)– QuerySet для истории документа в обратном
хронологическом порядке.document_restore(self, document_start=None)– восстановить документ в активное состояние из текущей старой версии.В составных документах надо переопределять, так как порядок
сохранения частей зависит от вида связи между частями. Ссылки
на другие части можно оставить как есть, а части ссылающиеся
на этот документ надо скопировать и перепривязать к новой
версии.restore_now(self)– аналогично document_restore, но в момент
заданный глобально (для треда).bulk_documents_save(cls, documents, document_start=None)– сохранить новые версии документов (массовая загрузка).bulk_save_now(cls, documents)– аналогично bulk_documents_save,
но в момент заданный глобально (для треда).bulk_documents_delete(cls, documents, delete_time=None)– массовое удаление документов, возвращает количество удаленных.bulk_delete_now(cls, documents)– аналогично bulk_documents_delete, но в момент заданный
глобально (для треда).DocumentPartF(DocumentPart)– абстрактный базовый класс для части документа с версиями, на
которую ссылаются из главной или более близкой к главной части
документа. Определяет:to_master(cls)– выдает префикс для пути к главной таблице документа. Делается
попытка найти модель унаследованную отDocumentPartссылающуюся на данную. Если это главная таблица - то задача
решена, в противном случае вызываетсяto_masterдля
найденной модели. Если модель не найдена или найдена не одна,
то вызывается исключениеConfigurationError. В этом случае
метод надо переопределить.DocumentPartB(DocumentPart)– абстрактный базовый класс для части документа с версиями, которая
ссылается на главную или более близкую к главной часть
документа. Определяет:to_master(cls)– выдает префикс для пути к главной таблице документа. Делается
попытка найти модель унаследованную отDocumentPart, на
которую ссылаются из данной. Если это главная таблица - то
задача решена, в противном случае вызываетсяto_masterдля
найденной модели. Если модель не найдена или найдена не одна,
то вызывается исключениеConfigurationError. В этом случае
метод надо переопределить.load_related_document_fk(datetime, object_list, field)– аналог select_related для полей, ссылающихся на другой объект
через DocumentForeignKey. Загружает связанные через поле
{field} объекты в поле {field}_cached (ПОКА НЕ РЕАЛИЗОВАНО).documents.fieldsСсылки на другие документы первых двух типов реализуются с помощью
обычного поля типаForeignKey. Ссылка третьего типа представляет
интерес при создании составного документа, который всегда показывает
на последние версии других документов. Для ее реализации нужно
использовать:DocumentForeignKey(models.ForeignKey)– первый аргумент должен указывать на модель унаследованную отDocument. Остальные значения не обязательны (такие же как дляForeignKey). Для доступа к объекту, на который сделана ссылка
используются те же методы что и дляForeignKey, за исключением
чтения, при котором нужно указывать момент времени:class Source(...):
  ...
  link = DocumentForeignKey(SomeDocument)
  ...

t = datetime.now()
s = Source.objects.get(pk=123)
some_document = s.link(t)В НАСТОЯЩИЙ МОМЕНТ ФУНКЦИОНАЛЬНОСТЬ НЕ РЕАЛИЗОВАНА. ИСПОЛЬЗУЕТСЯ
ПРОСТО ЦЕЛОЕ ЗНАЧЕНИЕ.documents.retrospectionnow(request=None)– возвращает время установленное в запросе (если есть). Если нет,
или запрос не указан, возвращает текущее время треда. Если время
треда не задано – устанавливает. Последующие вызовы будут
возвращать одно и то же время до тех пор пока не закончится
обработка запроса или не будет установлено новое фиксированное
время с помощьюset_now.set_now(dt=None)– устанавливает новое фиксированное время, локальное для треда, в
dt или в datetime.now() если параметр не задан.current_time(dt=None)– контекст менеджер – временно установить время в заданное значение.@with_real_time– декоратор – устанавливает реальное время для выполнения функции.documents.adminДля того, чтобы с документами можно было работать так же, как с
обычными моделями, нужно использоватьDocumentModelAdmin. При его
использовании мы видим только последнии версии документов, и можем
просмотреть историю изменений (вместе со значениями всех полей). При
каждом сохранении создается новая версия документа. Поддерживаются
поляDocumentForeignKey(в виджете мы видим только последние версии
документов). Но следует иметь ввиду, что совсем забыть о том, что мы
работает с версионными документами, не получится. Эта часть
проверялась на ограниченном наборе связей и моделей.Middleware для ретроспекцииРеализовано вdocuments.retrospection.RetrospectionMiddleware.Параметрсейчасудобно хранить в данных сессии. В этом случае
middleware может выполнить две функции:Установку значенияnowв данных сессии (если оно не задано
пользователем).Блокировку post-запросов в случае вручную заданного ограничения по
времени (Режим только для чтения).Hе все post-запросы надо блокировать, например запросы отменяющие
режим ретроспекции. Для того, чтобы middleware “пропустило” запрос,
необходимо включить в него полеpost_in_retrospection.Для включения нужно поместитьdocuments.retrospection.RetrospectionMiddlewareв
списокMIDDLEWARE_CLASSESпослеSessionMiddleware.ПримерВ качестве примера добавим поддержку версий документов в приложение изучебника Django.В примере рассматриваются два объекта: Poll и Choice. Poll – вопрос
для голосования, Choice - варианты ответов. Счетчик количества ответов
объединен с ответом:class Poll(models.Model):
   question = models.CharField(max_length=200)
   pub_date = models.DateTimeField('date published')

   def __unicode__(self):
       return self.question

class Choice(models.Model):
   poll = models.ForeignKey(Poll)
   choice = models.CharField(max_length=200)
   votes = models.IntegerField()

   def __unicode__(self):
       return self.choiceВарианты ответа привязаны к вопросу. Внесем следующие изменения:Сделаемвопросиответдокументами с версиями.ответ- универсальный документ - может встречаться в нескольких
вопросах.Результат опроса хранится в документерезультат.Модель становится такой:class Choice(Document):
    choice = models.CharField(max_length=200)

    def __unicode__(self):
       return self.choice

class Poll(Document):
    question = models.CharField(max_length=200)

    def __unicode__(self):
       return self.question

class PollChoices(DocumentPartB):
    poll = models.ForeignKey(Poll)
    choice = models.ForeignKey(Choice)

    @classmethod
    def to_master(cls):
       return 'poll'

class PollResults(Document):
    poll = models.ForeignKey(Poll)
    choice = models.ForeignKey(Choice)
    votes = models.IntegerField()

    @staticmethod
    def vote(poll_document_id, choice_document_id):
        n = datetime.now()
        p = Poll.document_get(n, document_id=poll_document_id)
        c = Choice.document_get(n, document_id=choice_document_id)
        try:
            v = PollResults.document_get(
                n, poll__document_id=poll_document_id,
                choice__document_id=choice_document_id)
            v.votes += 1
        except PollResults.DoesNotExist:
            v = PollResults(poll=p, choice=c, votes=1)
        v.document_save()
        return v.document_idDocumentбазовый абстрактный класс для всех документов.Choiceвариант ответа. Документ.Pollвопрос для голосования. Документ.PollChoicesчасть документаPoll– содержит текущий список вариантов ответов
на вопрос.PollResultsтекущее количество ответов заданного вида на вопрос. Документ.Пример работы>>> from documents_test.docs.models import *

# Пока нет голосований
>>> Poll.objects.all()
[]

# Создаем новое
>>> p = Poll(question='Who is who?')

# Сохраняем.
>>> p.document_save()

# Теперь есть id, document_id, document_start и document_end.
>>> p.id
1
>>> p.document_id
1

# Access database columns via Python attributes.
>>> print p.question
Who is who?

>>> p.document_start # doctest: +ELLIPSIS
datetime.datetime(...)

# Give the Poll a couple of Choices.
>>> now = datetime.now()
>>> p = Poll.document_get(now, document_id=1)

# Display any choices from the related object set -- none so far.
>>> PollChoices.at(now)
[]

# Create three choices.
>>> c1 = Choice(choice='President'); c1.document_save(); c1
<Choice: President>
>>> c2 = Choice(choice='Agent'); c2.document_save(); c2
<Choice: Agent>
>>> c3 = Choice(choice='Gena Crocodile'); c3.document_save(); c3
<Choice: Gena Crocodile>

# document_id назначен автоматически:
>>> for c in (c1, c2, c3): print c.document_id
1
2
3

# Добавим их:
>>> p.document_save() # новая версия
>>> p.pollchoices_set.add(*[PollChoices(choice=c) for c in (c1, c2, c3)])

# Голосование (результат - идентификатор документа в котором сделана отметка):
>>> PollResults.vote(1, 1)
1
>>> PollResults.vote(1, 1)
1
>>> PollResults.vote(1, 1)
1
>>> PollResults.vote(1, 2)
4

# Запомним момент в промежутке (t):
>>> from time import sleep
>>> sleep(0.1)
>>> t = datetime.now()
>>> sleep(0.1)

>>> PollResults.vote(1, 2)
4
>>> PollResults.vote(1, 3)
6

# Результаты голосования:
>>> for r in PollResults.at(datetime.now(), poll__document_id=1):
...     print r.votes, r.choice.choice
3 President
2 Agent
1 Gena Crocodile

# Ретроспекция (состояние на момент t):
>>> for r in PollResults.at(t, poll__document_id=1):
...     print r.votes, r.choice.choice
3 President
1 AgentРедактор вопросов и ответовУчебник Django предлагает создавать опросник через административный
интерфейс. В нашем случае такой подход не годится. Административный
интерфейс Django ничего не знает про документы и версии, поэтому его
надо использовать для управления пользователями и, при необходимости,
для редактирования данныхв обход системы управления
версиями. Существуютстационарныеданные в системе (например
некоторые справочники), которые не могут или не должны изменяться с
течением времени (за исключением исправления ошибок), для управления
которыми административный интерфейс будет пригоден.Примечание: есть такжеdocuments.admin, но он позволяет редактировать
в админке лишь простые документы (состоящие из одной модели).