В этой статье рассказывается, как за короткое время решить с помощью фреймворка Django, простую, но интересную задачу: создание системы баг-трекинга (система тикетов) для своего проекта. Наша система будет интегрирована с системой аутентификации пользователей Django и административным интерфейсом. Несмотря на свою примитивность, такое приложение, при некотором усовершенствовании, вполне может быть полезно для разработчика-одиночки или небольшой группы.
Я опишу действия в linux-окружении, используя Django версии 1.6, поэтому надо иметь ввиду, что для других операционных систем и версий фреймворка, что-то может работать по-другому (но без существенных изменений). Уровень статьи рассчитан на новичков, однако заострять внимание на подготовке рабочего окружения и разжевывать совсем элементарные вещи я не буду, и, если вам не понятно, что вообще делать вначале, рекомендую почитать вот эту прекрасную статью и пройти Django tutorial.
Итак, какие задачи должно выполнять наше приложение:
В целях соблюдения концепции минималистичности багрепорт (или тикет) будет содержать название (title), описание (description), дату и время создания (created), автора (author), и статус открыт\закрыт (closed).
Процесс работы над приложением будет разделен на несколько этапов, и, чтобы упростить перемещение от одного этапа к другому, я создал репозиторий на GitHub со всеми исходными кодами и коммитами на каждом этапе. Конечно, можно просто делать копипаст кода из статьи, но я предлагаю поступить иначе:
Вы получите полный код готового проекта, в вашей рабочей среде. Можно запустить
Прелесть git в том, что можно легко откатиться на любой этап разработки проекта, и для вашего удобства, я эти этапы пометил тегами. Итак, выполняем команду:
Перейдем в папку проекта и создадим приложение bugtracker
Вносим наше приложение в project/settings.py (добавив
Сразу настроим каталоги для темплейтов и статики:
Добавим такой urlpattern в project/urls.py —
Затем создаем модель для багрепорта в файле bugtracker/models.py:
Рассмотрим ее поближе: title — это название тикета, например «Пепелац не взлетает», text — описание и шаги для воспроизведения бага, created — время и дата создания тикета, будет автоматически заполняться благодаря
Отредактируем bugtracker/admin.py, чтобы появилась возможность управлять тикетами из админки:
И запустим
В результате этого, в базе данных создадутся необходимые таблицы, и будут запрошены логин, e-mail и пароль для создания пользователя-администратора.
Внимание, далее по тексту, при команде git checkout из git-репозитория db.sqlite3, и администратор имеет логин: admin, пароль: 123 (один-два-три)
Создав его, можно запускать:
И заходить по адресу http://127.0.0.1:8000/admin, а после ввода логина и пароля, создавать, редактировать и удалять тикеты.
Выполняем:
После этого админка станет намного удобнее. Можно отметить пункт «Редактирование багрепортов» как выполненный.
Следующее, что мы сделаем — добавим возможность просмотра списка тикетов, не заходя в административный интерфейс. Для этого нам нужно создать базовый html-шаблон и шаблон собственно списка тикетов. Базовый шаблон будет находиться в файле templates/base.html:
Как видите он очень прост и содержит в себе два блока, для заголовка и содержимого.
Шаблон списка создадим в файле templates/list.html
Все это будет подставлено в базовый шаблон вместо
Теперь напишем class-based view для этой функции в файле bugtracker/views.py, он очень прост:
Здесь определена модель для отображения в виде списка, и шаблон для отображения.
Осталось только, создать новый паттерн для URL "/bugs/" в файле bugtracker/urls.py: добавив
Заходим на http://127.0.0.1:8000/bugs/ и видим страшноватую таблицу со списком всех наших тикетов. Так как дизайн мы прикрутим потом, можем смело поставить статус «Closed» для этого этапа в админке.
Выполняем:
Добавим возможность просмотра каждого тега по-отдельности, используя DetailView, действуем по старой схеме.
Создаем шаблон templates/detail.html:
Создаем view в bugtracker/views.py:
и в bugtracker/urls.py:
в urlpattens добавляем
Кроме того, добавляем в шаблон templates/list.html ссылку на такой URL для каждого тикета, можно например сделать так:
благодаря url 'detail' наши урлы будут генерироваться автоматически, и даже если структура урлов поменяется, то шаблон не «поломаются»
Проверяем, перейдя на http://127.0.0.1:8000/bugs/ и затем кликнув по любой из ссылок. Если все работает, отмечаем в админке, что тикет закрыт.
Правильнее, конечно, вынести работу с пользователями в отдельное приложение и воспользоваться, например, django-registration для удобной работы с активацией, сменой и восстановлением паролей и т.д., но так как мы обучаемся, то не будем использовать батарейки, дабы не умножать сущности сверх необходимого.
При регистрации пользователей мы будем использовать встроенные функции django по работе с формами, для этого создадим шаблон templates/register.html:
В случае, если юзер не залогинен, то мы отображаем форму, которые передается в шаблон из следующего view (bugtracker/views.py):
success_url — это куда пользователя будет направлять при успешной регистрации, урл будет излечен из urlpatterns с помощью reverse_lazy('index'), то есть в нашем случае "/bugs/"
Дополним базовый шаблон ссылкой на форму регистрации, которая будет отображаться только если юзер не залогинен:
Не забываем добавить импорт RegisterView в bugtracker/urls.py и в urlpattern —
Теперь можно проверить работоспособность и перейти к следующему этапу.
Здесь все просто, мы будем использовать встроенные джанговские шорткаты. Для логаута шаблон, не понадобиться, так как отображать-то в этой функции и нечего, нас просто будет переносить на главную страницу. Поэтому создаем шаблон только для логина (templates/login.html):
В bugtracker/urls.py пропишем следующее:
и
А templates/base.html переделаем так:
Что бы после логина нас редиректило на главную страницу в settings.py пропишем
Поиграем вдоволь, создавая новых пользователей, логинясь и разлогиниваясь под ними.
Шаблон templates/add.html очень похож на тот, что мы создавали для RegisterView, только наоборот — функционал доступен авторизованному пользователю:
Добавляем куда-нибудь в базовый шаблон ссылочку на add:
View будет таким,
То есть пользователю будут видны только поля title и text, а благодаря переопределению метода form_valid в поле user будет подставлен индекс текущего пользователя.
Прописываем паттерн для урла 'add/' в bugtracker/urls.py, не забыв импортировать BugCreateView:
Попробуем добавить новых тикетов, и переходим к следующему этапу.
Все это более-менее удовлетворительно работает, но выглядит страшновато. На мой взгляд, самый простой и удобный способ улучшить наш интерфейс, доступный даже неспециалисту в верстке (как я) — это использовать twitter bootstrap. Для этого скачаем соответствующую библиотеку и распакуем её в папку static. После чего мы приступим к верстке шаблонов. У bootstrap куча примеров, поэтому особых сложностей возникнуть не должно. Поскольку, приводить здесь простыни получившегося HTML не имеет смысла, а нюансы верстки под bootstrap выходят за рамки этой статьи (и моих познаний), просто посмотрите исходники в репозитории, или выполните
Стоит заметить, что третья версия bootstrap требует добавлять class=«form-control» к инпутам в формах.Известный мне самый простой метод, сделать это в шаблонах django (не используя дополнительных батареек) — это кастомный фильтр, который описан здесь. Буду благодарен, если более искушенные читатели моей статьи поделятся своим опытом в комментариях.
Буквально за пару часов, а то и меньше, мы с вами создали вполне функциональное приложение, которое при дальнейшем улучшении, настройке и развитии, может хорошо послужить, в каких то других проектах, а может быть и как standalone-сервис. Вот мои несколько вполне реализуемых идей для его улучшения:
Жду ваших замечаний, идей и предложений в комментариях. Спасибо!
UPD:Пока я готовил статью, вышел релиз django 1.7, в которую внедрена поддержка миграций, что раньше осуществлялось с помощью south. Команда syncdb теперь deprecated, поэтому, после изменений в моделях делаем:
./manage.py makemigrations <имя_приложения>
./manage.py migrate
Для просмотра SQL-запроса соответствующего миграции, выполняем
./manage.py sqlmigrate <имя_приложения> <номер_миграции>
Я опишу действия в linux-окружении, используя Django версии 1.6, поэтому надо иметь ввиду, что для других операционных систем и версий фреймворка, что-то может работать по-другому (но без существенных изменений). Уровень статьи рассчитан на новичков, однако заострять внимание на подготовке рабочего окружения и разжевывать совсем элементарные вещи я не буду, и, если вам не понятно, что вообще делать вначале, рекомендую почитать вот эту прекрасную статью и пройти Django tutorial.
Итак, какие задачи должно выполнять наше приложение:
- Редактирование багрепортов (доступно для администраторов, через стандартную админку Django)
- Просмотр багрепортов в виде списка и по отдельности (доступно для всех посетителей)
- Возможность регистрации в качестве нового пользователя
- Соответственно, login и logout через веб-интерфейс
- Добавление багрепортов через веб-интерфейс (доступно только для залогиненых пользователей)
В целях соблюдения концепции минималистичности багрепорт (или тикет) будет содержать название (title), описание (description), дату и время создания (created), автора (author), и статус открыт\закрыт (closed).
Процесс работы над приложением будет разделен на несколько этапов, и, чтобы упростить перемещение от одного этапа к другому, я создал репозиторий на GitHub со всеми исходными кодами и коммитами на каждом этапе. Конечно, можно просто делать копипаст кода из статьи, но я предлагаю поступить иначе:
- Подготоваливаете ваше рабочее окружение, устанавливаете django
- Если у вас еще нет аккаунта на GitHub — создаете его
- Делаете форк (fork) моего репозитория
- В рабочей папке (в шелле) выполняете команду git clone <url вашего репозитория>
- Далее, в начале каждого этапа, я буду указывать команду git checkout -f <part-#>, которую необходимо выполнить, чтобы код в рабочей папке синхронизировался с соответствующим коммитом репозитория.
Вы получите полный код готового проекта, в вашей рабочей среде. Можно запустить
cd django-tutorial-bugreport
./manage.py runserver
и в браузере перейдя по адресу 127.0.0.1:8000/bugs «пощупать» финальный результат.Этап №0 — создание приложения
Прелесть git в том, что можно легко откатиться на любой этап разработки проекта, и для вашего удобства, я эти этапы пометил тегами. Итак, выполняем команду:
git checkout -f part-0
— тем самым сбросив проект к начальному состоянию, каким бы он был, если бы вы только что выполнили команду django-admin.py startproject…Перейдем в папку проекта и создадим приложение bugtracker
./manage.py startapp bugtracker
Вносим наше приложение в project/settings.py (добавив
'bugtracker'
в кортеж INSTALLED_APPS
)Сразу настроим каталоги для темплейтов и статики:
TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates/'),)
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
Добавим такой urlpattern в project/urls.py —
url(r'^bugs/', include('bugtracker.urls'))
, после чего, все остальные нужные нам урлы уже будем описывать именно в bugtracker\urls.py, который мы создадим вот таким, пока почти пустым, иначе django будет выдавать ошибку:# coding: utf-8
from django.conf.urls import patterns, include, url
urlpatterns = patterns('',
)
Затем создаем модель для багрепорта в файле bugtracker/models.py:
# coding: utf-8
from django.db import models
from django.contrib.auth.models import User
class Ticket(models.Model):
title = models.CharField(max_length=128)
text = models.TextField()
created = models.DateTimeField(auto_now=True)
closed = models.BooleanField(default=False)
user = models.ForeignKey(User,)
def __unicode__(self):
return self.title
Рассмотрим ее поближе: title — это название тикета, например «Пепелац не взлетает», text — описание и шаги для воспроизведения бага, created — время и дата создания тикета, будет автоматически заполняться благодаря
auto_now=True
, closed — простейший вариант описания статуса тикета, закрыт он или открыт, по-умолчанию (при созданию) closed = False
, то есть тикет не закрыт, user — пользователь, сообщивший о баге. В качестве юникодного представления модель возвращает titleОтредактируем bugtracker/admin.py, чтобы появилась возможность управлять тикетами из админки:
# coding: utf-8
from django.contrib import admin
from .models import Ticket
admin.site.register(Ticket)
И запустим
./manage.py syncdb
В результате этого, в базе данных создадутся необходимые таблицы, и будут запрошены логин, e-mail и пароль для создания пользователя-администратора.
Внимание, далее по тексту, при команде git checkout из git-репозитория db.sqlite3, и администратор имеет логин: admin, пароль: 123 (один-два-три)
Создав его, можно запускать:
./manage.py runserver
И заходить по адресу http://127.0.0.1:8000/admin, а после ввода логина и пароля, создавать, редактировать и удалять тикеты.
Этап №1 — список тикетов
Выполняем:
git checkout -f part-1
— по этой команде, появится файл db.sqlite3 содержащий ту самую базу данных, в которой уже созданы некоторые тикеты для тестирования, и логин администратора: admin, пароль: 123. Посмотрев содержимое тикетов, вы увидите, что я использовал небогатый функционал нашего приложения в качестве TODO-списка для него самого. Вид конечно в административном интерфейсе непригляден, и мы попробуем это исправить. Добавим в bugtracker/admin.py новый класс, который определяет интерфейс списка тикетов в админке и зарегистрируем его:class TicketAdmin(admin.ModelAdmin):
list_display = ('closed', 'title', 'text', 'created', 'user')
list_filter = ['created', 'closed']
search_fields = ['title', 'text']
admin.site.register(Ticket, TicketAdmin)
После этого админка станет намного удобнее. Можно отметить пункт «Редактирование багрепортов» как выполненный.
Следующее, что мы сделаем — добавим возможность просмотра списка тикетов, не заходя в административный интерфейс. Для этого нам нужно создать базовый html-шаблон и шаблон собственно списка тикетов. Базовый шаблон будет находиться в файле templates/base.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Bugtracker - {% block title_block %}{% endblock %}</title>
</head>
<body>
{% block content_block %}
{% endblock %}
</body>
</html>
Как видите он очень прост и содержит в себе два блока, для заголовка и содержимого.
Шаблон списка создадим в файле templates/list.html
{% extends 'base.html' %}
{% block title_block %}Main{% endblock %}
{% block content_block %}
<div>
<h1>Bug list</h1>
<table>
<thead>
<tr>
<td>Title</td>
<td>Created</td>
<td>Author</td>
<td>Status</td>
</tr>
</thead>
<tbody>
{% for ticket in object_list %}
<tr>
<td>{{ ticket.title }}</td>
<td>{{ ticket.created|date }}</td>
<td>{{ ticket.user }}</td>
<td>{{ ticket.closed|yesno:"CLOSED, OPENED" }}</td>
</tr>
{% empty %}
<tr>
<td colspan=4>No tickets yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
Все это будет подставлено в базовый шаблон вместо
{% block content_block %}
{% endblock %}
. Содержимое представляет собой таблицу из четырек колонок, которая будет заполнена данными тикетов, а при их отсутствии (пустая) в таблице будет надпись No tickets yet.
Теперь напишем class-based view для этой функции в файле bugtracker/views.py, он очень прост:
from django.views.generic import ListView
from .models import Ticket
class BugListView(ListView):
model = Ticket
template_name = 'list.html'
Здесь определена модель для отображения в виде списка, и шаблон для отображения.
Осталось только, создать новый паттерн для URL "/bugs/" в файле bugtracker/urls.py: добавив
from .views import BugListView
и url(r'^$', BugListView.as_view(), name='index'),
(как мы помним, в файле project/urls.py — общего для всего проекта, мы определили, что паттерны для "/bugs"
находятся в bugtracker/urls.py
, соответственно регэксп паттерна для URL "/bugs/"
будет выглядеть именно так: r'^$'
.Заходим на http://127.0.0.1:8000/bugs/ и видим страшноватую таблицу со списком всех наших тикетов. Так как дизайн мы прикрутим потом, можем смело поставить статус «Closed» для этого этапа в админке.
Этап №2 — детали тикета
Выполняем:
git checkout -f part-2
Добавим возможность просмотра каждого тега по-отдельности, используя DetailView, действуем по старой схеме.
Создаем шаблон templates/detail.html:
{% extends 'base.html' %}
{% block title_block %}{{ object.title }}{% endblock %}
{% block content_block %}
<div>
<h1>{{ object.closed|yesno:"CLOSED, OPENED" }} - {{ object.title }}</h1>
<p>{{ object.user }} - {{ object.created|date }}</p>
<p>{{ object.text }}</p>
</div>
{% endblock %}
Создаем view в bugtracker/views.py:
from django.views.generic import DetailView
class BugDetailView(DetailView):
model = Ticket
template_name = 'detail.html'
и в bugtracker/urls.py:
from .views import BugDetailView
в urlpattens добавляем
url(r'^(?P<pk>[0-9]+)/$', BugDetailView.as_view(), name='detail'),
Кроме того, добавляем в шаблон templates/list.html ссылку на такой URL для каждого тикета, можно например сделать так:
<td><a href="{% url 'detail' ticket.pk %}">{{ ticket.title }}</a></td>
благодаря url 'detail' наши урлы будут генерироваться автоматически, и даже если структура урлов поменяется, то шаблон не «поломаются»
Проверяем, перейдя на http://127.0.0.1:8000/bugs/ и затем кликнув по любой из ссылок. Если все работает, отмечаем в админке, что тикет закрыт.
Этап №3 — регистрация пользователей
git checkout -f part-3
Правильнее, конечно, вынести работу с пользователями в отдельное приложение и воспользоваться, например, django-registration для удобной работы с активацией, сменой и восстановлением паролей и т.д., но так как мы обучаемся, то не будем использовать батарейки, дабы не умножать сущности сверх необходимого.
При регистрации пользователей мы будем использовать встроенные функции django по работе с формами, для этого создадим шаблон templates/register.html:
{% extends 'base.html' %}
{% block title_block %}Registration{% endblock %}
{% block content_block %}
{% if user.is_authenticated %}
<a href="{% url 'index' %}">Back to main page</a>
{% else %}
<form action="{% url 'register' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
{% endif %}
{% endblock %}
В случае, если юзер не залогинен, то мы отображаем форму, которые передается в шаблон из следующего view (bugtracker/views.py):
from django.views.generic import CreateView
from django.core.urlresolvers import reverse_lazy
from django.contrib.auth.forms import UserCreationForm
class RegisterView(CreateView):
form_class = UserCreationForm
template_name = 'register.html'
success_url = reverse_lazy('index')
success_url — это куда пользователя будет направлять при успешной регистрации, урл будет излечен из urlpatterns с помощью reverse_lazy('index'), то есть в нашем случае "/bugs/"
Дополним базовый шаблон ссылкой на форму регистрации, которая будет отображаться только если юзер не залогинен:
{% if user.is_authenticated %}
<div>Welcome, {{ user.username }}!</div>
{% else %}
<div><a href="{% url 'register' %}">Register</a></div>
{% endif %}
Не забываем добавить импорт RegisterView в bugtracker/urls.py и в urlpattern —
url(r'^register/$', RegisterView.as_view(), name='register'),
Теперь можно проверить работоспособность и перейти к следующему этапу.
Этап №4 — логин и логаут
git checkout -f part-4
Здесь все просто, мы будем использовать встроенные джанговские шорткаты. Для логаута шаблон, не понадобиться, так как отображать-то в этой функции и нечего, нас просто будет переносить на главную страницу. Поэтому создаем шаблон только для логина (templates/login.html):
{% extends 'base.html' %}
{% block title_block %}Login{% endblock %}
{% block content_block %}
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<p>
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</p>
{% endif %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<p>
{% for error in field.errors %}
{{ error }}
{% endfor %}
</p>
{% endif %}
</div>
{% endfor %}
<input type="submit" value="Submit" />
</form>
{% endblock %}
В bugtracker/urls.py пропишем следующее:
from django.core.urlresolvers import reverse_lazy
и
url(r'^login/$', 'django.contrib.auth.views.login',
{"template_name" : "login.html"}, name="login"),
url(r'^logout/$', 'django.contrib.auth.views.logout',
{"next_page" : reverse_lazy('login')}, name="logout"),
А templates/base.html переделаем так:
{% if user.is_authenticated %}
<div>Welcome, {{ user.username }}
| <a href="{% url 'logout' %}">Logout</a></div>
{% else %}
<div><a href="{% url 'register' %}">Register
| <a href="{% url 'login' %}">Login</a></div>
{% endif %}
Что бы после логина нас редиректило на главную страницу в settings.py пропишем
from django.core.urlresolvers import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('index')
Поиграем вдоволь, создавая новых пользователей, логинясь и разлогиниваясь под ними.
Этап №5 — добавление нового тикета
git checkout -f part-5
Шаблон templates/add.html очень похож на тот, что мы создавали для RegisterView, только наоборот — функционал доступен авторизованному пользователю:
{% extends 'base.html' %}
{% block title_block %}Add ticket{% endblock %}
{% block content_block %}
{% if user.is_authenticated %}
<a href="{% url 'index' %}">Back to main page</a>
<form action="{% url 'add' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
{% else %}
<p>You should be logged in to add tickets!</p>
{% endif %}
{% endblock %}
Добавляем куда-нибудь в базовый шаблон ссылочку на add:
<div><a href="{% url 'add' %}">Add ticket</a></div>
View будет таким,
class BugCreateView(CreateView):
model = Ticket
template_name = 'add.html'
fields = ['title', 'text']
success_url = reverse_lazy('index')
def form_valid(self, form):
form.instance.user = self.request.user
return super(BugCreateView, self).form_valid(form)
То есть пользователю будут видны только поля title и text, а благодаря переопределению метода form_valid в поле user будет подставлен индекс текущего пользователя.
Прописываем паттерн для урла 'add/' в bugtracker/urls.py, не забыв импортировать BugCreateView:
url(r'^add/$', BugCreateView.as_view(), name='add'),
Попробуем добавить новых тикетов, и переходим к следующему этапу.
Этап №6 — сделать нарядно
git checkout -f part-6
Все это более-менее удовлетворительно работает, но выглядит страшновато. На мой взгляд, самый простой и удобный способ улучшить наш интерфейс, доступный даже неспециалисту в верстке (как я) — это использовать twitter bootstrap. Для этого скачаем соответствующую библиотеку и распакуем её в папку static. После чего мы приступим к верстке шаблонов. У bootstrap куча примеров, поэтому особых сложностей возникнуть не должно. Поскольку, приводить здесь простыни получившегося HTML не имеет смысла, а нюансы верстки под bootstrap выходят за рамки этой статьи (и моих познаний), просто посмотрите исходники в репозитории, или выполните
git checkout -f final
, что бы получить окончательный вариант кода.Стоит заметить, что третья версия bootstrap требует добавлять class=«form-control» к инпутам в формах.Известный мне самый простой метод, сделать это в шаблонах django (не используя дополнительных батареек) — это кастомный фильтр, который описан здесь. Буду благодарен, если более искушенные читатели моей статьи поделятся своим опытом в комментариях.
Итог
Буквально за пару часов, а то и меньше, мы с вами создали вполне функциональное приложение, которое при дальнейшем улучшении, настройке и развитии, может хорошо послужить, в каких то других проектах, а может быть и как standalone-сервис. Вот мои несколько вполне реализуемых идей для его улучшения:
- HTTP API (сразу появляется возможность взаимодействия практически со всем чем угодно — программы, консольные скрипты, другие веб-сервисы и т.д.
- Побольше статусов для тикетов
- Пагинация в шаблоне вывода списка
- Возможность поручить исполнение тикета другому юзеру и контролировать статус
- Загрузка файлов и изображений
- Легкая настройка и кастомизация дополнительных полей для тикетов
- Система плагинов
Жду ваших замечаний, идей и предложений в комментариях. Спасибо!
UPD:Пока я готовил статью, вышел релиз django 1.7, в которую внедрена поддержка миграций, что раньше осуществлялось с помощью south. Команда syncdb теперь deprecated, поэтому, после изменений в моделях делаем:
./manage.py makemigrations <имя_приложения>
./manage.py migrate
Для просмотра SQL-запроса соответствующего миграции, выполняем
./manage.py sqlmigrate <имя_приложения> <номер_миграции>