A bunch of ExtJS + Django + Apache + SVN deploy (and a simple CRUD controller on Django)

Foreword

Immediately I want to apologize for such an overloaded article, but for me now all this is relevant and related. I think that for some it may come in handy for future development. I want to pay attention that in this article I will not begin to tell you how to install certain trivial things, the installation of which, moreover, depends on a particular platform. Also, in the article I do not describe the gestures for setting access rights to server files, again, it depends on the implementation. The article describes the configuration process on a PDC server named tci.lan, all the names are saved, in your case they should be replaced with the ones that correspond to you. This article contains code; to improve readability, it is hidden in spoilers.

Formulation of the problem
Recently, I faced a task: to write a simple DB for the organization in which I work. In principle, I was given the opportunity to choose the architecture, storage, framework, etc. The technical task was also provided very ambiguously - in the form of a list of all attributes of all models (there was no division into models).

Architecture choice
In the choice of architecture, I was determined that the system should be cross-platform and, preferably, not requiring additional installation of software (frameworks, vine, flash, silverlight, etc.). Based on this, I stopped at a web application with a client-server architecture. Actually, this was also facilitated by the presence of a CentOS web server in the organization, which I administer.
I chose ExtJS as the GUI. At the time of development, the latest version was release 4.2b. As a backend, if you can call it that (it's more like an API server), I chose Django, since I had already encountered it and wanted to get to know each other better.
I chose PyCharm as the IDE - sort of like one of the most normal IDEs for Python.

Result
The result is a customized system for developing on ExtJS and Django:
  • Apache web server with a configured virtual host and handlers for working with static files.
  • SVN server. Carries out version control and code deployment on the server during commit'a.
  • CRUD controller implemented on Django. Allows you to implement a mechanism for creating, reading, updating and deleting records from the database using API requests.
  • PyCharm with configured SVN and local debugging capabilities.
  • Customized Sencha Architect 2. Used to develop ExtJS.


Setting in order

Configure Apache Web Server
So, let's start with Apache. We assume that the web server is already installed. First, we need a virtual host (at least for those who have more than one site hosted on the server).
To create a virtual host you just need to create a file /etc/httpd/conf/vhosts/db.tci.lan.confwith approximately
the following contents:

  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"
  
    SetHandler None
    Allow from all
  
  Alias /js "/home/lufton/public_html/db.tci.lan/public/db/js"
  
    SetHandler None
    Allow from all
  
  Alias /img "/home/lufton/public_html/db.tci.lan/public/db/img"
  
    SetHandler None
    Allow from all
  
  Alias /media "/usr/lib/python2.6/site-packages/django/contrib/admin/media"
  
    SetHandler None
    Allow from all
  
    SetHandler None
    Allow from all
  
  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

LoadModule python_module modules/mod_python.so

  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


This file configures the root folder location server files, sets file processing *.pyPython'om and creates aliases 5 ( /js/app.js, /img, /css, /js, /media) to service ExtJS and Django Admin static files. Also adds the project path to the Python system path.
Here lufton is the username, db.tci.lan is the address where our server will be available. It is also necessary to ensure that the files from the folder are /etc/httpd/conf/vhosts/included in the config file, for this, /etc/httpd/conf/httpd.confadd / uncomment the line in the file :
Include /etc/httpd/conf/vhosts/*.conf

Also, make sure that you have mod_wsgi installed and it is in this file.
It is also necessary to create a structure of the form in the appropriate folder:

This completes the Apache configuration.

Configure Subversion
First you need to create a repository, in my case it is called dblocated along the way /srv/svn/repos/db. After the repository has been created, it is necessary to configure SVN so that after each commit, the HEAD files of the repository are updated in the server root folder. To do this, the first thing to do is check out the repository in the root folder of the server. This is becoming normal.
svn checkout 127.0.0.1/svn/db /home/lufton/public_html/db.tci.lan/public/db
Now you need to copy the file from
/srv/svn/repos/db/hooks/post-commit.tmpl
to Add the lines
/srv/svn/repos/db/hooks/post-commit
instead 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

Now, after each commit to the repository, the folder will be updated automatically, which will reduce the number of your actions movements .

Creating a simple CRUD controller
The task set for me foreshadowed a large number of actions with records in the database, so working with models should have been simplified to a minimum, while the functionality should have been preserved. My implementation allows:
  • Perform operations on models (create, select, modify and delete) by model name.
  • Present a selection in a JSON-like structure (with output in response).
  • Limit the selection to start and limit.
  • Filter the selection by the exact match of the parameter values ​​(operator =).
  • Sort the selection by field names.
  • Filter the selection by query parameter (each model has its own way of comparing by the specified field).
  • Include model properties in the selection (not to be confused with model fields).
  • Include in the selection fields and properties of related models and lists (OneToOne, ForeignKey).


Model class inheritance
So, let's start by creating an abstract class inherited from Model. Each model should now be able to present itself as a JSON-like structure. For convenience, I also added some useful methods. I must say right away that it is far from Python, so the solution may not be the most elegant, but still working.
Class text:
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])


The class was written for specific purposes, so the toDict method should be finished with a file, the main thing is that you, I hope, understood how it works. If in short, then first the entire field is added to the dictionary, the value of the pair (the value depends on the class, at this stage you can teach the method of serialization of one type or another type / class). Then, the entire property-value of the pair from the list of additional properties is added to the dictionary. The property list is actually not a list, but a hash table of the form:
{
	address: {
		country: null,
		state: {
			type: null,
			fullTitle: null
		},
		district: null,
		city: {
			type: null,
			fullTitle: null
		},
		streetType: null,
		fullAddress: null
	},
	type: null
}

This hash table indicates that from the specified model you need to select some more additional properties: address and type. Each of which, in turn, in addition to the field values ​​must contain several additional properties: contry, state, district, city, streetType, fullAddress for the address property. null means fetching only the fields defined in the model class. Thanks to such a tree structure of the properties property, it is possible to select nested properties and selections.

Creating a universal handler
In this section, I will tell you how I implemented a unified handler. To get started, urls.pyadd urlpattern:
url(r'^(?P[^/]*)/.*$', 'db.views.page')

Now add the method pageto the file views.py.
Page method
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(**{p: query}) if q else Q(**{p: query})
					else:
						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
		})


The dictionary modelscontains the key-value of the pair, where the key is the name of the API method, value is the class of the corresponding model. The variable queryPropertiescontains the key-value of the pair, where the key is the name of the API method, the value is the name of the field or a list of such names (with modifications like "__in", "__gt", "__icontains", etc.). The selection will be filtered by the query parameter by the specified fields (the filters will be combined by the OR operator).
It remains only to implement the create, read, update and delete methods.
Here is the code for these methods:
@ 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
})

Each of the methods respectively creates, selects, changes and deletes models and returns to response a selection or created / modified models.

Configure PyCharm
Actually, there is nothing else complicated in setting up PyCharm. First, check out the repository on the local computer:
svn checkout db.tci.lan/svn/db ~/Projects/db
Open PyCharm and create the Django project from the template. We specify the folder as the path ~/Projects/db(here you need to be careful not to create a folder dbin the folder ~/Projects/db). The path to settings.pyshould be ~/Projects/db/settings.py. Add the automatically generated files to SVN, create a CRUD, as described above.
You must also create the file mod.wsgithat we warned Apache about.
Its contents may be something like this:
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()


Here it is sys.path.appendused twice so that the configuration works on Windows, which I still sometimes use.
Now each commit will not only provide you with version control, but will automatically shut down the project in the specified folder, after which the changes will immediately take effect.
Naturally, for local debugging, you will need to configure Django to use a remote database, for this I just specified the IP address of the server as the database server.

Sencha Architect 2 setup
Sencha Architect has a whole host of limitations, one of which is due to the fact that it’s rather difficult to configure the publication in the selected folder so that loading the project in the browser does not cause complications.
In our case, set the publication folder to ~/Projects/db. We set it in the Application properties. appFolder: 'js/app'Now, after each publication, it remains only to move the file ~/Projects/db/app.js,~/Projects/db/js/app.js you don’t even have to move the file app.js, because we created our alias for it and it can be perfectly displayed on request to /js/app.js.

Conclusion

You now have a fully operational and customized system for developing ExtJS and Django. The development process now consists of working with SA. After the next Publish in SA, if necessary, add the newly created model / storage / view / controller files to SVN , if you made changes with Application, then you also need to take care of moving the file app.jsas described above . Submit the project and watch the changes.

Also popular now: