dhtmlxGantt с Python

В этом руководстве описывается создание диаграммы Gantt на базе Python с использованием фреймворка Django 4 и RESTful API на серверной стороне.

Если вы работаете с другими платформами, доступны руководства по серверной интеграции с:

Полный исходный код доступен на GitHub.

Необходимые условия

Если Django еще не установлен, вот инструкции по установке:

Шаг 1. Инициализация проекта

Откройте папку вашего проекта и создайте новый проект Django с помощью команды:

django-admin startproject gantt_rest_python

Далее либо переместите содержимое папки gantt_rest_python в текущую директорию, либо перейдите в нее:

cd gantt_rest_python

Чтобы убедиться, что базовая настройка работает, выполните:

python manage.py runserver

Откройте http://localhost:8000 в браузере — должна появиться стандартная приветственная страница Django:

start_page

Шаг 2. Добавление Gantt на страницу

Создайте новое приложение для компонента Gantt:

python manage.py startapp gantt

Установите пакеты REST framework:

pip install djangorestframework
pip install djangorestframework-jsonapi

В папке gantt создайте директории static и templates.

Скопируйте папку codebase из пакета Gantt в папку static и переименуйте ее в gantt для удобства.

Затем создайте файл index.html в templates/gantt со следующим содержимым:

gantt/templates/gantt/index.html

<html>
    <head>
        {% load static %}
        <script src="{% static "gantt/dhtmlxgantt.js" %}" type="text/javascript">
        </script>
        <link rel="stylesheet" href="{% static "gantt/dhtmlxgantt.css" %}" />
    </head>
    <body>
        <div id="gantt_here" style='width:100%; height:500px;'>
        </div>
        <script>
        gantt.config.date_format = "%Y-%m-%d %H:%i";
 
        gantt.config.open_tree_initially = true;
        gantt.init("gantt_here");
        </script>
    </body>
</html>

В результате структура папок должна выглядеть так:

folder_structure

Откройте views.py в папке gantt и добавьте:

gantt/views.py

from django.shortcuts import render
 
def index(request):
    return render(request, 'gantt/index.html')

Далее настройте маршрутизацию, создав urls.py в папке gantt:

gantt/urls.py

from django.urls import include, re_path
from . import views
from rest_framework.urlpatterns import format_suffix_patterns
 
urlpatterns = [
    re_path(r'^$', views.index, name='index'),
]
urlpatterns = format_suffix_patterns(urlpatterns)

В файле urls.py папки gantt_rest_python обновите urlpatterns, чтобы добавить маршруты приложения gantt:

gantt_rest_python/urls.py

from django.urls import include, re_path
from django.contrib import admin
 
urlpatterns = [
    re_path(r'', include('gantt.urls')),
]

Чтобы Django мог находить ваши шаблоны и статические файлы, откройте settings.py в gantt_rest_python и добавьте в начало файла:

gantt_rest_python/settings.py

import os

Найдите настройку TEMPLATES и замените пустой массив DIRS:

'DIRS': [],

на:

'DIRS': [os.path.join(BASE_DIR, 'gantt/templates')],

В конце файла добавьте строку, указывающую расположение статических файлов:

STATICFILES_DIRS = [os.path.join(BASE_DIR, "gantt/static")]


Снова запустите сервер:

python manage.py runserver

Если все настроено правильно, на странице появится пустая диаграмма Gantt:

init_gantt

Шаг 3. Загрузка данных

В gantt_rest_python/settings.py добавьте 'rest_framework' и 'gantt.apps.GanttConfig' в список INSTALLED_APPS, а также настройте параметры REST framework:

gantt_rest_python/settings.py

INSTALLED_APPS = [
    'gantt.apps.GanttConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
]
 
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),
    'PAGE_SIZE': 10
}

Поскольку DHTMLX Gantt использует абсолютные даты без привязки к часовому поясу, отключите поддержку временных зон:

USE_TZ = False

Определите модели Task и Link в gantt/models.py:

gantt/models.py

from django.db import models
 
class Task(models.Model):
    id = models.AutoField(primary_key=True, editable=False)
    text = models.CharField(blank=True, max_length=100)
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()
    duration = models.IntegerField()
    progress = models.FloatField()
    parent = models.CharField(max_length=100)
 
class Link(models.Model):
    id = models.AutoField(primary_key=True, editable=False)
    source = models.CharField(max_length=100)
    target = models.CharField(max_length=100)
    type = models.CharField(max_length=100)
    lag = models.IntegerField(blank=True, default=0)

Создайте миграции для новых моделей:

python manage.py makemigrations gantt

Примените миграции для обновления схемы базы данных:

python manage.py migrate

Чтобы добавить начальные данные, откройте Django shell:

python manage.py shell

Внутри shell проверьте текущие данные:

from gantt.models import Task
Task.objects.all()
 
from gantt.models import Link
Link.objects.all()

Так как база пуста, добавьте задачи и связи следующим образом:

t1=Task(id="10",text="Project #1",start_date="2025-04-01 00:00",
    end_date="2025-04-03 00:00",duration=2,progress=0.5,parent="0")
t1.save()
t1=Task(id="1", text="Task #1",start_date="2025-04-01 00:00",
    end_date="2025-04-02 00:00", duration=1, progress=0.45, parent="10")
t1.save()
t1=Task(id="2", text="Task #2",start_date="2025-04-02 00:00",
    end_date="2025-04-03 00:00", duration=1, progress=0.15, parent="10")
t1.save()
t1=Task(id="20", text="Project #2",start_date="2025-04-03 00:00",
    end_date="2025-04-05 00:00", duration=2, progress=0.35, parent="0")
t1.save()
t1=Task(id="3", text="Task #3",start_date="2025-04-03 00:00",
    end_date="2025-04-04 00:00", duration=1, progress=0.85, parent="20")
t1.save()
t1=Task(id="4", text="Task #4",start_date="2025-04-04 00:00",
    end_date="2025-04-06 00:00", duration=1, progress=0.65, parent="20")
t1.save()
 
l1=Link(id="1",source="1",target="2",type="0",lag=0)
l1.save()
l1=Link(id="2",source="2",target="3",type="0",lag=0)
l1.save()
l1=Link(id="3",source="3",target="4",type="0",lag=0)
l1.save()

Теперь выполнение Task.objects.all() и Link.objects.all() должно возвращать добавленные элементы.

Для сериализации создайте serializers.py в папке gantt:

gantt/serializers.py

from .models import Task
from .models import Link
from rest_framework import serializers
 
class TaskSerializer(serializers.ModelSerializer):
    start_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
    end_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
 
    class Meta:
        model = Task
        fields = ('id','text','start_date','end_date','duration','progress','parent')
 
 
class LinkSerializer(serializers.ModelSerializer):
 
    class Meta:
        model = Link
        fields = ('id', 'source', 'target', 'type', 'lag')

Обновите gantt/views.py, добавив представление для отдачи данных Gantt:

gantt/views.py

from django.shortcuts import render
from .models import Task
from .models import Link
from gantt.serializers import TaskSerializer
from gantt.serializers import LinkSerializer
 
from rest_framework.decorators import api_view
from rest_framework.response import Response
 
def index(request):
    return render(request, 'gantt/index.html')
 
 
@api_view(['GET'])
def data_list(request, offset):
    if request.method == 'GET':
        tasks = Task.objects.all()
        links = Link.objects.all()
        taskData = TaskSerializer(tasks, many=True)
        linkData = LinkSerializer(links, many=True)
        return Response({
            "tasks": taskData.data,
            "links": linkData.data
        })

Добавьте маршруты для загрузки данных в gantt/urls.py:

gantt/urls.py

from django.urls import include, re_path
from . import views
from rest_framework.urlpatterns import format_suffix_patterns
 
urlpatterns = [
    re_path(r'^$', views.index, name='index'),
    re_path(r'^data/(.*)$', views.data_list),
]
urlpatterns = format_suffix_patterns(urlpatterns)

Наконец, обновите файл gantt/templates/gantt/index.html для загрузки данных с сервера, добавив:

gantt/templates/gantt/index.html

gantt.load("/data/", "json");

Теперь при запуске сервера диаграмма Gantt будет заполнена задачами и связями:

gantt

Шаг 4. Сохранение изменений

Чтобы обеспечить сохранение изменений, добавьте поддержку методов POST, PUT и DELETE в gantt/views.py:

gantt/views.py

from django.shortcuts import render
from .models import Task
from .models import Link
from gantt.serializers import TaskSerializer
from gantt.serializers import LinkSerializer
 
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.http import JsonResponse
 
 
def index(request):
    return render(request, 'gantt/index.html')
 
@api_view(['GET'])
def data_list(request, offset):
    if request.method == 'GET':
        tasks = Task.objects.all()
        links = Link.objects.all()
        taskData = TaskSerializer(tasks, many=True)
        linkData = LinkSerializer(links, many=True)
        return Response({
            "tasks": taskData.data,
            "links": linkData.data
        })
 
 
@api_view(['POST'])
def task_add(request):
    if request.method == 'POST':
        serializer = TaskSerializer(data=request.data)
        print(serializer)
 
        if serializer.is_valid():
            task = serializer.save()
            return JsonResponse({'action':'inserted', 'tid': task.id})
        return JsonResponse({'action':'error'})
 
@api_view(['PUT', 'DELETE'])
def task_update(request, pk):
    try:
        task = Task.objects.get(pk=pk)
    except Task.DoesNotExist:
        return JsonResponse({'action':'error2'})
 
    if request.method == 'PUT':
        serializer = TaskSerializer(task, data=request.data)
        print(serializer)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse({'action':'updated'})
        return JsonResponse({'action':'error'})
 
    if request.method == 'DELETE':
        task.delete()
        return JsonResponse({'action':'deleted'})
 
 
@api_view(['POST'])
def link_add(request):
    if request.method == 'POST':
        serializer = LinkSerializer(data=request.data)
        print(serializer)
 
        if serializer.is_valid():
            link = serializer.save()
            return JsonResponse({'action':'inserted', 'tid': link.id})
        return JsonResponse({'action':'error'})
 
@api_view(['PUT', 'DELETE'])
def link_update(request, pk):
    try:
        link = Link.objects.get(pk=pk)
    except Link.DoesNotExist:
        return JsonResponse({'action':'error'})
 
    if request.method == 'PUT':
        serializer = LinkSerializer(link, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse({'action':'updated'})
        return JsonResponse({'action':'error'})
 
    if request.method == 'DELETE':
        link.delete()
        return JsonResponse({'action':'deleted'})

Добавьте соответствующие маршруты в gantt/urls.py:

gantt/urls.py

from django.urls import include, re_path
from . import views
from rest_framework.urlpatterns import format_suffix_patterns
 
urlpatterns = [
    re_path(r'^$', views.index, name='index'),
    re_path(r'^data/task/(?P<pk>[0-9]+)$', views.task_update),
    re_path(r'^data/task', views.task_add),
    re_path(r'^data/link/(?P<pk>[0-9]+)$', views.link_update),
    re_path(r'^data/link', views.link_add),
    re_path(r'^data/(.*)$', views.data_list),
]
urlpatterns = format_suffix_patterns(urlpatterns)

Чтобы отправлять изменения на сервер, включите Data Processor в gantt/templates/gantt/index.html:

gantt/templates/gantt/index.html

    var dp = new gantt.dataProcessor("/data/");
    dp.init(gantt);
    dp.setTransactionMode("REST");

Теперь при добавлении, изменении или удалении задач и связей изменения будут сохраняться. После перезагрузки страницы данные сохранятся:

saving_changes

Хранение порядка задач

Поскольку DHTMLX Gantt работает на стороне клиента, порядок задач не сохраняется автоматически. Порядок зависит от последовательности в JSON-данных. Один из способов — сортировать задачи на сервере перед отправкой в Gantt. Подробнее здесь.

Другой способ — использовать родительскую задачу и позицию в ветке. ID родителя хранится в поле parent, а позиция в ветке соответствует временному свойству $local_index. Хотя изменение $local_index не влияет на отображение, его можно использовать для отслеживания порядка и сохранения в отдельном свойстве. После загрузки задачи можно отсортировать по этому свойству.

Сначала добавьте поле sort_order в модель Task в gantt/models.py:

gantt/models.py

class Task(models.Model):
    id = models.AutoField(primary_key=True, editable=False)
    text = models.CharField(blank=True, max_length=100)
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()
    duration = models.IntegerField()
    progress = models.FloatField()
    parent = models.CharField(max_length=100)
    sort_order = models.IntegerField(default=0)

Добавьте sort_order в сериализатор в gantt/serializers.py:

gantt/serializers.py

class TaskSerializer(serializers.ModelSerializer):
    start_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
    end_date = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
 
    class Meta:
        model = Task
        fields = ('id', 'text', 'start_date', 'end_date', 'duration', 'progress', 
            'parent', 'sort_order')

Примените изменения к базе данных:

python manage.py makemigrations gantt
python manage.py migrate

Далее обновите index.html, чтобы изменять sort_order при добавлении или изменении порядка задач:

gantt.attachEvent("onRowDragEnd", function (id, target) {
    gantt.batchUpdate(function () {
        gantt.eachTask(function (task) {
            task.sort_order = task.$local_index + 1;
            gantt.updateTask(task.id)
        })
    })
});
gantt.attachEvent("onBeforeTaskAdd", function (id, task) {
    task.sort_order = task.$local_index + 1;
    return true;
});

Включите вертикальное изменение порядка, добавив это перед gantt.init:

gantt.config.order_branch = "marker";
gantt.config.order_branch_free = true;

Чтобы отсортировать задачи после загрузки данных, добавьте это перед gantt.init или gantt.load:

gantt.attachEvent("onLoadEnd", function () {
    gantt.batchUpdate(function () {
        gantt.sort("sort_order", false)
    })
});

В результате соответствующий фрагмент index.html будет выглядеть так:

gantt/templates/gantt/index.html

gantt.config.date_format = "%Y-%m-%d %H:%i";
 
gantt.config.order_branch = "marker";
gantt.config.order_branch_free = true;
 
gantt.config.open_tree_initially = true;
 
gantt.attachEvent("onLoadEnd", function() {
    gantt.batchUpdate(function() {
        gantt.sort("sort_order", false)
    })
});
 
gantt.attachEvent("onRowDragEnd", function(id, target) {
    //обновление порядка задач
    gantt.batchUpdate(function() {
        gantt.eachTask(function(task) {
            task.sort_order = task.$local_index + 1;
            gantt.updateTask(task.id)
        })
    })
});
gantt.attachEvent("onBeforeTaskAdd", function(id, task) {
    task.sort_order = task.$local_index + 1;
    return true;
});
 
gantt.init("gantt_here");
gantt.load("/data/", "json");
 
var dp = new gantt.dataProcessor("/data/");
dp.init(gantt);
dp.setTransactionMode("REST");

Теперь при вертикальном изменении порядка задач новая последовательность будет сохраняться:

sort_order

Безопасность приложения

DHTMLX Gantt не содержит встроенной защиты от таких угроз, как SQL-инъекции, XSS или CSRF. Защита приложения лежит на ответственности разработчика на серверной стороне. Подробнее см. в статье по безопасности.

Устранение неполадок

Если задачи и связи не отображаются после завершения интеграции, ознакомьтесь с руководством по устранению неполадок в Устранение проблем интеграции с backend. Там приведены советы по диагностике и решению распространенных проблем.

Что дальше

На данном этапе у вас есть работающее приложение с диаграммой Gantt. Полный исходный код доступен на GitHub для клонирования или скачивания для использования в ваших проектах.

Рекомендуем также ознакомиться с руководствами по различным возможностям Gantt или уроками по интеграции Gantt с другими серверными фреймворками.

К началу