{"id":14293,"url":"\/distributions\/14293\/click?bit=1&hash=05c87a3ce0b7c4063dd46190317b7d4a16bc23b8ced3bfac605d44f253650a0f","hash":"05c87a3ce0b7c4063dd46190317b7d4a16bc23b8ced3bfac605d44f253650a0f","title":"\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u043d\u0435 \u043f\u043e\u0442\u0440\u0430\u0442\u0438\u0432 \u043d\u0438 \u043a\u043e\u043f\u0435\u0439\u043a\u0438","buttonText":"","imageUuid":""}

Разработка Task-manager при помощи библиотек Flask и psycopg2

Сегодня я, Рулев Владислав, буду рассказывать, как создавал свой собственный Task-manager. Что такое таск-менеджер? Это программное решение для управления проектами, с его помощью можно раздавать и контролировать поручения.

В настоящее время используются различные инструменты для управления задачами, такие как электронная почта, Excel-таблицы и живое общение. Однако такой подход затрудняет отслеживание статуса задач, распределение ресурсов и общую координацию работ.

Разработка собственного инструмента даст следующие преимущества:

1. Централизованное управление задачами.

Можно вести все задачи в одном месте, что облегчит их отслеживание, приоритизацию и распределение между сотрудниками. Каждый пользователь сможет видеть свои назначенные задачи и отслеживать их статус.

2. Анализ производительности.

Позволит собирать данные о выполнении задач, времени, затраченном на каждую задачу, и других метриках производительности. Так инструмент поможет анализировать эффективность работы, выявлять узкие места и принимать меры для их устранения.

Выбор инструментов

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

1. Интуитивно понятный интерфейс.

Нужно создать инструмент, использовать который может каждый, вне зависимости от уровня знаний.

2. Наличие единого шаблона создания задачи.

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

Опираясь на требования к разработке рассмотрю варианты реализации инструмента:

1. Пошаговый скрипт в Jupyter Notebook

2. Web-приложение

Изначально была идея сделать приложение в Jupyter Notebook используя для отрисовки интерфейса библиотеку ipywidgets, но из-за того, что на имеющейся версии библиотеки не получилось бы реализовать весь задуманный функционал, то фокус сместился на создание своего сайта.

С точки зрения пользователя сайт легче запустить и использовать, чем Jupyter Notebook, а с точки зрения разработки можно реализовать любой дизайн и почти неограниченный функционал.

Следующим шагом после определения вида задачника, будет выбор инструментов. Так как у всех сотрудников установлен python, то и разработка будет вестись на нём. На старте использовать буду следующие библиотеки:

1. Flask – легкий и гибкий фреймворк для разработки веб-приложений;

2. Flask-WTF – расширение Flask, интегрирующее библиотеку WTForms, которая предоставляет полезные функции для простого создания и обработки форм в веб-приложении.

3. Psycopg2 - адаптер базы данных PostgreSQL для Python, т.к. использую Greenplum;

4. Pandas - программная библиотека для обработки и анализа данных.

Для начала такого набора инструментов и создания первой версии task-manager хватит.

Создание страницы «Авторизация»

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

$ pip install flask $ pip install flask-wtf $ pip install pandas $ pip install psycopg2

Отлично, теперь есть необходимые библиотеки для разработки веб-приложения, пора создавать проект в PyCharm-е и поднимать локальный сервер. Структура проекта на начальном этапе должна выглядеть как на Рис.1.

Рисунок 1

Далее в файлы заполняются следующим образом:

from flask import Flask app = Flask(__name__) from app import routes #Код файла __init__.py
from app import app @app.route('/') @app.route('/index') def login(): return ('Hello, World!') #

Немного разберёмся что же делают декораторы @app.route . В данном случае декоратор создаёт связь между URL-адресом, заданным в качестве аргумента, и функцией, которая написана ниже. Таким образом, когда браузер будет запрашивать адрес ‘/’ или ‘/index’, Flask выполнит функцию ниже и передаст обратно в браузер возвращаемое значение.

Запущу проект и перейду на указанный локальный сервер (Рис.2).

Рисунок 2

Сервер поднялся, теперь создам форму для авторизации. Для этого и была установлена библиотека Flask-WTF.

В данный момент приложение простое и сильно беспокоится о конфигурации не требуется, но всё же стоит поставить секретный ключ. Для этого создам config в корневой директории приложения, а также обновлю __init__.py

import os class Config(object): SECRET_KEY = os.environ.get('SECRET_KEY') or 'never-gonna-give-you-up'

Flask и некоторые его расширения используют значение секретного колюча в качестве криптографического ключа, который требуется для генерации подписей и токенов. Конкретно расширение Flask-WTF использует секретный ключ для защиты веб-формы от атаки Cross-Site Request Forgery (подделка межсайтовых запросов).

Важно: при развертывании данного приложения на сервере нужно будет устанавливать уникальное и трудно подбираемое значение. Секретный ключ должен знать только доверенный круг лиц, которые сопровождают приложение!

from flask import Flask from config import Config app = Flask(__name__) app.config.from_object(Config) from app import routes

Мне понадобилось создать ещё один файл и заполнить его следующим образом.

from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import DataRequired class LoginForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) submit = SubmitField('Войти')

StringField – поле ввода текста, в HTML равно тегу .<input type=”text”>.

PasswordField – поле ввода пароля, в HTML равно тегу .<input type=”password”>.

SubmitField – кнопка отправки формы на сервер, в HTML равно тегу <input type=”submit”>.

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

<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='my_style.css' )}}"> <link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='style.css' )}}"> <title>Задачник ОАРБ - Авторизация</title> </head> <style> </style> <body> <div class="authorization position-absolute top-50 start-50 translate-middle"> <div class="position-absolute top-50 start-50 translate-middle"> <h1>Авторизация</h1><br> <form action="" method="post" novalidate> {{ form.hidden_tag() }} <p> {{ form.username(size=32, placeholder="login") }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.password(size=32, placeholder="password") }}<br> {% for error in form.password.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p style="color: red">{{ form.error_connect.label }}</p> <p>{{ form.submit() }}</p> </form> </div> </div> </body> </html>

После этого доработаю файл routes следующим образом:

from flask import render_template, redirect from app import app from app.forms import LoginForm @app.route('/') @app.route('/index') def login(): form = LoginForm() return render_template('index.html', form=form)

Стартовая страница выглядит как на Рис.3.

Рисунок 3

Создание страницы «Профиль»

После того, как я сверстал форму, нужно настроить подключение к БД. Для подключения буду использовать библиотеку psycopg2, а для обработки и хранения возьму pandas.

Импортирую библиотеки:

import pandas as pd import psycopg2 as pg

Создам метод для подключения к БД:

# Функция подключения к БД Greenplum def get_db_connection(user, password): try: # Переменные для глобального использования global user_login global user_password user_login = user user_password = password # Указываем параметры подключения conn_param = f'host=server.ru port=1234 dbname=mydb user={user} password={password}' # Создаем подключение и включаем автокомит conn = pg.connect(conn_param) conn.autocommit = True return conn except pg.OperationalError: # В случае ошибки выводим сообщение print("EGOOOOOOOOOOOOOOOOOOOR:", pg.OperationalError) return 0

Обновляю метод login:

@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) def login(): # Создам форму, которую буду передавать для отрисовки form = LoginForm() # Если нажали кнопку if form.validate_on_submit(): # Создам подключение global conn # form.username.data – логин с формы # form.password.data – пароль с формы conn = get_db_connection(str(form.username.data), str(form.password.data)) # Если подключение успешно создано if conn: return redirect('/main') return render_template('index.html', form=form)

Чуть подробнее остановлюсь на этом моменте.

В дизайнере маршрутов я добавил аргумент methods. Этим аргументом я показываю, что функция принимает запросы GET и POST (по умолчанию принимаются только GET запросы).

GET-запросы – это запросы, которые возвращают информацию клиенту (в моем случае – страница в веб-браузере).

POST-запросы – это запросы, которые используются при отправке браузером формы на сервер (на примере данной страницы будут отправлены значения с полей ввода логина и пароля).

Рассмотрю метод form.validate_on_submit(). Данный метод выполняет обработку формы (POST-запрос). Когда пользователь только попадает на страницу, то отправляется GET-запрос и метод возвращает значение False, поэтому конструкция if пропускается, и пользователь видит стартовую страницу.

Когда браузер отправляет POST-запрос (после нажатия кнопки submit), метод собирает данные с формы, запускает валидаторы, которые я создал для полей, и если всё нормально, то возвращает True.

Проработаю следующую страницу «Профиль», на которую пользователь будет попадать после аутентификации, но для начала создам шаблон, по которому будут строиться дальнейшие странички.

Механизм шаблонов Jinja2 поставляется в комплекте с Flask. Jinja2 заменяет блоки {{…}} соответствующими значениями, заданными аргументами, указанными при вызове функции render_template().

Для чего же нужен шаблонизатор?

1. Сокращение лишней работы.

Вместо того, чтобы для каждой страницы заново писать одну и ту же повторяющуюся структуру (Тег head и header), напишу её один раз, динамически изменяя только основное содержимое страницы.

2. Удобная корректировка.

Если я решу внести какие-либо изменения в структуру или стиль страницы, то нужно будет изменить только один файл. (Пример: добавление нового элемента навигации).

3. Разделение логики и дизайна.

При помощи макета можно отдельно работать с поведением сайта и его визуальной составляющей. Это особенно удобно, если работать в паре «дизайнер-программист»

С плюсами использования шаблонизатора я разобрался, теперь создам файл base.html, который будет выглядеть следующим образом:

<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='my_style.css' )}}"> <title>Задачник - {% block title %}{% endblock %}</title> </head> <header> <ul class="nav nav-pills nav-fill"> <li class="nav-item"> <a class="nav-link" href="/main">Профиль</a> </li> <li class="nav-item"> <a class="nav-link" href="/create_task">Создание задачи</a> </li> <li class="nav-item"> <a class="nav-link" href="/select_task_for_me">Просмотр задач для меня</a> </li> <li> <a class="nav-link" href="/select_task_from_me">Просмотр задач, которые назначил я</a> </li> </ul> </header> <body> {% block content_h1 %}{% endblock %} <hr> {% block content %}{% endblock %} <hr> </body> </html>

{% block … %} {% endblock %} – шаблон, куда из другого файла будет вставляться контент.

Отдельное внимание стоит уделить подключению css файла (блок )

Для того, чтобы сервер мог получить css была создана структура проекта как на Рис.4.

Рисунок 4

Файл my_style.css содержит немного доработанную выгрузку bootstrap. Bootstrap понадобился мне для создания адаптивной верстки, чтобы приложение работало хорошо при разном разрешении экрана.

Теперь создаю страницу «Профиль» следующим образом:

{% extends "base.html" %} {% block title %}Профиль{% endblock %} {% block content_h1 %} <h1 style="text-align:center">Профиль</h1> {% endblock %} {% block content %} <p><b>login:</b> {{selected.iloc[0]['user_id']}} </p> <p><b>ФИО:</b> {{selected.iloc[0]['login']}} </p> <p><b>email:</b> {{selected.iloc[0]['email']}} </p> {% endblock %}

{% extends “base.html” %} – использование созданного шаблона.

{% block … %} {% endblock %} – все теги, написанные внутри такой конструкции, подставятся в шаблон.

{{ … }} – использование переданного объекта. В данном случае я передаю Data Frame selected.

Но как получить данные для профиля? – Написать метод с запросом к БД. Выглядеть метод будет так:

# Выборка данных о пользователе def select_profile(connect,created_user): global user_login SQL=f'''select * from tm_schema.task_manager_user where user_id={user_login} '''.format(created_user) return pd.read_sql_query(SQL, connect)

Осталось прописать route(‘/main’).

@app.route('/main') def main(): global conn selected = select_profile(conn, 1) return render_template('main.html', selected=selected)

Теперь после успешной авторизации попадаю на страницу профиля (Рис.5).

Рисунок 5

Создание страницы «Создание задачи»

По ходу создания Task manager у меня получилось подключиться БК и подтянуть из нее данные о пользователе. Применю этот опыт для получения данных о сотрудниках, текущих спринтах и типах задач. Как итог получу шаблон, изображённый на Рис.6.

Рисунок 6

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

# Получение списка сотрудников def select_users(connect, created_user): SQL = f'''select * from tm_schema.task_manager_user '''.format(created_user) return pd.read_sql_query(SQL, connect) # Получение списка спринтов def select_checks(connect, created_user): SQL = f'''select * from tm_schema.task_manager_check '''.format(created_user) return pd.read_sql_query(SQL, connect) # Получение типов задач def select_task_types(connect, created_user): global user_login SQL = f'''select * from tm_schema.task_manager_dic_task_type '''.format(created_user) return pd.read_sql_query(SQL, connect)

Следующим шагом будет создание формы с интересующими полями.

class Create_task(FlaskForm): task_name = StringField('Название задачи:', validators=[DataRequired()]) task_created_date = DateTimeField('Дата создания:', format='%d-%m-%Y %H:%M') task_in_check = StringField("Спринт:") task_created_user = StringField('Пользователь, создавший задачу:') task_update_date = DateTimeField('Дата обновления:', format='%d-%m-%Y %H:%M') task_update_user = StringField('Пользователь, обновивший задачу:') task_description = TextAreaField('Описание задачи:') task_control_date = DateTimeField('Контрольная дата:', format='%d-%m-%Y %H:%M') task_assigned_date = DateTimeField('Дата назначения:', format='%d-%m-%Y %H:%M') to_user = StringField('Кому:') task_priority = SelectField('Приоритет:', choices=[('Низкий', 'Низкий'), ('Средний', 'Средний'), ('Высокий', 'Высокий')], validate_choice=False, validators=[DataRequired()]) task_type = StringField('Тип задачи:') task_status = SelectField('Статус:', choices=[('1', 'Новая'), ('2', 'В работе'), ('3', 'Переназначена'), ('4', 'Закрыта')], default='1', validators=[DataRequired()]) submit = SubmitField('Создать задачу')

DateTimeField – поле выбора даты и времени, в HTML равно тегу <input type=”datetime-local”>.

TextAreaField – область ввода нескольких строк текста, в HTML равно тегу <textarea></textarea>.

SelectField – выпадающий список, в HTML равно тегу <select ></select>.

Функции и форма созданы, теперь пора вернуться к файлу routes.py, чтобы прописать обработку страницы создания задачи.

@app.route('/create_task', methods=['GET', 'POST']) def create_task(): conn = get_db_connection(user_login, user_password) form = Create_task() select_user = select_users(conn, 1) select_check = select_checks(conn, 1) task_type = select_task_types(conn, 1) selected = select_profile(conn, 1) conn.close() if form.validate_on_submit(): . . . . return render_template('create_task.html', form=form, select_user=select_user, select_check=select_check, task_type=task_type)

Одним из важных шагов, на котором стоит остановиться подробнее, будет верстка самой страницы.

{% extends "base.html" %} {% block title %}Создание задачи{% endblock %} {% block content_h1 %} <h1 style="text-align:center">Создание задачи</h1> {% endblock %} {% block content %} <form action="" method="POST" style="text-align:center"> {{ form.hidden_tag() }} <!-- Выбор пользователя, кому будет отправлена задача --> <p> {{ form.to_user.label }}<br> <input name="pers" type="text" list="person"> <datalist id="person"> {% for i, user in select_user.iterrows() %} <option value="{{ user['user_id']}}">{{ user['login'] }}</option> {% endfor %} </datalist> <br> {% for error in form.to_user.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} </p> <p> {{ form.task_in_check.label }} <br> <input name="check" type="text" list="check"> <datalist id="check"> {% for i, check in select_check.iterrows() %} <option value="{{ check['km'] }}">{{ check['name'] }}</option> {% endfor %} </datalist> <br> {% for error in form.task_in_check.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} </p> <p> {{ form.task_name.label }}<br> {{ form.task_name(size=35) }} <br> {% for error in form.task_name.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} </p> <p> {{ form.task_description.label }}<br> {{ form.task_description }} </p> <p> {{ form.task_control_date.label }} <br> <input name="control_date" type="datetime-local"> <br> {% for error in form.task_control_date.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} </p> <p> {{ form.task_type.label }}<br> <input name="type_task" type="text" list="type_task"> <datalist id="type_task"> {% for i, type in task_type.iterrows() %} <option value="{{ type['name'] }}"></option> {% endfor %} </datalist> <br> {% for error in form.task_type.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} </p> <p> {{ form.task_priority.label }} {{ form.task_priority(length=35) }} </p> {{ form.submit() }} </form> {% endblock %}

Разберу генерацию выпадающего списка (Рис.6)

Рисунок 6

<p> {{ form.to_user.label }}<br> <input name="pers" type="text" list="person"> <datalist id="person"> {% for i, user in select_user.iterrows() %} <option value="{{ user['user_id']}}">{{ user['login'] }}</option> {% endfor %} </datalist> <br> {% for error in form.to_user.errors %} <span style="color: red;">{{ error }}</span> {% endfor %} </p>

Для того, чтобы динамически сгенерировать список построчно переберу полученный Data Frame c данными о сотрудниках и выберу нужную информацию.

Остальные списки (Спринт и тип задачи) делаются аналогичным образом.

Теперь нужно прописать получение данных с формы и их обработку в файле routes.py

if form.validate_on_submit(): # Создаем переменную где будем считать кол-во ошибок err_counter = 0 # Получение значений переменных print('title:\t' + form.task_name.data) form.task_created_date.data = datetime.datetime.now() print(f'created_date:\t', form.task_created_date.data) # Конструкция try-except используем чтобы проверить все поля, а не завершать обработку при первой неудаче. try: # Собираем полученные данные с формы и сравниваем их с теми, что есть check_temp = select_check[select_check['km'] == request.form["check"]] # В случае если результат сравнения не пустой, то сохраняем данные form.task_in_check.data = check_temp.iloc[0]['check_id'] except: err_counter = err_counter + 1 form.task_in_check.errors = ['Поле не может быть пустым!'] print('check_id:\t' + str(form.task_in_check.data)) # Сохраняем данные из DataFrame при помощи iloc (Срез по индексам и столбцам) form.task_created_user.data = selected.iloc[0]['user_id'] print('created_user:\t' + str(form.task_created_user.data)) # Сохраняем текущее время form.task_update_date.data = datetime.datetime.now() print('update_date:\t', form.task_update_date.data) # Сохраняем данные из DataFrame при помощи iloc (Срез по индексам и столбцам) form.task_update_user.data = selected.iloc[0]['user_id'] print('update_user:\t' + str(form.task_update_user.data)) print('description:\t' + form.task_description.data) # Сохраняем данные с формы form.task_control_date.data = request.form['control_date'] # Если переменная пустая, то сообщаем об ошибке if form.task_control_date.data == '': err_counter = err_counter + 1 form.task_control_date.errors = ['Поле не может быть пустым!'] print('control_date:\t' + form.task_control_date.data) # Сохраняем текущее время form.task_assigned_date.data = datetime.datetime.now() print('assigned_date:\t', form.task_assigned_date.data) # Сохраняем данные с формы form.to_user.data = request.form['pers'] # Если переменная пустая, то сообщаем об ошибке if form.to_user.data == '': err_counter = err_counter + 1 form.to_user.errors = ['Поле не может быть пустым!'] print('assigned_user\t' + str(form.to_user.data)) print('priority:\t' + form.task_priority.data) try: # Собираем полученные данные с формы и сравниваем их с теми, что есть task_type_temp = task_type[task_type['name'] == str(request.form['type_task'])] # В случае если результат сравнения не пустой, то сохраняем данные form.task_type.data = task_type_temp.iloc[0]['task_type_id'] except: err_counter = err_counter + 1 form.task_type.errors = ['Поле не может быть пустым!'] print('type:\t', str(form.task_type.data)) print('status:\t' + form.task_status.data) print('Error ---------------------------------', err_counter) # Если не было ошибок, то выполняем запрос на внесение данных в таблицу if err_counter == 0: conn = get_db_connection(user_login, user_password) tip = insert_task(conn, 1, form) print(tip) conn.close() return redirect('/select_task_from_me')

Здесь идет присваивание переменным данных из формы, а также поля проверяются на пустоту и корректность данных. В случае, если не было обнаружено ошибок, то выполняется запрос на добавление строки в таблицу.

def insert_task(connect, created_user, form): # task_id , try: SQL = f'''insert into tm_schema.task_manager_task (title, created_date, check_id, created_user, update_date, update_user, description, control_date, assigned_date, assigned_user, priority, type_id, status_id) values ( '{form.task_name.data}', '{form.task_created_date.data}', '{form.task_in_check.data}', '{form.task_created_user.data}', '{form.task_update_date.data}', '{form.task_update_user.data}', '{form.task_description.data}', '{form.task_control_date.data}', '{form.task_assigned_date.data}', '{form.to_user.data}', '{form.task_priority.data}', '{form.task_type.data}', '{form.task_status.data}' ) '''.format(created_user) print(SQL) res = pd.read_sql_query(SQL, connect) return res except Exception as err: return err

Ниже будет приведён пример заполненной формы.

Пример создания задачи

1. Выбор исполнителя (Рис.7).

Выполняется поиск по фамилии.

Рисунок 7

2. Выбор спринта, в рамках которой назначается задача (Рис.8).

Поиск можно выполнить по названию.

Рисунок 8

3. Далее нужно ввести название и описание задачи (Рис.9).

Рисунок 9

4. Следующим шагом будет выставление контрольной даты (Рис.11).

Рисунок 10

5. После выбора контрольной даты нужно выбрать подходящий тип задачи (Рис.11).

Рисунок 11

6. Последним шагом будет выбор приоритета задачи (Рис.12).

Рисунок 12

7. Проверка введенных данных (Рис.13).

Рисунок 13

8. После проверки данных нажимаю кнопку «Создать задачу».

Создание страниц просмотра задач

Последним этапом будет создание страниц для просмотра созданных задач. Как и в прошлый раз сначала нужно определиться какие данные нужны и составить правильный запрос к БД.

def select_task_for_user(connect, created_user): global user_login SQL = f''' select task.task_id, task.title, task.created_date, create_user.login as created_user, create_user.email as create_user_email, select_check."name" as check_name, select_check.km as check_km, task.update_date, update_user.login as update_user, update_user.email as update_user_email, task.description, task.control_date, task.assigned_date, task.assigned_user, assigned_user.login as assigned_username, task.priority, task_type."name" as "type", status."name" as "status" from tm_schema.task_manager_task as task join tm_schema.task_manager_user as create_user on task.created_user = create_user.user_id join tm_schema.task_manager_user as update_user on task.update_user = update_user.user_id join tm_schema.task_manager_check as select_check on task.check_id = select_check.check_id join tm_schema.task_manager_dic_status as status on task.status_id = status.status_id join tm_schema.task_manager_dic_task_type as task_type on task.type_id = task_type.task_type_id join tm_schema.task_manager_user as assigned_user on task.assigned_user = assigned_user.user_id where assigned_user={user_login} '''.format(created_user) return pd.read_sql_query(SQL, connect)

Запрос получился достаточно большой, поэтому стоит разобрать его подробнее.

1. Нужно получать информацию о пользователе, который создал (create_user), обновил (update_user) и является исполнителем задачи (assigned_user).

2. Нужна полная информация о спринте (select_check).

3. А также понадобится узнать статус (status) и тип (task_type) задачи

Следующим шагом будет написание обработки страницы с задачами:

@app.route('/select_task_for_me', methods=['GET', 'POST']) def select_task_for_me(): conn = get_db_connection(user_login, user_password) my_task = select_task_for_user(conn, 1) conn.close() if request.method == 'POST': ... return render_template('select_task_for_me.html', my_task=my_task)

Теперь преступлю к верстке самой страницы.

{% extends "base.html" %} {% block title %}Просмотр задач{% endblock %} {% block content_h1 %} <h1 style="text-align:center">Просмотр задач</h1> {% endblock %} {% block content %} <div class="container"> <div class="row"> {% for i, task in my_task.iterrows() %} <div class="col-lg-4 col-sm-12 mb-3"> <div class="card-tovar"> <div class="tovar-details"> <form action="" method="post"> <table> <tr> <td style="width: 25%"> <input type="hidden" name="id_task" value="{{task['task_id']}}"> <h4>{{task['task_id']}}</h4> </td> <td> <h4>{{task['title']}}</h4> </td> </tr> </table> <hr> <p> Описание: {{ task['description'] }} </p> <p> Куратор: {{ task['created_user'] }} </p> <p> Статус: {{ task['status'] }} </p> <hr> <p> Срок выполнения: {{ task['control_date'] }} </p> <p> Исполнитель: {{ task['assigned_username'] }} </p> <input type="submit" value="Открыть задачу" style="width: 100%"> </form> </div> </div> </div> {% endfor %} </div> </div> {% endblock %}

Карточка с заданием будет выглядеть как на Рис.14.

Рисунок 14

А страница с задачами будет выглядеть как на Рис.15:

Рисунок 15

Планируемые доработки

1. Полноценный просмотр задачи Рис.16 (В данный момент в стадии шаблона).

Рисунок 16

Кнопки «Принять задачу» и «Завершить задачу» будут менять статус задачи на «В работе» и «Закрыта» соответственно.

2. Кнопка «Переназначить задачу» будет открывать новую страницу с переназначением Рис.17 (В данный момент в стадии шаблона). При переназначении можно будет указать нового исполнителя и добавить комментарии

Рисунок 17

3. Сделать отдельную страницу, где пользователь мог бы просматривать задачи, которые отправил он (Страница идентична «Просмотр задач», но имеет другие условия выборки)

4. При раскрытии карточки задачи со страницы «Задачи, которые назначил я» будет немного другой интерфейс Рис.18 (В данный момент в стадии шаблона).

Рисунок 18

Будет возможность редактировать отправленную задачу при помощи кнопки «Редактировать задачу». Данные на страницы будут заранее заполнены Рис.20 (В данный момент в стадии шаблона).

Рисунок 19

Вот так я и сделал MVP своего task manager-а. В скором времени будет запущено ограниченное тестирование, чтобы улучшать получившееся ПО. Далее я планирую сделать более приятный глазу дизайн, реализовать пункты из раздела «Планируемые доработки», а также исправить первые баги, которые найдутся в первый день тестирования.

Как только я выпущу стабильную версию task manager-a, то сразу предоставлю к ней свободный доступ и инструкцию для настройки задачника под свою команду.

0
Комментарии
-3 комментариев
Раскрывать всегда