Для практики в Django — решил создать более продвинутую версию домашней бухгалтерии.
Старый вариант — консольный bash-скрипт, описан в посте bash + MySQL: скрипт домашней бухгалтерии.
Да и женщина никак не хочет приобщиться к прекрасному миру консольных приложений 🙂
Имеется сервер CentOS 6.6, Python 2.7, Django 1.8.
Доступ к проекту реализован через NGINX +uWSGI, база данных — MariaDB 5.
Файл настроек uWSGI:
[uwsgi] socket = 127.0.0.1:9091 chdir = /var/www/django/money_domain_org_ua pythonpath = /usr/local/lib/python2.7/site-packages/ module = django.core.wsgi:get_wsgi_application() env = DJANGO_SETTINGS_MODULE=domain_money.settings master = True pidfile = /tmp/project-master.pid daemonize=/var/log/uwsgi/money.domain.org.ua.log plugins = python uid = setevoy gid = setevoy
NGINX:
server {
server_name money.domain.org.ua;
access_log /var/log/nginx/money.domain.org.ua-access.log;
error_log /var/log/nginx/money.domain.org.ua-error.log;
root /var/www/django/money_domain_org_ua/;
auth_basic_user_file /var/www/django/money_domain_org_ua/.htaccess;
auth_basic "Password-protected Area";
location /static/ {
alias /usr/local/lib/python2.7/site-packages/Django-1.8.1-py2.7.egg/django/contrib/admin/static/;
expires modified +1w;
}
location /
{
index index.py;
uwsgi_pass 127.0.0.1:9091;
include uwsgi_params;
}
}
Содержание
Создание проекта и приложения
Переходим в нужный каталог и создаём приложение:
$ cd /var/www/django $ django-admin startproject setevoy_money
Переименовываем корневой каталог проекта:
$ mv setevoy_money money_domain_org_ua
$ tree -L 2 money_domain_org_ua
money_domain_org_ua
├── manage.py
└── setevoy_money
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
$ cd money_domain_org_ua
Создаём базу:
MariaDB [(none)]> CREATE DATABASE setevoy_money CHARACTER SET utf8 COLLATE utf8_general_ci; Query OK, 1 row affected (0.01 sec)
MariaDB [(none)]> GRANT ALL ON setevoy_money.* TO 'setevoy_money'@'%' IDENTIFIED BY 'p@ssw0rd'; Query OK, 0 rows affected (0.02 sec)
Драйвер MySQL для Python уже установлен. Если нет — можно установить через PIP:
# pip install MySQL-python
Настраиваем подключение. В файле setevoy_money/settings.py указываем:
...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'setevoy_money',
'USER': 'setevoy_money',
'PASSWORD': 'p@ssw0rd',
'HOST': 'localhost',
}
}
...
Там же меняем часовой пояс:
... TIME_ZONE = 'EET' ...
Запускаем, проверяем что Django работает:
$ python manage.py runserver 77.***.***.20:8000
Заполняем базу:
$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: staticfiles, messages
Apply all migrations: admin, contenttypes, auth, sessions
Synchronizing apps without migrations:
Creating tables...
Running deferred SQL...
Installing custom SQL...
Running migrations:
Rendering model states... DONE
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying sessions.0001_initial... OK
Создание моделей
Создаём приложение money, которое и будет выполнять всю работу:
$ python manage.py startapp money
$ tree -L 2 . . ├── manage.py ├── money │ ├── admin.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py └── setevoy_money ├── __init__.py ├── __init__.pyc ├── settings.py ├── settings.pyc ├── urls.py ├── urls.pyc ├── wsgi.py └── wsgi.pyc
Примечание: в моделях ниже я сделал одну ошибку — согласно PEP8 имена классов дожны иметь вид ClassName, а не Class_name — как я написал. Поэтому — следует переписать Source_types, Transactions_types и Total_aval в SourceTypes, TransactionsTypes и TotalAval соответственно.
В нём редактируем файл models.py:
# -*- coding: utf-8 -*-
from django.db import models
"""будет хранить общую доступную сумму
на всех Source_types"""
class Total_aval(models.Model):
avail_date = models.DateField()
avail_sum = models.IntegerField()
class Meta:
verbose_name = 'Вcего доступно'
def __unicode__(self):
return self.avail_sum
""" у каждого юзера (члена семьи)
свои источники платежей Source_types"""
class Users(models.Model):
username = models.CharField(max_length=200)
class Meta:
verbose_name = 'Пользователи'
def __unicode__(self):
return self.username
"""источник денег - зарплатная карта,
вебмани, наличные и т.д."""
class Source_types(models.Model):
type = models.CharField(max_length=200)
user = models.ForeignKey(Users, unique=False)
source_sum = models.IntegerField()
class Meta:
verbose_name = 'Типы источников'
def __unicode__(self):
return self.type
"""типы транзакций - Приход/расход"""
class Transactions_types(models.Model):
type = models.CharField(max_length=10)
class Meta:
verbose_name = 'Типы транзакций'
def __unicode__(self):
return self.type
"""самая важная таблица
транзакции, которая будет содержать
записи о получении денег, затрате денег
тип транзакции - Приход/расход (Transactions_types)
тип источника - карта, наличные и т.д. (Source_types)
дата в формате Год-месяц-день
сумма транзакции
описание, например - "Купил XBox"""
class Transactions(models.Model):
transaction_type = models.ForeignKey(Transactions_types)
source_type = models.ForeignKey(Source_types)
transaction_date = models.DateField()
transaction_sum = models.IntegerField()
transaction_desc = models.CharField(max_length=255)
class Meta:
verbose_name = 'Транзакции'
def __unicode__(self):
return self.transaction_desc
Возвращаемся к файлу setevoy_money/settings.py и добавляем новое приложение:
...
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'money',
)
...
Проверяем модели (хотя — это совсем не панацея, у меня нашлось несколько опечаток в именах таблиц ForeignKey при создании миграции):
$ python manage.py check System check identified no issues (0 silenced).
Создаём миграцию:
$ python manage.py makemigrations money
Migrations for 'money':
0001_initial.py:
- Create model Source_types
- Create model Total_aval
- Create model Transactions
- Create model Transactions_types
- Create model Users
- Add field transaction_type to transactions
- Add field user to source_types
SQL будет выглядеть так:
$ python manage.py sqlmigrate money 0001 BEGIN; CREATE TABLE `money_source_types` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` varchar(200) NOT NULL, `source_sum` integer NOT NULL); CREATE TABLE `money_total_aval` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `avail_date` date NOT NULL, `avail_sum` integer NOT NULL); CREATE TABLE `money_transactions` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `transaction_date` date NOT NULL, `transaction_sum` integer NOT NULL, `transaction_desc` varchar(255) NOT NULL, `source_type_id` integer NOT NULL); CREATE TABLE `money_transactions_types` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` varchar(10) NOT NULL); CREATE TABLE `money_users` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(200) NOT NULL); ALTER TABLE `money_transactions` ADD COLUMN `transaction_type_id` integer NOT NULL; ALTER TABLE `money_transactions` ALTER COLUMN `transaction_type_id` DROP DEFAULT; ALTER TABLE `money_source_types` ADD COLUMN `user_id` integer NOT NULL UNIQUE; ALTER TABLE `money_source_types` ALTER COLUMN `user_id` DROP DEFAULT; ALTER TABLE `money_transactions` ADD CONSTRAINT `money_tr_source_type_id_4459214f3675887_fk_money_source_types_id` FOREIGN KEY (`source_type_id`) REFERENCES `money_source_types` (`id`); CREATE INDEX `money_transactions_92c431dc` ON `money_transactions` (`transaction_type_id`); ALTER TABLE `money_transactions` ADD CONSTRAINT `da53091746f07a70e338cee4e66034bf` FOREIGN KEY (`transaction_type_id`) REFERENCES `money_transactions_types` (`id`); ALTER TABLE `money_source_types` ADD CONSTRAINT `money_source_types_user_id_49efb61c4abcab27_fk_money_users_id` FOREIGN KEY (`user_id`) REFERENCES `money_users` (`id`); COMMIT;
Создаём таблицы:
$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: staticfiles, messages
Apply all migrations: admin, money, contenttypes, auth, sessions
Synchronizing apps without migrations:
Creating tables...
Running deferred SQL...
Installing custom SQL...
Running migrations:
Rendering model states... DONE
Applying money.0001_initial... OK
Вот как они выглядят в базе:
MariaDB [setevoy_money]> show tables where Tables_in_setevoy_money like 'money%'; +--------------------------+ | Tables_in_setevoy_money | +--------------------------+ | money_source_types | | money_total_aval | | money_transactions | | money_transactions_types | | money_users | +--------------------------+ 5 rows in set (0.00 sec)
MariaDB [setevoy_money]> desc money_source_types; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | type | varchar(200) | NO | | NULL | | | source_sum | int(11) | NO | | NULL | | | user_id | int(11) | NO | MUL | NULL | | +------------+--------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec)
MariaDB [setevoy_money]> desc money_total_aval; +------------+---------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+---------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | avail_date | date | NO | | NULL | | | avail_sum | int(11) | NO | | NULL | | +------------+---------+------+-----+---------+----------------+ 3 rows in set (0.01 sec)
MariaDB [setevoy_money]> desc money_transactions; +---------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | transaction_date | date | NO | | NULL | | | transaction_sum | int(11) | NO | | NULL | | | transaction_desc | varchar(255) | NO | | NULL | | | source_type_id | int(11) | NO | MUL | NULL | | | transaction_type_id | int(11) | NO | MUL | NULL | | +---------------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.00 sec)
MariaDB [setevoy_money]> desc money_transactions_types; +-------+-------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+-------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | type | varchar(10) | NO | | NULL | | +-------+-------------+------+-----+---------+----------------+ 2 rows in set (0.01 sec)
MariaDB [setevoy_money]> desc money_users; +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | username | varchar(200) | NO | | NULL | | +----------+--------------+------+-----+---------+----------------+ 2 rows in set (0.00 sec)
Если что-то потребовалось поменять (например — я одно поле указал как OneToOneField, а не ForeignKey) — то редактируем класс модели, и создаём новую миграцию:
$ python manage.py makemigrations money
Migrations for 'money':
0002_auto_20150512_1204.py:
- Change Meta options on source_types
- Change Meta options on total_aval
- Change Meta options on transactions
- Change Meta options on transactions_types
- Change Meta options on users
- Alter field user on source_types
$ python manage.py sqlmigrate money 0002 BEGIN; ALTER TABLE `money_source_types` DROP FOREIGN KEY `money_source_types_user_id_49efb61c4abcab27_fk_money_users_id`; ALTER TABLE `money_source_types` DROP INDEX `user_id_2`; ALTER TABLE `money_source_types` ADD CONSTRAINT `money_source_types_user_id_49efb61c4abcab27_fk_money_users_id` FOREIGN KEY (`user_id`) REFERENCES `money_users` (`id`); COMMIT;
$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: staticfiles, messages
Apply all migrations: admin, money, contenttypes, auth, sessions
Synchronizing apps without migrations:
Creating tables...
Running deferred SQL...
Installing custom SQL...
Running migrations:
Rendering model states... DONE
Applying money.0002_auto_20150512_1204... OK
Настройка админпанели
Создаём пользователя:
$ python manage.py createsuperuser Username (leave blank to use 'setevoy'): Email address: [email protected] Password: Password (again): Superuser created successfully.
Редактируем файл money/admin.py, что бы добавить приложение money в админку:
# -*- coding: utf-8 -*-
from django.contrib import admin
from .models import Transactions, Transactions_types, Source_types, Users
class TransactionsAdmin(admin.ModelAdmin):
list_display = ('transaction_desc', 'transaction_type', 'transaction_date', 'transaction_sum')
admin.site.register(Transactions, TransactionsAdmin)
admin.site.register(Transactions_types)
admin.site.register(Source_types)
admin.site.register(Users)
Перезапускаем uWSGI (хотя пока лучше было бы пользоваться runserver, конечно):
# service uwsgi restart Shutting down uWSGI Starting uWSGI [ OK ]
И проверяем:
Что бы продолжить работу — надо создать несколько записей:
Готово.
Переходим к представлениям.
Создание представлений и URLconf
Начнём с URLconf.
В файле setevoy_money/urls.py добавляем include():
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from django.contrib import admin
from money.views import index
urlpatterns = [
url(r'^$', index, name='index'),
url(r'^money/', include('money.urls')),
url(r'^admin/', include(admin.site.urls)),
]
Создаём файл money/urls.py:
# -*- coding: utf-8 -*- from django.conf.urls import url from money.views import index, transactions, add_transaction, update urlpatterns = [ url(r'^$', index, name='index'), url(r'^index', index, name='index'), url(r'^transactions', transactions, name='transactions'), url(r'^add-transaction', add_transaction, name='add_transaction'), url(r'^update', update, name='update'), ]
Отдельный URL index нужен для переадресации на главную после добавления транзакции (с помощью django.core.urlresolvers.reverse()).
Создаём представления. В файл money/views.py добавляем:
# -*- coding: utf-8 -*-
from django.http import HttpResponse
def index(request):
return HttpResponse('Index')
def transactions(request):
return HttpResponse('transactions')
def add_transaction(request):
return HttpResponse('transactions')
def update(request):
return HttpResponse('update')
Сохраняем, проверяем:
$ curl -u user:pass http://money.domain.org.ua/ Index
$ curl -u user:pass http://money.domain.org.ua/money/transactions transactions
URLconf работает.
Возвращаемся к представлениям.
Я намеренно не использую generic views тут, т.к. при таком виде — более понятно (самому себе, в первую очередь) — что и как работает. Подрасту — сделаю с общими представлениями 🙂
Приводим money/views.py к такому виду:
# -*- coding: utf-8 -*-
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.db.models import F, Sum
from models import Transactions, Transactions_types, Source_types
"""Главная страница со ссылками. суммами по каждому Source_types
и целиком"""
def index(request):
sources = Source_types.objects.all()
names = Source_types.objects.values('type')
sums = Source_types.objects.values_list('source_sum')
total = Source_types.objects.aggregate(Sum('source_sum'))
return render(request, 'index.html', {
'sources': sources,
'sums': sums,
'names': names,
'total': total.get('source_sum__sum'),
})
"""Список всех последних транзакций"""
def transactions(request):
latest_transaction_list = Transactions.objects.order_by('-transaction_date')
types = Transactions_types.objects.all()
sources = Source_types.objects.all()
return render(request, 'transactions.html', {
'latest_transaction_list': latest_transaction_list,
'types': types,
'sources': sources,
})
"""Страница добавления транзакции"""
def add_transaction(request):
types = Transactions_types.objects.all()
sources = Source_types.objects.all()
return render(request, 'add_transaction.html', {
'types': types,
'sources': sources,
})
"""Представление, которое выполняет запись в та лица транзакций.
После выполнения записи - переадресовывает на станицу списка тразакций"""
def update(request):
if request.method == 'POST':
date = request.POST['date']
desc = request.POST['desc']
sum = request.POST['sum']
type = Transactions_types.objects.get(pk=(request.POST['type']))
source = Source_types.objects.get(pk=(request.POST['source']))
# обновляем запись в списке транзакций
result = Transactions(transaction_type=type, source_type=source, transaction_date=date, transaction_sum=sum, transaction_desc=desc)
result.save()
# обновляем запись об остатке на source
# не самое красивое решение - ID могут быть и не 1 для прихода, а 2 для расхода
# а наоборот, но лучшего не придумал
if type.id == 1:
source.source_sum = F('source_sum') + sum
elif type.id == 2:
source.source_sum = F('source_sum') - sum
source.save()
return HttpResponseRedirect(reverse('index'))
Добавление шаблонов
Создаём каталог для шаблонов, находясь по прежнему в корневом каталоге проекта /var/www/django/money_domain_org_ua:
$ mkdir -p money/templates/
В нём создаём файлы шаблонов — base.html (будет родительским шаблоном), index.html, transactions.html, add_transaction.html, :
$ vim -p money/templates/index.html money/templates/transactions.html money/templates/add_transaction.html money/templates/base.html
Файл-шаблон base.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="ru">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Домашняя бухгалтерия: {% block title %}{% endblock %}</title>
</head>
<body>
{% block header %}
<p><a href="{% url 'index' %}">На главную</a> | <a href="{% url 'transactions' %}">Транзакции</a> | <a href="{% url 'add_transaction' %}">Добавить транзакцию</a> | <a href="http://money.setevoy.org.ua/admin">Админка</a></p>
{% endblock %}
<hr>
{% block content %}
{% endblock %}
<hr>
{% block footer %}
<p><a href="{% url 'index' %}">На главную</a> | <a href="{% url 'transactions' %}">Транзакции</a> | <a href="{% url 'add_transaction' %}">Добавить транзакцию</a> | <a href="http://money.setevoy.org.ua/admin">Админка</a></p>
{% endblock %}
</body>
</html>
Содержимое index.html:
{% extends "base.html" %}
{% block title %}
Главная
{% endblock %}
{% block content %}
<table border="1">
<tr><th>Тип источника</th><th>Остаток</th></tr>
{% for source in sources %}
<tr><td>{{ source.type }}</td><td>{{ source.source_sum }}</td></tr>
{% endfor %}
</table>
<p>Всего: {{ total }}</p>
{% endblock %}
Файл transactions.html:
{% extends "base.html" %}
{% block title %}
Список транзакций
{% endblock %}
{% block content %}
{% if latest_transaction_list %}
<table border="1">
<tr><th>Описание</th><th>Дата</th><th>Сумма</th><th>Тип</th></tr>
{% for transaction in latest_transaction_list %}
{% if transaction %}
<tr><td>{{ transaction.transaction_desc }}</td><td>{{ transaction.transaction_date }}</td><td>{{ transaction.transaction_sum }}</td><td>{{ transaction.transaction_type }}</td></tr>
{% endif %}
{% endfor %}
</table>
{% else %}
<p>No transaction are available.</p>
{% endif %}
{% endblock %}
И шаблон add_transaction.html:
{% extends "base.html" %}
{% block title %}
Добавление тразакции
{% endblock %}
{% block content %}
<form action="{% url 'update' %}" method="post">
<fieldset>
{% csrf_token %}
<p>Выбери расход или приход:</p>
<p> {% for type in types %}
<input type="radio" name="type" id="type{{ forloop.counter }}" value="{{ type.id }}"/>
<label for="type{{ forloop.counter }}">{{ type.type }}</label><br />
{% endfor %}</p>
<p>Выбери тип платежа:</p>
<p> {% for source in sources %}
<input type="radio" name="source" id="source{{ forloop.counter }}" value="{{ source.id }}"/>
<label for="type{{ forloop.counter }}">{{ source.type }}</label><br />
{% endfor %}</p>
<p><label for="date">Дата: </label>
<input type="date" name="date"></p>
<p><label for="sum">Сумма: </label>
<input type="text" name="sum" style="width: 70px;"/></p>
<textarea rows="4" cols="50" name="desc" placeholder="Описание"></textarea>
<input type="submit" value="Добавить" />
</fieldset>
</form>
{% endblock %}
Редактируем файл setevoy_money/settings.py и добавляем DIRS в TEMPLATES:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [[os.path.join(BASE_DIR, 'templates')]],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Перезапускаем сервер, проверяем (тут я уже добавил второй source):
Вот, как-то так оно всё работает.
Для создания использовались данные, полученные из циклов переводов:
Django Book – русский перевод
Django: пример создания приложения











