Связка ExtJS+Django+Apache+SVN deploy (и простой CRUD контроллер на Django)

в 10:20, , рубрики: Apache, CentOS, crud, database, django, extjs, extjs 4, python, sencha, subversion, svn, Библиотека ExtJS/Sencha, контроллер, Питон, метки: , , , , , , , , , , , ,

Предисловие

Сразу хочу попросить прощения за столь перегруженную статью, но для меня сейчас всё это актуально и связано. Думаю что некоторым это может пригодиться для будущей разработки. Хочу обратить внимание, что в этой статье я не стану рассказывать вам как устанавливать те или иные тривиальные вещи, установка которых, к тому же, зависит от той или иной платформы. Также в статье я не описываю телодвижения по настройке прав доступа к файлам сервера, опять же, это зависит от реализации. В статье описан процесс настройки на 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 и он в этом файле инлюдится.
Необходимо также в соответствующей папке создать структуру вида:
Связка ExtJS+Django+Apache+SVN deploy (и простой CRUD контроллер на Django)
На этом настройка 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.

Метод page

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.

Вот код этих методов:

@transaction.commit_manually
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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js