-
Понадобилось написать несложное веб-приложение с веб-админкой, начитался восторженных отзывов о django и решил попробовать.
Цель такая. Нужен список пользователей с паролями (табличка users) и список IP-адресов этих пользователей плюс некоторые атрибуты (табличка addresses), отношение 1 ко многим.
Пользователи должны логиниться на отдельной страничке и им по выбранному из базы списку их адресов будет показываться статистика (вопросы хранения и извлечения статистики не важны в данном примере, можно считать что пользователь просто увидит список своих адресов и всё).
Также должен быть администратор (или администраторы) которые могут добавлять логины/пароли пользователей и указывать список их адресов.
Третий компонент - cron job которому надо читать из базы список пользователей и их адресов.
Проблема в том что я не дружу с OOP и испытываю сложности с пониманием того как работают написанные кем-то сложные объектные модели. Вопросы пока такие:
1. Должен ли я создать свою модель users или допустимо использование единой базы django.contrib.auth? Не появится ли во втором случае у пользователей какого-либо незапланированного мной функционала доступного по умолчанию? Если возможна работа на базе штатного модуля, как при необходимости добавить к нему дополнительные поля?
2. Я хочу хранить IP адрес в виде INT UNSIGNED (mysql) и для преобразования в читабельный вид использовать inet_ntoa и inet_aton (это позволит мне при необходимости легко выбирать диапазон адресов). Как сделать чтобы админка django с этим нормально работала (выполняла необходимые преобразования)? Также для удобной забивки базы хотелось бы в том же окне редактирования таблицы addresses (для конкретного юзера) иметь поле в которое можно было ввести диапазон в виде 123.45.67.89/24 а затем проверив его на валидность и преобразовав в последовательность адресов вставить или удалить из таблицы (и всё это не теряя штатных функций админки по редактированию таблицы).
3. Как в отдельном не интерактивном скрипте (вызываемом по крону) подключить и использовать ORM django (и ничего лишнего) для обращения к базе? -
Да, можно(и даже нужно) использовать. Не будет хватать полей и возможностей. всегда от него можно отнаследоваться или воспользоваться системой профилей.
Для этого пишется свои поля, как для модели так и для формы(ещё и с виджетом своим), где можно предусмотреть все нужные валидации и действия.
Наждо настроить оружение джанги для этого есть несколько способов, как описаных в документации, так и в мнгочисленных постах блоггеров.
-
Добро пожаловать!
- Должен ли я создать свою модель users или допустимо использование единой базы django.contrib.auth? Не появится ли во втором случае у пользователей какого-либо незапланированного мной функционала доступного по умолчанию? Если возможна работа на базе штатного модуля, как при необходимости добавить к нему дополнительные поля?
Использование стандартной модели User сильно рекомендуется. По умолчанию юзер может только логиниться, все дополнительные действия (доступ в админку, редактирование моделей, специальные права) должны разрешаться ему явно (в той же админке).
По поводу дополнительных полей есть по большому счету три разных способа:
- дописывание атрибутов к модели в рантайме ("манки-патчинг")
- наследование от модели User своей модели юзера
- привязывание своей модели профиля к User отношением 1-1
Я бы ни в коем случае не рекомендовал пользоваться первым способом. Он многими любим за кажущуюся простоту и ковбойскую крутость, но это дорога к maintenance hell, наложению полей разных приложений и прочим "прелестям" неявной магии.
Второй и третье физически одно и то же — отдельная таблица с дополнительными полями, привязанная к User 1-1. Разница только синтаксическая. Мне, например, удобно думать об этих данных как об отдельном профиле, свойственном приложению, а не как о расширенном User'е.
- Я хочу хранить IP адрес в виде INT UNSIGNED (mysql) и для преобразования в читабельный вид использовать inetntoa и inetaton (это позволит мне при необходимости легко выбирать диапазон адресов). Как сделать чтобы админка django с этим нормально работала (выполняла необходимые преобразования)?
Вообще, в Джанге есть IPAdressField. Правда, под MySQL это переводится в varchar(15), поэтому вам не подойдет. Чтобы хранить это целым числом, надо будет написать наследника от PositiveIntegerField'а и написать ему две функции преобразования: из строки в число и обратно. Про то, как это делать, есть подробная инструкция: http://docs.djangoproject.com/en/dev/howto/custom-model-fields/. Она большая, потому что подробная, а не потому что сложная :-)
Также для удобной забивки базы хотелось бы в том же окне редактирования таблицы addresses (для конкретного юзера) иметь поле в которое можно было ввести диапазон в виде 123.45.67.89/24 а затем проверив его на валидность и преобразовав в последовательность адресов вставить или удалить из таблицы (и всё это не теряя штатных функций админки по редактированию таблицы).
Для этого форму редактирования надо заменить своей, в которую будет добавлено такое поле. Тоже штатное средство, но на память я не помню, описано ли это где-то подробно. Если соберетесь делать, напишите, расскажем.
- Как в отдельном не интерактивном скрипте (вызываемом по крону) подключить и использовать ORM django (и ничего лишнего) для обращения к базе?
Оформить его в виде команды Джанго. Тогда в скрипте сразу будет нужная среда.
-
Спасибо за подробные ответы!
Написал модель, вроде даже валидацию делать не пришлось из-за того что выдаю в качестве form_class стандартный IPAddressField:
from django.db import models from socket import inet_aton, inet_ntoa from struct import pack, unpack ROUTER_CHOICES = ( ('R1', 'Router 1'), ('R2', 'Router 2'), ('R3', 'Router 3') ) IPTYPE_CHOICES = ( ('R', 'Real'), ('P', 'Private') ) class IPField(models.Field): empty_strings_allowed = False __metaclass__ = models.SubfieldBase def get_db_prep_value(self, value): # print "to db", type(value), value if value is None: return None return unpack('!L', inet_aton(value))[0] def get_internal_type(self): return "PositiveIntegerField" def to_python(self, value): if type(value).__name__ in ('NoneType', 'unicode'): return value try: # print "to py", type(value), value return inet_ntoa(pack('!L', value)) except (TypeError, ValueError): raise models.exceptions.ValidationError("IP address cannot be converted to string.") def formfield(self, **kwargs): defaults = {'form_class': models.forms.IPAddressField} defaults.update(kwargs) return super(IPField, self).formfield(**defaults) class Addresses(models.Model): router = models.CharField("Router ID", max_length=3, choices=ROUTER_CHOICES) address = IPField("IP address") adrtype = models.CharField("Type", max_length=1, choices=IPTYPE_CHOICES)
В admin.py пришлось сделать list_display = ('address', 'adrtype', 'router') чтобы показывались колонки а не абстрактный Address object. Кстати, правильно ли я понял что в современном Django админка описывается в отдельном файле и в модель уже нельзя добавлять класс Admin?
В каких направлениях теперь нужно смотреть чтобы привести админку в юзабельное состояние?
Для начала как делается связка с таблицей пользователей (и как вообще в джанго принято организовывать редактирование подобных связок)? Скажем если бы я делал всё на PHP, возможно переход в редактор addresses у меня был бы по клику на строке из users, либо подумал насчёт варианта размещения обеих форм на одной странице.
Ну и хотелось бы в разделе admin/clientdb/addresses/ иметь возможность групповых операций с помощью отдельного поля ввода cidr нотации и возможно пометки записей для последующего удаления или массовой смены одного из атрибутов. Также было бы удобно непосредственное редактирование полей типа CHOICE в таблице.
EDIT: если кому-то пригодится вышеприведённый код, то будьте внимательны - в нём не хватает метода get_db_prep_lookup который должен уметь выполнять преобразования типов для работоспособности поиска по этому полю -
Кстати, правильно ли я понял что в современном Django админка описывается в отдельном файле и в модель уже нельзя добавлять класс Admin?
Ага, она теперь совсем отделена от определения моделей.
В каких направлениях теперь нужно смотреть чтобы привести админку в юзабельное состояние?
http://docs.djangoproject.com/en/dev/ref/contrib/admin/
Для начала как делается связка с таблицей пользователей (и как вообще в джанго принято организовывать редактирование подобных связок)
Это два отдельных вопроса.
Чтобы связать модели, нужно в Address написать ForeignKey на User:
from django.contrib.auth.models import User class Address(models.Model): user = models.ForeignKey(User) router = models.CharField("Router ID", max_length=3, choices=ROUTER_CHOICES) address = IPField("IP address") adrtype = models.CharField("Type", max_length=1, choices=IPTYPE_CHOICES)И кстати, принято писать названия классов моделей в единственном числе, так потом все естественней выглядит. Соответственно, после такого объявления не только у каждого объекта Address будет ссылка
address.user, но и у каждого юзера будет ссылкаuser.address_set. Если не нравится автоматическое название "address_set", можно указать свое:user = models.ForeignKey(User, related_name='addresses')В админке это можно редактировать двояко: либо как отдельную модель адресов, либо как список адресов внутри модели юзера. Второе делается через inline-модели (в документашке выше есть).
Ну и хотелось бы в разделе admin/clientdb/addresses/ иметь возможность групповых операций
Вот групповых операций в штатной админке пока нет. В принципе, все дописываемо извне некими хаками, но тут я не специалист.
-
Иван, спасибо за выделенное на ответы время. Ещё один вопрос, и я пошёл читать документацию :)
Насколько вот это описание http://www.b-list.org/weblog/2006/jun/13/how-django-processes-request/ в целом соответствует архитектуре текущего Django (у меня 1.0.2)? -
Соответствует полностью. Эта архитектура — пожалуй и есть суть Джанго. Она не меняется с момента открытия фреймворка в 2005 году.
-
Отщеплен новый топик "Развёртывание: apache + mod_wsgi".
-
В админке это можно редактировать двояко: либо как отдельную модель адресов, либо как список адресов внутри модели юзера. Второе делается через inline-модели (в документашке выше есть).
Сделал и так и так. В модели юзера для удобства свернул все fieldset'ы кроме первого чтобы не загромождало страницу. Правда не очень нравится что пришлось скопировать параметр fieldsets из UserAdmin вместе с импортом ugettext_lazy. Можно как-то в MyUserAdmin модифицировать fieldsets класса-предка и проставить им 'classes'='collapse' не трогая ничего более? И кстати правильно ли я понял, что fieldsets это свойство класса а не его экземпляра?
Для этого форму редактирования надо заменить своей, в которую будет добавлено такое поле. Тоже штатное средство, но на память я не помню, описано ли это где-то подробно. Если соберетесь делать, напишите, расскажем.
Теперь собственно про групповые операции... На данный момент через inline редактор всё несколько удобнее нежели непосредственное редактированием модели, т.к. в TabularInline всё достаточно компактно расположено и сравнительно небольшим количеством кликов я могу изменить поля типа choice или удалить несколько записей одновременно.
Однако при большом количестве записей начинает мешать осутствие фильтров и поиска по inline-модели, поэтому решил посмотреть в сторону расширения режима просмотра модели в виде списка (myapp/mymodel/, шаблон change_list.html). Пока что из стандартных вещей добавил поиск по адресу и фильтр по полям с небольшим количеством вариантов (router, adrtype). Добавление фильтра по имени пользователя не подошло, т.к. список пользователей может быть достаточно большим. Далее вопросы по увеличению удобства работы с большим числом записей такие:
* как поле user сделать ссылкой на inline-редактор (auth/user/<id>/)
* возможно ли создание своего фильтра с полем типа text и собственной логикой выборки записей из таблицы на основе этого поля?
* сузив при помощи фильтров выборку до нужного диапазона, неплохо бы иметь возможность пометки строк таблицы с помощью checkbox'ов
* добавить под таблицей свою форму и кнопки для группового изменения атрибутов выбранного подмножества, плюс форму добавления подсети
Ещё из мелочей не понял каким образом можно в разделе добавления или редактирования записи модели (разделы админки myapp/mymodel/add/ и myapp/mymodel/<id>/) расположить поля в строку горизонтально. -
Можно как-то в MyUserAdmin модифицировать fieldsets класса-предка и проставить им 'classes'='collapse' не трогая ничего более?
В принципе можно. Написать к своему классу метакласс, в котором пробежаться по унаследованным атрибутам и их поменять. Но на практике я бы не стал так делать. Настройка админки довольно статичная вещь и пользы от реюза там немного, поэтому тут copy-paste, мне кажется, самое то.
И кстати правильно ли я понял, что fieldsets это свойство класса а не его экземпляра?
Да. Раз оно прописано в классе, а не в
__init__создается — это атрибут класса.как поле user сделать ссылкой на inline-редактор (auth/user/<id>/)
В change_list можно выводить не только поле, но и любой атрибут. Сделайте у модели атрибут, который будет выдавать наружу HTML со ссылкой:
class MyModel(models.Model): # ... def user_link(self): return '<a href="%s">%s</a>' % ('/admin/auth/user/' % self.user_id, self.user) user_link.allow_tags = Trueвозможно ли создание своего фильтра с полем типа text и собственной логикой выборки записей из таблицы на основе этого поля?
У нследника AdminSite нужно переопределить метод queryset, который получает в параметрах request, из которого можно вытащить любые GET-параметры и как угодно дофильтровать возвращаемый queryset.
- сузив при помощи фильтров выборку до нужного диапазона, неплохо бы иметь возможность пометки строк таблицы с помощью checkbox'ов
- добавить под таблицей свою форму и кнопки для группового изменения атрибутов выбранного подмножества, плюс форму добавления подсети
Посмотрите на batchadmin. Сам я с ним не работал, но народ отзывается хорошо.
-
В принципе можно. Написать к своему классу метакласс, в котором пробежаться по унаследованным атрибутам и их поменять.
К сожалению концепция метаклассов пока для меня малопонятна... Я сделал вот так, правда не уверен что побочных эффектов нет:
class MyUserAdmin(UserAdmin): fieldsets = tuple((f[0], f[1] if f[0] is None else dict([(k, v) for k, v in f[1].iteritems()]+[('classes', ('collapse',))]) or f[1]) for f in UserAdmin.fieldsets) inlines = [AddressAdminInline, ]
По крайней мере после такой операции у родителя fieldsets не поменялся.
За batchadmin спасибо, пошёл смотреть. Если и не целиком использую, то хотя бы как там всё сделано погляжу.
И ещё попутно нашёл пару примеров по написанию собственных FilterSpec:
http://www.djangosnippets.org/snippets/1051/
http://tilarids.blogspot.com/2008/03/django-custom-filterspecs.html
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.


