Если Вы когда-нибудь задумывался о том, как создать своё собственное веб-приложение для управления задачами, надеюсь, эта статья вам поможет.
Мы пройдём весь путь — от установки необходимых инструментов и настройки окружения до разработки интерфейса и деплоя приложения на сервере. Каждый этап будет сопровождаться объяснениями и примерами кода, которые вы сможете найти в репозитории на GitHub.
Перед началом разработки необходимо убедиться, что на вашем компьютере установлены Python 3 и Git. Python будет использоваться для создания серверной части приложения, а Git — для управления версиями и размещения кода на GitHub.
Установка Python и виртуального окружения
Установка Python и виртуального окружения
Первым делом убедимся, что установлен Python 3. Если нет, скачайте его с официального сайта.
Теперь создадим виртуальное окружение, чтобы все зависимости проекта были изолированы:
Создайте папку для проекта:
mkdir task_manager
cd task_manager
Создайте виртуальное окружение:
python3 -m venv venv
Активируйте виртуальное окружение:
На 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">✓</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"
Откройте Talend API Tester в браузере.
Выберите метод POST.
Введите URL
http://127.0.0.1:5000/add
.Перейдите на вкладку Body и выберите Form.
Добавьте параметры формы:
Ключ:
content
, Значение:Тестовая задача
Ключ:
date
, Значение:25.12.2023
Ключ:
time
, Значение:12:00
Нажмите кнопку "Send" для отправки запроса.
Проверьте, что сервер отвечает статусом 302 Found, что означает перенаправление.
Откройте браузер и перейдите по адресу
http://127.0.0.1:5000/add
, чтобы убедиться, что задача добавлена
Откройте браузер и перейдите по адресу http://127.0.0.1:5000/
, чтобы убедиться, что задача добавлена.
Тестирование получения списка задач
Метод: GET
URL:
http://127.0.0.1:5000/
Выберите метод GET.
Введите URL
http://127.0.0.1:5000/
.Нажмите "Send".
Проверьте ответ сервера. Вы должны увидеть HTML-код страницы с добавленной задачей в теле ответа.
Деплой на сервера Amvera
После успешного тестирования нашего Task Manager на локальном сервере, давайте развернём его на удалённом сервере, чтобы он был доступен 24/7 и не зависел от твоего компьютера.
Сервис Amvera мы выбрали, так как он даст нам
Бесплатное доменное имя
Возможность доставлять обновления тремя командами через git push (что нмного проще настройки классической VPS)
Это блог нашей компании, странно было бы выбирать конкурентов)
Регистрация в сервисе Amvera
Создаем аккаунт:
Переходим на сайт Amvera
Нажимаем на кнопку "Регистрация".
Подтверждаем почту и телефон.
Создание проекта и размещение приложения
Создай новый проект:
После входа на платформу, на главной странице нажми кнопку "Создать" или "Создать первый!".
2.Настройка проекта:
Присвоим проекту название (лучше на английском).
Выберем тарифный план. Для развертывания бота достаточно самого простого тарифа.
Подготовка кода для развертывания:
Вам потребуется создать файл конфигурации
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 руб. на тестирование функционала.