
A bunch of ExtJS + Django + Apache + SVN deploy (and a simple CRUD controller on Django)
- From the sandbox
- Tutorial
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.conf
with approximatelythe 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
*.py
Python'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.conf
add / 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 calleddb
located 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
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
{
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.py
add urlpattern:url(r'^(?P[^/]*)/.*$', 'db.views.page')
Now add the method
page
to 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
models
contains 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 queryProperties
contains 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
})
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 db
in the folder ~/Projects/db
). The path to settings.py
should 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.wsgi
that 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.append
used 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 ~/Projects/db/app.js
,~/Projects/db/js/app.js
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 SVNapp.js
as described above