Предисловие
Сразу хочу попросить прощения за столь перегруженную статью, но для меня сейчас всё это актуально и связано. Думаю что некоторым это может пригодиться для будущей разработки. Хочу обратить внимание, что в этой статье я не стану рассказывать вам как устанавливать те или иные тривиальные вещи, установка которых, к тому же, зависит от той или иной платформы. Также в статье я не описываю телодвижения по настройке прав доступа к файлам сервера, опять же, это зависит от реализации. В статье описан процесс настройки на PDC сервер с именем tci.lan, все имена сохранены, в вашем случае их следует заменить на соответствующие вам. Данная статья содержит код, для улучшения читаемости он спрятан в спойлерах.
Постановка задачи
Недавно передо мной встала задача: написать простенькую БД для организации в которой я работаю. В принципе мне дали возможность самому выбрать архитектуру, хранилище, фреймворк и т.п. Техническое задание тоже было предоставлено весьма не однозначно — в виде списка всех аттрибутов всех моделей (при этом разделения на модели не было).
Выбор архитектуры
В выборе архитектуры я орудовался тем, что система должна быть кроссплатформенной и, желательно, не требующей дополнительной установки софта (фреймвоков, вайна, флеша, сильверлайта и т.п.). Исходя из этого остановился на веб-приложении с клиент-серверной архитектурой. Собственно этому также поспособствовало наличие в организации веб-сервера на CentOS, который я же и администрирую.
В качестве GUI выбрал ExtJS. На момент разработки, последней версией был релиз 4.2b. В качестве бэкэнда, если его так можно назвать (он скорее API-сервер), выбрал Django, в виду того, что я с ним уже сталкивался и хотел познакомиться поближе.
В качестве IDE выбрал PyCharm — вроде как один из самых нормальных IDE для Python.
Результат
В результате получилась настроенная система для разработки на ExtJS и Django:
- Веб-сервер Apache с настроенным виртуальным хостом и хэндлерами для работы со статическими файлами.
- SVN-сервер. Осуществляет контроль версий и развёртывание кода на сервере во время commit'а.
- CRUD контроллер реализованный на Django. Позволяет реализовать механизм создания, чтения, обновления и удаления записей из базы данных при помощи запросов к API.
- PyCharm с настроенным SVN'ом и возможностью локальной отладки.
- Настроенный Sencha Architect 2. Использовался для разработки ExtJS.
Настройка по-порядку
Настройка веб-сервера Apache
Итак, начнём с Apache. Будем считать что веб-сервер уже установлен. Для начала нам понадобится виртуальный хост (по крайней мере для тех, у кого на сервере хоститься более одного сайта).
Для создания виртуального хоста нужно всего лишь создать файл /etc/httpd/conf/vhosts/db.tci.lan.conf
с примерно
<VirtualHost *:80>
ServerAdmin lufton@gmail.com
ServerName www.db.tci.lan
ServerAlias db.tci.lan
DirectoryIndex index.html index.php
DocumentRoot /home/lufton/public_html/db.tci.lan/public
WSGIScriptAlias / /home/lufton/public_html/db.tci.lan/public/db/mod.wsgi
Alias /js/app.js "/home/lufton/public_html/db.tci.lan/public/db/app.js"
Alias /css "/home/lufton/public_html/db.tci.lan/public/db/css"
<Location "/css">
SetHandler None
Allow from all
</Location>
Alias /js "/home/lufton/public_html/db.tci.lan/public/db/js"
<Location "/js">
SetHandler None
Allow from all
</Location>
Alias /img "/home/lufton/public_html/db.tci.lan/public/db/img"
<Location "/img">
SetHandler None
Allow from all
</Location>
Alias /media "/usr/lib/python2.6/site-packages/django/contrib/admin/media"
<Location "/media">
SetHandler None
Allow from all
</Location>
<Location "/svnmanager">
SetHandler None
Allow from all
</Location>
LogLevel warn
ErrorLog /home/lufton/public_html/db.tci.lan/log/error.log
CustomLog /home/lufton/public_html/db.tci.lan/log/access.log combined
</VirtualHost>
LoadModule python_module modules/mod_python.so
<Directory /home/lufton/public_html/db.tci.lan/>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
allow from all
AddHandler mod_python .py
PythonHandler mod_python.publisher | .py
AddHandler mod_python .psp .psp_
PythonHandler mod_python.psp | .psp .psp_
PythonDebug On
</Directory>
Этот файл настраивает корневую папку размещения файлов сервера, устанавливает обработку файлов *.py
Python'ом и создаёт 5 alias'ов (/js/app.js
, /img
, /css
, /js
, /media
) для обслуживания статических файлов ExtJS и Django Admin. Также добавляет путь к проекту в системный путь Python'а.
Здесь lufton — имя пользователя, db.tci.lan — адрес по которому будет доступен наш сервер. Также необходимо позаботиться чтобы файлы из папки /etc/httpd/conf/vhosts/
инклюдились в файл конфига, для этого в файле /etc/httpd/conf/httpd.conf
добавьте/раскомментируйте строку:
Include /etc/httpd/conf/vhosts/*.conf
Убедитесь, также, что у вас установлен mod_wsgi и он в этом файле инлюдится.
Необходимо также в соответствующей папке создать структуру вида:
На этом настройка Apache закончена.
Настройка Subversion
Для начала нужно создать репозиторий, в моём случае он называется db
расположен по пути /srv/svn/repos/db
. После того, как репозиторий создан необходимо настроить SVN так, чтобы после каждого commit'а HEAD-файлы репозитория обновлялись в корневой папке сервера. Для этого первым делом нужно зачекаутить репозиторий в корневую папку сервера. Делается это обычным
svn checkout http://127.0.0.1/svn/db /home/lufton/public_html/db.tci.lan/public/db
Теперь нужно скопировать файл из
/srv/svn/repos/db/hooks/post-commit.tmpl
в
/srv/svn/repos/db/hooks/post-commit
Вместо mailer.py commit "$REPOS" "$REV" /path/to/mailer.conf
добавьте строки:
cd /home/lufton/public_html/db.tci.lan/public/db
/usr/bin/svn update
# python manage.py syncdb
# /etc/init.d/httpd restart
Теперь после каждого commit'а в репозиторий, папка будет обновляться автоматически, что уменьшит кол-во ваших телодвижений действий.
Создание простого CRUD контроллера
В поставленной передо мной задачей предвещалось большое кол-во действий с записями в базе данных, поэтому работа с моделями должна была быть упрощена до минимума, при этом функционал должен был быть сохранён. Моя реализации позволиляет:
- Производить операции над моделями (создание, выборку, изменение и удаление) по названию модели.
- Представлять выборку в JSON-подобной структуре (с выводом в response).
- Ограничивать выборку параметрами start и limit.
- Фильтровать выборку по точному совпадению значений параметров (оператор =).
- Сортировать выборку по названиям полей.
- Фильтровать выборку по параметру query (для каждой модели настраивается свой способ сравнения по указанному полю).
- Включать в выборку свойства моделей (не путать с полями модели).
- Включать в выборку поля и свойства связанных моделей и списков (OneToOne, ForeignKey).
Наследование от класса Model
Итак, начнём пожалуй с создания абстрактного класса, наследуемого от Model. Каждая модель теперь должна уметь представлять себя в виде JSON-подобной структуры. Для удобства я также добавил несколько полезных методов. Сразу скажу, что от Python'а далёк, так что решение может быть и не самое изящное, но всё же, рабочее.
class CoolModel ( Model ):
class Meta:
abstract = True
app_label = "db"
def __init__ ( self, *args, **kwargs ):
super(CoolModel, self).__init__(*args, **kwargs)
self.__initial = self._dict
def toDict ( self, properties = "*" ):
def getValue ( field, properties = "*" ):
value = getattr(self, field.name)
if isinstance(field, ForeignKey):
if field.name in properties:
return value.toDict(properties[field.name])
elif isinstance(value, datetime.date) or isinstance(value, datetime.datetime):
return value.isoformat()
elif isinstance(field, CommaSeparatedIntegerField) and isinstance(value, basestring):
return json.loads(value)
elif isinstance(value, Decimal):
return float(value)
elif isinstance(field, ImageField):
return value.url if value else None
elif isinstance(field, NullBooleanField):
return None if value not in (True, False) else 1 if value else 0
else:
return value
result = {}
fields = {}
for field in self._meta.fields:
fields[field.name] = field
if isinstance(field, ForeignKey):
idAttr = "%s_id" % field.name
result[idAttr] = getattr(self, idAttr)
else:
result[field.name] = getValue(field, properties)
if isinstance(properties, dict):
for k, v in properties.iteritems():
if hasattr(self, k):
value = getattr(self, k)
if isinstance(value, CoolModel):
result[k] = value.toDict(v)
elif value.__class__.__name__ == "RelatedManager":
result[k] = toJSON(value.all(), v)
elif value is None:
result[k] = {} if k in fields and isinstance(fields[k], ForeignKey) else None
else:
result[k] = value
return result
@property
def diff ( self ):
d1 = self.__initial
d2 = self._dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
return dict(diffs)
@property
def original ( self ):
try:
return self.__class__.objects.get(id = self.id)
except self.__class__.DoesNotExist:
return None
@property
def hasChanged ( self ):
return bool(self.diff)
@property
def changedFields ( self ):
return self.diff.keys()
def getFieldDiff ( self, field_name ):
return self.diff.get(field_name, None)
def save ( self, *args, **kwargs ):
super(CoolModel, self).save(*args, **kwargs)
self.__initial = self._dict
@property
def _dict ( self ):
return model_to_dict(self, fields = [field.name for field in self._meta.fields])
Класс писался для конкретных целей, так что метод toDict следует допилить напильником, главное — что вы, я надеюсь поняли как он устроен. Если в кратце, то сначала в словарь добавляются все поле-значение пары (значение зависит от класса, на этом этапе можно нужно научить метод серриализации того или иного типа/класса). Затем в словарь добавляются все свойство-значение пары из списка дополнительных свойств. Список свойств на самом деле не список, а хэш-таблица вида:
{
address: {
country: null,
state: {
type: null,
fullTitle: null
},
district: null,
city: {
type: null,
fullTitle: null
},
streetType: null,
fullAddress: null
},
type: null
}
Данная хэш-таблица говорит о том, что из указанной модели нужно выбрать ещё несколько дополнительных свойств: address и type. Каждое из которых, в свою очередь, помимо значений полей должно содержать несколько дополнительных свойств: contry, state, district, city, streetType, fullAddress для свойства address. null означает выборку только полей определённых в классе модели. Благодаря такой древовидной структуре свойства properties возможна выборка вложенных свойств и выборок.
Создание универсального handler'а
В этом разделе я расскажу вам как я реализовал унифицированный обработчик. Для начала в urls.py
добавим urlpattern:
url(r'^(?P<view>[^/]*)/.*$', 'db.views.page')
Теперь добавим метод page
в файл views.py
.
def page ( req, view ):
models = {
"countries": Country,
"statetypes": StateType,
"states": State,
"districts": District,
"cities": City,
"people": Person
#...
}
modelClass = models[view] if view in models else None
if view in models:
method = req.method
properties = json.loads(req.GET.get("properties", "{}"))
if method == "GET":
id = req.GET.get("id", None)
if id:
return read(req, modelClass, filters = {"id": id}, properties = properties)
else:
query = req.GET.get("query", None)
start = int(req.GET.get("start", 0))
limit = int(req.GET.get("limit", -1))
if limit < 0: limit = None
f = json.loads(req.GET.get("filter", "[]"))
s = json.loads(req.GET.get("sort", "[]"))
q = None
filters = {}
for filter in f: filters[filter["property"]] = filter["value"]
queryProperties = {
"countries": "title__icontains",
"states": "title__icontains",
"districts": "title__icontains",
"cities": "title__icontains",
"people": ["lastName__icontains", "firstName__icontains", "middleName__icontains"]
#...
}
if view in queryProperties and query:
if isinstance(queryProperties[view], list):
for p in queryProperties[view]:
q = q | Q(**{p: query}) if q else Q(**{p: query})
else:
q = q | Q(**{queryProperties[view]: query}) if q else Q(**{queryProperties[view]: query})
sorters = ["%s%s" % ("" if k == "ASC" else "-", v) for k, v in s]
return read(req, modelClass, start, limit, filters, q, sorters, properties)
elif method == "POST":
items = json.loads(req.raw_post_data)
return create(req, modelClass, items, properties)
elif method == "PUT":
items = json.loads(req.raw_post_data)
return update(req, modelClass, items, properties)
elif method == "DELETE":
items = json.loads(req.raw_post_data)
return delete(req, modelClass, items)
elif view in globals():
if not view in ["signin"]:
if not req.user.is_authenticated:
return JsonResponse({
"success": False,
"message": u"Вы не авторизированы!"
})
else:
if not req.user.is_superuser:
return JsonResponse({
"success": False,
"message": u"Вы не являетесь администратором!"
})
return globals()[view](req)
else:
return JsonResponse({
"success": False,
"message": u"Указанное действие (%s) не найдено!" % view
})
Словарь models
содержит ключ-значение пары, где ключ — название метода API, значение — класс соответствующей модели. Переменная queryProperties
содержит ключ-значение пары, где ключ — название метода API, значение — название поля или список таких названий (с модификациями типа "__in", "__gt", "__icontains" и т.д.). Выборка будет отфильтрована параметром query по указанным полям (фильры объеденятся оператором OR).
Осталось только реализовать методы create, read, update и delete.
def create ( req, modelClass, items, properties = None ):
results = []
try:
for item in items:
model = modelClass()
for k, v in item.iteritems():
if hasattr(model, k):
setattr(model, k, v)
model.save()
results.append(toJSON(model, properties))
transaction.commit()
return JsonResponse({
«success»: True,
«items»: results
})
except Exception, e:
transaction.rollback()
return JsonResponse({
«success»: False,
«message»: e.message
})
def read ( req, modelClass, start = 0, limit = None, filters = None, q = None, sorters = None, properties = None ):
try:
query = modelClass.objects.all()
if filters:
query = query.filter(**filters)
if q:
query = query.filter(q)
if sorters:
query = query.order_by(sorters)
count = query.count()
results = toJSON(query[start:(start+limit) if limit else None], properties)
return JsonResponse({
«success»: True,
«items»: results,
«total»: count
})
except Exception, e:
return JsonResponse({
«success»: False,
«message»: e.message
})
@transaction.commit_manually
def update ( req, modelClass, items, properties = None ):
results = []
try:
for item in items:
try:
model = modelClass.objects.get(id = item[«id»])
for k, v in item.iteritems():
if hasattr(model, k):
setattr(model, k, v)
model.save()
results.append(toJSON(model, properties))
except modelClass.DoesNotExist:
pass
transaction.commit()
return JsonResponse({
«success»: True,
«items»: results
})
except Exception, e:
transaction.rollback()
return JsonResponse({
«success»: False,
«message»: e.message
})
@transaction.commit_manually
def delete ( req, modelClass, items ):
try:
for item in items:
modelClass.objects.get(id = item[«id»]).delete()
transaction.commit()
return JsonResponse({
«success»: True
})
except Exception, e:
transaction.rollback()
return JsonResponse({
«success»: False,
«message»: e.message
})
Каждый из методов выполняет соответственно создание, выборку, изменение и удаление моделей и возвращает в response выборку или созданные/изменённые модели.
Настройка PyCharm
Собственно инчего сложного в настройке PyCharm нет. Для начала чекаутим репозиторий на локальный компьютер:
svn checkout http://db.tci.lan/svn/db ~/Projects/db
Открываем в PyCharm и создаём из шаблона проект Django. Указываем в качестве пути папку ~/Projects/db
(тут нужно быть осторожным, чтобы не создать папку db
в папке ~/Projects/db
). Путь к settings.py
должен быть ~/Projects/db/settings.py
. Добавляем созданные автоматически файлы в SVN, создаём CRUD, как описано выше.
Необходимо также создать файл mod.wsgi
, о котором мы предупредили Apache.
import os, sys
sys.path.append('/home/lufton/public_html/db.tci.lan/public')
sys.path.append('C:/Documents and Settings/lufton.TCI/Projects')
os.environ['DJANGO_SETTINGS_MODULE'] = 'db.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()
Здесь sys.path.append
используется два раза для того, чтобы конфигурация работала и на Windows, которой я ещё иногда пользуюсь.
Теперь каждый commit не только обеспечит вам контроль версий, но и автоматически задеплоит проект в указанную папку, после чего изменения моментально вступят в силу.
Естественно для локальной отладки нужно будет настроить Django на использование удалённой базы данных, я для этого просто указал в качестве сервера базы данных IP-адрес сервера.
Настройка Sencha Architect 2
В Sencha Architect есть целое множество ограничений, одно из них связано с тем, что достаточно сложно настроить публикацию в выбранную папку так, чтобы загрузка проекта в браузере не вызвала осложнений.
В нашем случае папку публикации устанавливаем в ~/Projects/db
. В свойствах Application устанавливаем appFolder: 'js/app'
.Теперь, после каждой публикации, остаётся только переместить файл вам даже не придётся перемещать файл ~/Projects/db/app.js
в ~/Projects/db/js/app.js
app.js
, потому как мы создали для него свой alias и он может превосходно выдаваться по запросу к /js/app.js
.
Заключение
Теперь у вас есть полностью рабочая и настроенная система для разработки ExtJS и Django. Процесс разработки теперь заключается в работе с SA. После очередного Publish в SA, по необходимости добавьте вновь созданные файлы моделей/хранилищ/видов/контроллеров в SVN, если производили изменения с Application'ом, то также нужно позаботиться о перемещении файла . Закоммитте проект и наблюдайте изменения.app.js
как было описано выше
Автор: lufton