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

от автора

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

Мы пройдём весь путь — от установки необходимых инструментов и настройки окружения до разработки интерфейса и деплоя приложения на сервере. Каждый этап будет сопровождаться объяснениями и примерами кода, которые вы сможете найти в репозитории на 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 руб. на тестирование функционала.


ссылка на оригинал статьи https://habr.com/ru/articles/857704/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *