Как стать автором
Обновить
108.53
Amvera
Amvera — облако для хостинга IT-приложений

Разработка Task Manager с нуля до полнофункционального продукта

Уровень сложностиСложный
Время на прочтение13 мин
Количество просмотров531

Если Вы когда-нибудь задумывался о том, как создать своё собственное веб-приложение для управления задачами, надеюсь, эта статья вам поможет.

Мы пройдём весь путь — от установки необходимых инструментов и настройки окружения до разработки интерфейса и деплоя приложения на сервере. Каждый этап будет сопровождаться объяснениями и примерами кода, которые вы сможете найти в репозитории на GitHub.

Перед началом разработки необходимо убедиться, что на вашем компьютере установлены Python 3 и GitPython будет использоваться для создания серверной части приложения, а Git — для управления версиями и размещения кода на GitHub.

Установка Python и виртуального окружения

Установка Python и виртуального окружения

Первым делом убедимся, что установлен Python 3. Если нет, скачайте его с официального сайта.

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

  1. Создайте папку для проекта:

mkdir task_manager 
cd task_manager
  1. Создайте виртуальное окружение:

python3 -m venv venv
  1. Активируйте виртуальное окружение:

  • На macOS/Linux:

source venv/bin/activate
  • На Windows:

 venv\Scripts\activate

Теперь в начале командной строки должены увидеть (venv), что означает, что виртуальное окружение активировано.

Установка необходимых библиотек

С активированным виртуальным окружением установим необходимые пакеты:

pip install flask flask_sqlalchemy
  • Flask: наш основной веб-фреймворк.

  • Flask_SQLAlchemy: расширение для работы с базами данных.

Отлично! Теперь мы готовы перейти к созданию структуры проекта.

Организуем файлы и папки нашего приложения:

task_manager/
├── app.py
├── models.py
├── extensions.py
├── templates/
│   ├── index.html
│   └── update.html
└── static/
    ├── style.css
    └── timer.js

Что означает каждая часть:

  • app.py: основной файл нашего приложения Flask.

  • models.py: файл, где мы опишем модели базы данных.

  • extensions.py: здесь мы инициализируем расширения для Flask.

  • templates/: папка для HTML-шаблонов.

  • static/: папка для статических файлов (CSS, JavaScript, изображения).

Создайте эти файлы и папки в своем проекте.

Инициализация базы данных

Начнем с файла extensions.py, где мы инициализируем базу данных с помощью SQLAlchemy:

from flask_sqlalchemy import SQLAlchemy 

db = SQLAlchemy()

Определение модели данных

В файле models.py опишем модель Task, которая представляет задачу в нашем приложении:

from extensions import db
from datetime import datetime

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.String(200), nullable=False)
    completed = db.Column(db.Boolean, default=False)
    deadline = db.Column(db.DateTime, nullable=True)

    def __repr__(self):
        return f''

Настройка основного приложения

Теперь перейдем к файлу app.py, где мы настроим Flask-приложение и определим маршруты:

from flask import Flask, render_template, request, redirect, url_for
from extensions import db
from models import Task
from datetime import datetime
import os

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False  
db.init_app(app) 

@app.route('/')
@app.route('/<filter>')
def index(filter='all'):
    if filter == 'completed':
        tasks = Task.query.filter_by(completed=True).all()
    elif filter == 'pending':
        tasks = Task.query.filter_by(completed=False).all()
    else:
        tasks = Task.query.all()
    return render_template('index.html', tasks=tasks)

@app.route('/add', methods=['POST'])
def add_task():
    task_content = request.form['content']
    date_str = request.form.get('date')
    time_str = request.form.get('time')
    deadline = None
    if date_str and time_str:
        deadline_str = f"{date_str} {time_str}"
        deadline = datetime.strptime(deadline_str, '%d.%m.%Y %H:%M')
    new_task = Task(content=task_content, deadline=deadline)
    db.session.add(new_task)
    db.session.commit()
    return redirect(url_for('index'))

@app.route('/delete/<int:id>')
def delete_task(id):
    task = Task.query.get_or_404(id)
    db.session.delete(task)
    db.session.commit()
    return redirect(url_for('index'))

@app.route('/complete/<int:id>')
def complete_task(id):
    task = Task.query.get_or_404(id)
    task.completed = not task.completed
    db.session.commit()
    return redirect(url_for('index'))

@app.route('/update/<int:id>', methods=['GET', 'POST'])
def update_task(id):
    task = Task.query.get_or_404(id)
    if request.method == 'POST':
        task.content = request.form['content']
        date_str = request.form.get('date')
        time_str = request.form.get('time')
        if date_str and time_str:
            deadline_str = f"{date_str} {time_str}"
            task.deadline = datetime.strptime(deadline_str, '%d.%m.%Y %H:%M')
        else:
            task.deadline = None
        db.session.commit()
        return redirect(url_for('index'))
    return render_template('update.html', task=task)

if __name__ == "__main__":
    with app.app_context():
        if not os.path.exists('tasks.db'):
            db.create_all()
    app.run(debug=True)

Главная страница и фильтры (/ и /<filter>):

@app.route('/')
@app.route('/<filter>')
def index(filter='all'):
    if filter == 'completed':
        tasks = Task.query.filter_by(completed=True).all()
    elif filter == 'pending':
        tasks = Task.query.filter_by(completed=False).all()
    else:
        tasks = Task.query.all()
    return render_template('index.html', tasks=tasks)

Добавление новой задачи (/add):

@app.route('/add', methods=['POST'])
def add_task():
    task_content = request.form['content']
    date_str = request.form.get('date')
    time_str = request.form.get('time')
    deadline = None
    if date_str and time_str:
        deadline_str = f"{date_str} {time_str}"
        deadline = datetime.strptime(deadline_str, '%d.%m.%Y %H:%M')
    new_task = Task(content=task_content, deadline=deadline)
    db.session.add(new_task)
    db.session.commit()
    return redirect(url_for('index'))

Удаление задачи (/delete/<int:id>):

@app.route('/delete/<int:id>')
def delete_task(id):
    task = Task.query.get_or_404(id)
    db.session.delete(task)
    db.session.commit()
    return redirect(url_for('index'))

Переключение статуса выполнения задачи (/complete/<int:id>):

@app.route('/complete/<int:id>')
def complete_task(id):
    task = Task.query.get_or_404(id)
    task.completed = not task.completed
    db.session.commit()
    return redirect(url_for('index'))

Обновление задачи (/update/<int:id>):

@app.route('/update/<int:id>', methods=['GET', 'POST'])
def update_task(id):
    task = Task.query.get_or_404(id)
    if request.method == 'POST':
        task.content = request.form['content']
        date_str = request.form.get('date')
        time_str = request.form.get('time')
        if date_str and time_str:
            deadline_str = f"{date_str} {time_str}"
            task.deadline = datetime.strptime(deadline_str, '%d.%m.%Y %H:%M')
        else:
            task.deadline = None
        db.session.commit()
        return redirect(url_for('index'))
    return render_template('update.html', task=task)

Запуск приложения:

if __name__ == "__main__":
    with app.app_context():
        if not os.path.exists('tasks.db'):
            db.create_all()
    app.run(debug=True)

Теперь создадим шаблоны для нашего приложения, чтобы отображать информацию пользователю.

Главная страница (index.html)

В файле templates/index.html добавим следующий код:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Task Manager</title>
    <!-- Подключаем стили и библиотеки -->
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <!-- Библиотеки для анимаций -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css">
</head>
<body>
    <div class="container">
        <!-- Левая боковая панель -->
        <aside class="sidebar-left">
            <h2>О приложении</h2>
            <p>Управляйте своими задачами эффективно с помощью дедлайнов и напоминаний.</p>
        </aside>
        
        <!-- Основной контент -->
        <main class="content">
            <div class="main-content" data-aos="fade-up">
                <h1>Task Manager</h1>

                <!-- Форма добавления задачи -->
                <form action="/add" method="POST" class="task-form" data-aos="fade-up">
                    <input type="text" name="content" placeholder="Добавить новую задачу" required>
                    <input type="text" name="date" placeholder="Дата дедлайна (дд.мм.гггг)" pattern="\d{2}\.\d{2}\.\d{4}">
                    <input type="text" name="time" placeholder="Время дедлайна (чч:мм)" pattern="\d{2}:\d{2}">
                    <button type="submit" class="add-btn">Добавить задачу</button>
                </form>

                <!-- Фильтры задач -->
                <div class="filters" data-aos="fade-up">
                    <a href="{{ url_for('index', filter='all') }}">Все</a>
                    <a href="{{ url_for('index', filter='completed') }}">Выполненные</a>
                    <a href="{{ url_for('index', filter='pending') }}">Невыполненные</a>
                </div>

                <!-- Список задач -->
                <ul class="task-list">
                    {% for task in tasks %}
                    <li class="{% if task.completed %}completed{% endif %}" data-aos="fade-up">
                        {% if task.completed %}
                        <span class="checkmark">&#10003;</span>
                        {% endif %}
                        <span class="task-content">{{ task.content }}</span>
                        {% if task.deadline and not task.completed %}
                        <span class="deadline">
                            Дедлайн: {{ task.deadline.strftime('%d.%m.%Y %H:%M') }}
                            <span class="timer" data-deadline="{{ task.deadline.isoformat() }}"></span>
                        </span>
                        {% endif %}
                        <div class="actions">
                            <a href="/complete/{{ task.id }}">{{ "Отменить" if task.completed else "Выполнить" }}</a>
                            <a href="/update/{{ task.id }}">Редактировать</a>
                            <a href="/delete/{{ task.id }}">Удалить</a>
                        </div>
                    </li>
                    {% endfor %}
                </ul>
            </div>
        </main>
        
        <!-- Правая боковая панель -->
        <aside class="sidebar-right">
            <h2>Быстрые ссылки</h2>
            <ul>
                <li><a href="#">Настройки</a></li>
                <li><a href="#">Помощь</a></li>
                <li><a href="#">Контакты</a></li>
            </ul>
        </aside>
    </div>

    <!-- Подключаем скрипты -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js"></script>
    <!-- GSAP для анимаций -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.2/gsap.min.js"></script>
    <!-- Mo.js для эффектов -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mo-js/0.288.0/mo.min.js"></script>
    <!-- Наш скрипт -->
    <script src="{{ url_for('static', filename='timer.js') }}"></script>
    <script>
        // Инициализация AOS
        AOS.init({
            duration: 1000, // Продолжительность анимаций
        });
    </script>
</body>
</html>

Страница обновления задачи (update.html)

Создадим файл templates/update.html для редактирования задачи:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Обновить задачу</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <h1>Обновить задачу</h1>

        <!-- Форма обновления задачи -->
        <form action="{{ url_for('update_task', id=task.id) }}" method="POST" class="task-form">
            <input type="text" name="content" value="{{ task.content }}" required>
            <input type="text" name="date" value="{{ task.deadline.strftime('%d.%m.%Y') if task.deadline else '' }}" placeholder="Дата дедлайна (дд.мм.гггг)">
            <input type="text" name="time" value="{{ task.deadline.strftime('%H:%M') if task.deadline else '' }}" placeholder="Время дедлайна (чч:мм)">
            <button type="submit" class="add-btn">Обновить задачу</button>
        </form>

        <a href="{{ url_for('index') }}" class="back-link">Вернуться к списку задач</a>
    </div>
</body>
</html>

В папке static создаем файл style.css и добавляем стили для нашего приложения:

/* Общие стили */
body {
    font-family: 'Arial', sans-serif;
    background-color: #f4f4f4;
    margin: 0;
    padding: 0;
    display: flex;
    min-height: 100vh;
}

.container {
    display: flex;
    flex: 1;
    margin: 20px auto;
    max-width: 1200px;
    background: #fff;
    padding: 20px;
    border-radius: 8px;
}

/* Боковые панели */
.sidebar-left, .sidebar-right {
    width: 200px;
    background: #f0f0f0;
    padding: 15px;
    border-radius: 8px;
}

.sidebar-left h2, .sidebar-right h2 {
    font-size: 1.2em;
    margin-bottom: 10px;
}

.sidebar-left p, .sidebar-right ul {
    font-size: 0.9em;
}

.sidebar-right ul {
    list-style: none;
    padding: 0;
}

.sidebar-right ul li {
    margin-bottom: 10px;
}

.sidebar-right ul li a {
    color: #007bff;
    text-decoration: none;
}

.sidebar-right ul li a:hover {
    text-decoration: underline;
}

/* Основной контент */
.main-content {
    flex: 1;
    margin: 0 20px;
}

/* Форма задач */
.task-form {
    display: flex;
    flex-direction: column;
    margin-bottom: 20px;
}

.task-form input {
    margin-bottom: 10px;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.task-form .add-btn {
    padding: 10px;
    background-color: #5cb85c;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.task-form .add-btn:hover {
    background-color: #4cae4c;
}

/* Фильтры */
.filters {
    margin-bottom: 20px;
}

.filters a {
    margin: 0 10px;
    text-decoration: none;
    color: #5cb85c;
    font-size: 16px;
}

.filters a:hover {
    color: #3e8e41;
}

/* Список задач */
.task-list {
    list-style: none;
    padding: 0;
}

.task-list li {
    background: #f9f9f9;
    padding: 15px;
    margin-bottom: 10px;
    border-radius: 4px;
    display: flex;
    align-items: center;
}

.task-list li.completed {
    background: #e6ffe6;
}

.task-list li.completed .task-content {
    text-decoration: line-through;
    color: #888;
}

.checkmark {
    color: #5cb85c;
    font-size: 1.5em;
    margin-right: 10px;
}

.task-content {
    flex: 1;
}

.deadline {
    font-size: 0.9em;
    color: #777;
    margin-left: 10px;
}

.timer {
    font-size: 0.9em;
    color: #ff5733;
    margin-left: 10px;
}

.timer.expired {
    color: #dc3545;
}

/* Действия с задачами */
.actions {
    margin-left: auto;
    display: flex;
    align-items: center;
}

.actions a {
    margin-left: 10px;
    text-decoration: none;
    color: #007bff;
    font-size: 16px;
}

.actions a:hover {
    color: #0056b3;
}

/* Ссылка возврата */
.back-link {
    display: block;
    margin-top: 20px;
    text-decoration: none;
    color: #007bff;
}

.back-link:hover {
    color: #0056b3;
}

В файле static/timer.js добавим код для обновления таймеров и анимаций:

document.addEventListener('DOMContentLoaded', () => {
    const timers = document.querySelectorAll('.timer');
    const completeLinks = document.querySelectorAll('.actions a[href*="complete"]');

    // Функция для анимации при выполнении задачи
    function createBurstAnimation(x, y) {
        new mojs.Burst({
            radius: { 0: 100 },
            angle: 45,
            count: 10,
            children: {
                shape: 'circle',
                radius: 10,
                fill: ['#FF5722', '#FFC107', '#8BC34A'],
                duration: 2000
            },
            x: x,
            y: y,
            opacity: { 1: 0 },
        }).play();
    }

    // Обновление таймеров
    function updateTimers() {
        timers.forEach(timer => {
            const deadline = new Date(timer.getAttribute('data-deadline'));
            const now = new Date();
            const remainingTime = deadline - now;

            if (remainingTime <= 0) {
                timer.textContent = 'Дедлайн прошёл';
                timer.classList.add('expired');
            } else {
                const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
                const hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
                const minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
                const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);

                timer.textContent = `${days}д ${hours}ч ${minutes}м ${seconds}с`;
            }
        });
    }

    // Обработка клика по кнопке "Выполнить"
    function handleTaskComplete(event) {
        event.preventDefault();
        const link = event.currentTarget;
        const taskItem = link.closest('li');
        const { left, top } = taskItem.getBoundingClientRect();

        createBurstAnimation(left + window.scrollX + taskItem.offsetWidth / 2, top + window.scrollY + taskItem.offsetHeight / 2);
        
        setTimeout(() => {
            window.location.href = link.href;
        }, 1000);
    }

    completeLinks.forEach(link => {
        link.addEventListener('click', handleTaskComplete);
    });

    updateTimers();
    setInterval(updateTimers, 1000);
});

Запуск приложения

Теперь, когда всё готово, давай протестируем наше приложение.

В терминале, находясь в корневой директории проекта, введем:

python app.py

Получим сообщение о том, что сервер запущен на http://127.0.0.1:5000.

Открываем браузер и переходим по адресу http://127.0.0.1:5000. Видим интерфейс нашего Task Manager, где сможешь добавлять, редактировать, выполнять и удалять задачи.

Тестирование с помощью Talend API Tester

Чтобы убедиться, что всё работает правильно, можно использовать инструмент Talend API Tester — расширение для браузера Google Chrome, которое позволяет тестировать API и HTTP-запросы.

Как его установить:

  • Открываем браузер Google Chrome.

  • Перейдем в Интернет-магазин Chrome и найдем Talend API Tester.

  • Нажмаем кнопку "Установить" и следуем инструкциям для добавления расширения в браузер.

После установки мы получим такую страницу:

Тестирование добавления новой задачи

  • Метод: POST

  • URL: http://127.0.0.1:5000/add

  • Параметры формы (Form Data):

    • content"Тестовая задача"

    • date"25.12.2023"

    • time"12:00"

  1. Откройте Talend API Tester в браузере.

  2. Выберите метод POST.

  3. Введите URL http://127.0.0.1:5000/add.

  4. Перейдите на вкладку Body и выберите Form.

  5. Добавьте параметры формы:

    • Ключ: content, Значение: Тестовая задача

    • Ключ: date, Значение: 25.12.2023

    • Ключ: time, Значение: 12:00

  6. Нажмите кнопку "Send" для отправки запроса.

  7. Проверьте, что сервер отвечает статусом 302 Found, что означает перенаправление.

  8. Откройте браузер и перейдите по адресу http://127.0.0.1:5000/add, чтобы убедиться, что задача добавлена

Откройте браузер и перейдите по адресу http://127.0.0.1:5000/ , чтобы убедиться, что задача добавлена.

Тестирование получения списка задач

  • Метод: GET

  • URL: http://127.0.0.1:5000/

  1. Выберите метод GET.

  2. Введите URL http://127.0.0.1:5000/.

  3. Нажмите "Send".

  4. Проверьте ответ сервера. Вы должны увидеть HTML-код страницы с добавленной задачей в теле ответа.

Деплой на сервера Amvera

После успешного тестирования нашего Task Manager на локальном сервере, давайте развернём его на удалённом сервере, чтобы он был доступен 24/7 и не зависел от твоего компьютера.

Сервис Amvera мы выбрали, так как он даст нам

  • Бесплатное доменное имя

  • Возможность доставлять обновления тремя командами через git push (что нмного проще настройки классической VPS)

  • Это блог нашей компании, странно было бы выбирать конкурентов)

Регистрация в сервисе Amvera

  1. Создаем аккаунт:

    • Переходим на сайт Amvera

    • Нажимаем на кнопку "Регистрация".

    • Подтверждаем почту и телефон.

Создание проекта и размещение приложения

  1. Создай новый проект:

    • После входа на платформу, на главной странице нажми кнопку "Создать" или "Создать первый!".

    https://habrastorage.org/r/w1560/getpro/habr/upload_files/90a/00c/aa0/90a00caa0d20e5d56d26f898a5eb9fe2.png

2.Настройка проекта:

  • Присвоим проекту название (лучше на английском).

  • Выберем тарифный план. Для развертывания бота достаточно самого простого тарифа.

  1. Подготовка кода для развертывания:

  • Вам потребуется создать файл конфигурации amvera.yml, который подскажет облаку, как запускать ваш проект.

  • Для упрощения создания этого файла воспользуйтесь графическим инструментом генерации.

  • Выбор окружения и зависимостей:

    • Укажите версию Python и путь до файла requirements.txt, который содержит все необходимые пакеты (можно использовать команду pip freeze для получения списка зависимостей, но она может сгенерировать лишнии зависимости, что замедлит сборку).

    • Укажите путь до основного файла вашего проекта, например main.py.

  • Генерация и загрузка файла:

    • Нажмите "Generate YAML" для создания файла amvera.yml и загрузите его в корень вашего проекта.

Файл конфигурации amvera.yml служит для того, чтобы платформа Amvera знала, как правильно собрать и запустить ваш проект. Этот файл содержит ключевую информацию об окружении, зависимостях, а также инструкциях для запуска приложения.

Структура файла amvera.yml:

meta:
  environment: python
  toolchain:
    name: pip
    version: "3.8"
build:
  requirementsPath: requirements.txt
run:
  scriptName: app.py
  persistenceMount: /data
  containerPort: "5000"

Порт в коде и конфигурации должен совпадать, и если у вас используется БД SQLite, обязательно сохраняйте ее в постоянное хранилище /data.

Для того чтобы наш проект корректно работал в среде Amvera, важно указать все необходимые пакеты в файле requirements.txt. Этот файл определяет все зависимости Python, которые нужны для выполнения кода.

Вот так выглядит наш файл requirements.txt :

Flask==3.0.3
Flask_SQLAlchemy==3.0.5

Инициализация и отправка проекта в репозиторий:

  • Инициализируйте git репозиторий в корне вашего проекта, если это еще не сделано:

    git init
  • Привяжите локальный репозиторий к удаленному на Amvera:

    git remote add amvera 
  • Добавьте и зафиксируйте изменения:

    git add .
    git commit -m "Initial commit"
  • Отправьте проект в облако:

    git push amvera master

Сборка и развертывание проекта:

  • После отправки проекта в систему, на странице проекта статус изменится на "Выполняется сборка". После завершения сборки проект перейдет в стадию "Выполняется развертывание", а затем в статус "Успешно развернуто".

  • Если проект не развернулся, проверьте логи сборки и логи приложения для отладки.

  • Если проект завис на этапе "Сборка", убедитесь в корректности файла amvera.yml

Мы вместе прошли путь от установки Python и настройки окружения до разработки интерфейса и развёртывания приложения на сервере. Надеюсь, этот процесс был понятным и увлекательным.


Если вам требуется легко развернуть проект на сервере и доставлять в него обновления тремя командами в IDE, зарегистрируйтесь в облаке со встроенным CI/CD Amvera Cloud, и получите 111 руб. на тестирование функционала.

Теги:
Хабы:
+6
Комментарии0

Публикации

Информация

Сайт
amvera.ru
Дата регистрации
Численность
11–30 человек
Местоположение
Россия
Представитель
Кирилл Косолапов

Истории