Для практики в 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: пример создания приложения