Разработан чат-бот
1
ai/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .ai import update_statistics
|
17
ai/ai.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import database
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
def update_statistics():
|
||||||
|
if not config.AI.enabled:
|
||||||
|
return
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
database.update_statistics(config.AI.k)
|
||||||
|
except Exception:
|
||||||
|
logging.error('\n\t%s' % '\n\t'.join(traceback.format_exc().split('\n')).rstrip())
|
||||||
|
time.sleep(config.AI.update_frequency)
|
1
api/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .api import create_issue, read_issue
|
52
api/api.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import config
|
||||||
|
from jira import JIRA, Issue
|
||||||
|
from models import date, Subcategory, TimeRange
|
||||||
|
|
||||||
|
|
||||||
|
jira = JIRA(
|
||||||
|
basic_auth=(
|
||||||
|
config.Jira.username,
|
||||||
|
config.Jira.token,
|
||||||
|
),
|
||||||
|
server=config.Jira.server,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
time_ranges = {
|
||||||
|
1: ('T09:00:00.000+0300', 'T12:00:00.000+0300'),
|
||||||
|
2: ('T12:00:00.000+0300', 'T15:00:00.000+0300'),
|
||||||
|
3: ('T15:00:00.000+0300', 'T18:00:00.000+0300'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_issue(
|
||||||
|
subcategory: Subcategory,
|
||||||
|
_date: date,
|
||||||
|
time_range: TimeRange,
|
||||||
|
email_address: str,
|
||||||
|
phone_number: str,
|
||||||
|
comment: str,
|
||||||
|
firstname: str,
|
||||||
|
) -> Issue:
|
||||||
|
return jira.create_issue(
|
||||||
|
project='SFX',
|
||||||
|
summary='Задача от %s' % firstname,
|
||||||
|
issuetype={
|
||||||
|
'name': 'Telegram API',
|
||||||
|
},
|
||||||
|
customfield_10033={
|
||||||
|
'value': subcategory.category.name,
|
||||||
|
'child': {
|
||||||
|
'value': subcategory.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customfield_10035='%s%s' % (str(_date), time_ranges[time_range.id][0]),
|
||||||
|
customfield_10036='%s%s' % (str(_date), time_ranges[time_range.id][1]),
|
||||||
|
customfield_10037=email_address,
|
||||||
|
customfield_10038=phone_number,
|
||||||
|
customfield_10039=comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_issue(issue_id: str):
|
||||||
|
return jira.issue(issue_id)
|
1
bot/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .bot import dispatcher, storage
|
115
bot/bot.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.contrib.fsm_storage.memory import MemoryStorage
|
||||||
|
|
||||||
|
import config
|
||||||
|
from fsm import FSM
|
||||||
|
import handlers
|
||||||
|
import messages
|
||||||
|
|
||||||
|
|
||||||
|
bot = Bot(
|
||||||
|
token=config.Bot.token,
|
||||||
|
)
|
||||||
|
|
||||||
|
storage = MemoryStorage()
|
||||||
|
|
||||||
|
dispatcher = Dispatcher(
|
||||||
|
bot=bot,
|
||||||
|
storage=storage,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=messages.start,
|
||||||
|
commands=['start'],
|
||||||
|
state='*',
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.start,
|
||||||
|
state=FSM.start,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.select_category,
|
||||||
|
state=FSM.select_category,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.select_subcategory,
|
||||||
|
state=FSM.select_subcategory,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.select_date,
|
||||||
|
state=FSM.select_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.select_time_range,
|
||||||
|
state=FSM.select_time_range,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.input_email_address,
|
||||||
|
state=FSM.input_email_address,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.input_phone_number,
|
||||||
|
state=FSM.input_phone_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.input_comment,
|
||||||
|
state=FSM.input_comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.confirm_order,
|
||||||
|
state=FSM.confirm_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.success_sign_up,
|
||||||
|
state=FSM.success_sign_up,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.select_order,
|
||||||
|
state=FSM.select_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.no_orders,
|
||||||
|
state=FSM.no_orders,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.select_operation,
|
||||||
|
state=FSM.select_operation,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.reschedule_order_select_date,
|
||||||
|
state=FSM.reschedule_order_select_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.reschedule_order_select_time_range,
|
||||||
|
state=FSM.reschedule_order_select_time_range,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.success_reschedule_order,
|
||||||
|
state=FSM.success_reschedule_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.cancel_order,
|
||||||
|
state=FSM.cancel_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher.register_message_handler(
|
||||||
|
callback=handlers.success_cancel_order,
|
||||||
|
state=FSM.success_cancel_order,
|
||||||
|
)
|
1
config/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .config import Bot, Database, AI, Jira
|
78
config/config.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from configparser import RawConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
config = RawConfigParser()
|
||||||
|
config.read(
|
||||||
|
filenames='./config.ini',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
token = config.get(
|
||||||
|
section='Bot',
|
||||||
|
option='token',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
dbname = config.get(
|
||||||
|
section='Database',
|
||||||
|
option='dbname',
|
||||||
|
)
|
||||||
|
|
||||||
|
user = config.get(
|
||||||
|
section='Database',
|
||||||
|
option='user',
|
||||||
|
)
|
||||||
|
|
||||||
|
password = config.get(
|
||||||
|
section='Database',
|
||||||
|
option='password',
|
||||||
|
)
|
||||||
|
|
||||||
|
host = config.get(
|
||||||
|
section='Database',
|
||||||
|
option='host',
|
||||||
|
fallback='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
port = config.getint(
|
||||||
|
section='Database',
|
||||||
|
option='port',
|
||||||
|
fallback=5432,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AI:
|
||||||
|
enabled = config.getboolean(
|
||||||
|
section='AI',
|
||||||
|
option='enabled',
|
||||||
|
fallback=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
k = config.getint(
|
||||||
|
section='AI',
|
||||||
|
option='k',
|
||||||
|
fallback=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_frequency = config.getint(
|
||||||
|
section='AI',
|
||||||
|
option='update_frequency',
|
||||||
|
fallback=86400,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Jira:
|
||||||
|
username = config.get(
|
||||||
|
section='Jira',
|
||||||
|
option='username',
|
||||||
|
)
|
||||||
|
token = config.get(
|
||||||
|
section='Jira',
|
||||||
|
option='token',
|
||||||
|
)
|
||||||
|
server = config.get(
|
||||||
|
section='Jira',
|
||||||
|
option='server',
|
||||||
|
)
|
382
database.sql
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
create table categories (
|
||||||
|
id smallserial not null,
|
||||||
|
name character varying (64) not null,
|
||||||
|
description text not null,
|
||||||
|
primary key (id),
|
||||||
|
unique (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into categories (name, description) values
|
||||||
|
('Прикладное программное обеспечение', '[Описание категории]'),
|
||||||
|
('Системное программное обеспечение', '[Описание категории]'),
|
||||||
|
('Системы обеспечения IT-безопасности', '[Описание категории]'),
|
||||||
|
('Специализированное программное обеспечение', '[Описание категории]');
|
||||||
|
|
||||||
|
create table subcategories (
|
||||||
|
category_id smallint not null,
|
||||||
|
id smallint not null,
|
||||||
|
name character varying (64) not null,
|
||||||
|
description text not null,
|
||||||
|
foreign key (category_id) references categories on delete cascade,
|
||||||
|
primary key (category_id, id),
|
||||||
|
unique (category_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
create function sfx_subcategories_id_seq() returns trigger as $$
|
||||||
|
begin
|
||||||
|
if new.id is null then
|
||||||
|
new.id := (select
|
||||||
|
coalesce(max(subcategories.id) + 1, 1)
|
||||||
|
from
|
||||||
|
subcategories
|
||||||
|
where
|
||||||
|
subcategories.category_id = new.category_id);
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create trigger subcategories_id_seq before insert on subcategories
|
||||||
|
for each row execute procedure sfx_subcategories_id_seq();
|
||||||
|
|
||||||
|
insert into subcategories (category_id, name, description) values
|
||||||
|
(1, 'Программы 1С', '[Описание подкатегории]'),
|
||||||
|
(1, 'Программы ЭОС', '[Описание подкатегории]'),
|
||||||
|
(1, 'Собственные разработки на платформе 1С', '[Описание подкатегории]'),
|
||||||
|
(2, 'Продукция Astra Linux', '[Описание подкатегории]'),
|
||||||
|
(2, 'Продукция Alt Linux (Базальт-СПО)', '[Описание подкатегории]'),
|
||||||
|
(2, 'ПО для работы с текстом', '[Описание подкатегории]'),
|
||||||
|
(2, 'МойОфис', '[Описание подкатегории]'),
|
||||||
|
(3, 'Системы обеспечения сохранности данных', '[Описание подкатегории]'),
|
||||||
|
(3, 'Антивирусное ПО', '[Описание подкатегории]'),
|
||||||
|
(3, 'Системы защиты корпоративной информации', '[Описание подкатегории]'),
|
||||||
|
(3, 'Программный комплекс «Стахановец»', '[Описание подкатегории]'),
|
||||||
|
(3, 'Решение StaffCop Enterprise', '[Описание подкатегории]'),
|
||||||
|
(4, 'Графические редакторы Movavi', '[Описание подкатегории]'),
|
||||||
|
(4, 'Сметные программы', '[Описание подкатегории]'),
|
||||||
|
(4, 'Библиотеки нормативов и стандартов', '[Описание подкатегории]'),
|
||||||
|
(4, 'САПР', '[Описание подкатегории]'),
|
||||||
|
(4, 'Решения для совместной работы TrueConf', '[Описание подкатегории]'),
|
||||||
|
(4, 'Polys - система безопасных онлайн-голосований', '[Описание подкатегории]'),
|
||||||
|
(4, 'VISOCALL IP Телекоммуникация в медицине', '[Описание подкатегории]');
|
||||||
|
|
||||||
|
create table executors (
|
||||||
|
id bigserial not null,
|
||||||
|
name character varying (64) not null,
|
||||||
|
primary key (id),
|
||||||
|
unique (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into executors (name) values
|
||||||
|
('Богатырёва Ю. И.'),
|
||||||
|
('Ванькова В. С.'),
|
||||||
|
('Даниленко С. В.'),
|
||||||
|
('Екатериничев А. Л.'),
|
||||||
|
('Клепиков А. К.'),
|
||||||
|
('Мартынюк Ю. М.'),
|
||||||
|
('Надеждин Е. Н.'),
|
||||||
|
('Привалов А. Н.'),
|
||||||
|
('Родионова О. В.');
|
||||||
|
|
||||||
|
create table executor_specialties (
|
||||||
|
executor_id bigint not null,
|
||||||
|
category_id smallint not null,
|
||||||
|
subcategory_id smallint not null,
|
||||||
|
foreign key (executor_id) references executors on delete cascade,
|
||||||
|
foreign key (category_id, subcategory_id) references subcategories on delete cascade,
|
||||||
|
primary key (executor_id, category_id, subcategory_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into executor_specialties (executor_id, category_id, subcategory_id) values
|
||||||
|
(1, 1, 1),
|
||||||
|
(2, 1, 1),
|
||||||
|
(4, 1, 1),
|
||||||
|
(5, 1, 1),
|
||||||
|
(6, 1, 1),
|
||||||
|
(7, 1, 1),
|
||||||
|
(8, 1, 1),
|
||||||
|
(9, 1, 1),
|
||||||
|
(4, 1, 2),
|
||||||
|
(5, 1, 2),
|
||||||
|
(7, 1, 2),
|
||||||
|
(8, 1, 2),
|
||||||
|
(1, 1, 3),
|
||||||
|
(2, 1, 3),
|
||||||
|
(3, 1, 3),
|
||||||
|
(5, 1, 3),
|
||||||
|
(6, 1, 3),
|
||||||
|
(7, 1, 3),
|
||||||
|
(8, 1, 3),
|
||||||
|
(4, 2, 1),
|
||||||
|
(5, 2, 1),
|
||||||
|
(6, 2, 1),
|
||||||
|
(7, 2, 1),
|
||||||
|
(8, 2, 1),
|
||||||
|
(9, 2, 1),
|
||||||
|
(2, 2, 2),
|
||||||
|
(5, 2, 2),
|
||||||
|
(6, 2, 2),
|
||||||
|
(7, 2, 2),
|
||||||
|
(8, 2, 2),
|
||||||
|
(9, 2, 2),
|
||||||
|
(2, 2, 3),
|
||||||
|
(4, 2, 3),
|
||||||
|
(8, 2, 3),
|
||||||
|
(4, 2, 4),
|
||||||
|
(5, 2, 4),
|
||||||
|
(8, 2, 4),
|
||||||
|
(9, 2, 4),
|
||||||
|
(2, 3, 1),
|
||||||
|
(3, 3, 1),
|
||||||
|
(6, 3, 1),
|
||||||
|
(7, 3, 1),
|
||||||
|
(1, 3, 2),
|
||||||
|
(4, 3, 2),
|
||||||
|
(5, 3, 2),
|
||||||
|
(6, 3, 2),
|
||||||
|
(9, 3, 2),
|
||||||
|
(1, 3, 3),
|
||||||
|
(6, 3, 3),
|
||||||
|
(9, 3, 3),
|
||||||
|
(3, 3, 4),
|
||||||
|
(8, 3, 4),
|
||||||
|
(1, 3, 5),
|
||||||
|
(3, 3, 5),
|
||||||
|
(5, 3, 5),
|
||||||
|
(6, 3, 5),
|
||||||
|
(7, 3, 5),
|
||||||
|
(9, 3, 5),
|
||||||
|
(5, 4, 1),
|
||||||
|
(6, 4, 1),
|
||||||
|
(7, 4, 2),
|
||||||
|
(9, 4, 4),
|
||||||
|
(3, 4, 6),
|
||||||
|
(4, 4, 6);
|
||||||
|
|
||||||
|
create table time_ranges (
|
||||||
|
id smallserial not null,
|
||||||
|
start_time time not null,
|
||||||
|
end_time time not null,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into time_ranges (start_time, end_time) values
|
||||||
|
('9:00'::time, '12:00'::time),
|
||||||
|
('12:00'::time, '15:00'::time),
|
||||||
|
('15:00'::time, '18:00'::time);
|
||||||
|
|
||||||
|
create table orders (
|
||||||
|
id bigserial not null,
|
||||||
|
category_id smallint not null,
|
||||||
|
subcategory_id smallint not null,
|
||||||
|
date date not null,
|
||||||
|
time_range_id smallint not null,
|
||||||
|
executor_id bigint not null,
|
||||||
|
telegram_id bigint not null,
|
||||||
|
email_address character varying (256) not null,
|
||||||
|
phone_number character varying (16) not null,
|
||||||
|
comment character varying (1024) not null,
|
||||||
|
start_time timestamp,
|
||||||
|
end_time timestamp,
|
||||||
|
foreign key (category_id, subcategory_id) references subcategories on delete cascade,
|
||||||
|
foreign key (time_range_id) references time_ranges on delete cascade,
|
||||||
|
foreign key (executor_id) references executors on delete cascade,
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table issues (
|
||||||
|
id bigint not null,
|
||||||
|
key character varying (16) not null,
|
||||||
|
status character varying (16) not null,
|
||||||
|
telegram_id bigint not null,
|
||||||
|
primary key (id),
|
||||||
|
unique (key)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table statistics (
|
||||||
|
category_id smallint not null,
|
||||||
|
subcategory_id smallint not null,
|
||||||
|
execution_time interval not null,
|
||||||
|
foreign key (category_id, subcategory_id) references subcategories on delete cascade,
|
||||||
|
primary key (category_id, subcategory_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into statistics (category_id, subcategory_id, execution_time) values
|
||||||
|
(1, 1, interval '15 minutes'),
|
||||||
|
(1, 2, interval '30 minutes'),
|
||||||
|
(1, 3, interval '15 minutes'),
|
||||||
|
(2, 1, interval '1 hour'),
|
||||||
|
(2, 2, interval '2 hours'),
|
||||||
|
(2, 3, interval '1 hour'),
|
||||||
|
(2, 4, interval '2 hours'),
|
||||||
|
(3, 1, interval '15 minutes'),
|
||||||
|
(3, 2, interval '30 minutes'),
|
||||||
|
(3, 3, interval '15 minutes'),
|
||||||
|
(3, 4, interval '30 minutes'),
|
||||||
|
(3, 5, interval '15 minutes'),
|
||||||
|
(4, 1, interval '1 hour'),
|
||||||
|
(4, 2, interval '2 hours'),
|
||||||
|
(4, 3, interval '1 hour'),
|
||||||
|
(4, 4, interval '2 hours'),
|
||||||
|
(4, 5, interval '1 hour'),
|
||||||
|
(4, 6, interval '2 hours'),
|
||||||
|
(4, 7, interval '1 hour');
|
||||||
|
|
||||||
|
create or replace function sfx_update_statistics(
|
||||||
|
k integer
|
||||||
|
) returns void as $$
|
||||||
|
declare subcategory subcategories;
|
||||||
|
declare _order record;
|
||||||
|
begin
|
||||||
|
for subcategory in (
|
||||||
|
select
|
||||||
|
subcategories.category_id,
|
||||||
|
subcategories.id
|
||||||
|
from
|
||||||
|
subcategories
|
||||||
|
) loop
|
||||||
|
for _order in (
|
||||||
|
select
|
||||||
|
count(t1) as count,
|
||||||
|
avg(t1.end_time - t1.start_time) as avg
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
orders.start_time,
|
||||||
|
orders.end_time
|
||||||
|
from
|
||||||
|
orders
|
||||||
|
where
|
||||||
|
orders.category_id = subcategory.category_id and
|
||||||
|
orders.subcategory_id = subcategory.id and
|
||||||
|
orders.start_time is not null and
|
||||||
|
orders.end_time is not null
|
||||||
|
order by
|
||||||
|
orders.start_time desc
|
||||||
|
limit
|
||||||
|
k
|
||||||
|
) t1
|
||||||
|
) loop
|
||||||
|
if _order.count <> k then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
update
|
||||||
|
statistics
|
||||||
|
set
|
||||||
|
execution_time = _order.avg
|
||||||
|
where
|
||||||
|
statistics.category_id = subcategory.category_id and
|
||||||
|
statistics.subcategory_id = subcategory.id;
|
||||||
|
end loop;
|
||||||
|
end loop;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create or replace function sfx_read_free_to_order(
|
||||||
|
_category_id smallint,
|
||||||
|
_subcategory_id smallint
|
||||||
|
) returns table (
|
||||||
|
date date,
|
||||||
|
time_range_id smallint,
|
||||||
|
executor_id bigint,
|
||||||
|
busy_interval interval
|
||||||
|
) as $$
|
||||||
|
declare avg_interval interval := (
|
||||||
|
select
|
||||||
|
statistics.execution_time
|
||||||
|
from
|
||||||
|
statistics
|
||||||
|
where
|
||||||
|
statistics.category_id = _category_id and
|
||||||
|
statistics.subcategory_id = _subcategory_id
|
||||||
|
);
|
||||||
|
declare max_interval interval := (
|
||||||
|
select
|
||||||
|
max(time_ranges.end_time - time_ranges.start_time)
|
||||||
|
from
|
||||||
|
time_ranges
|
||||||
|
);
|
||||||
|
declare executor executors;
|
||||||
|
declare time_range time_ranges;
|
||||||
|
declare _date date := now()::date;
|
||||||
|
declare _busy_interval interval;
|
||||||
|
begin
|
||||||
|
if avg_interval > max_interval then
|
||||||
|
raise '0x00000005';
|
||||||
|
end if;
|
||||||
|
create temporary table tmp (
|
||||||
|
date date not null,
|
||||||
|
time_range_id smallint not null,
|
||||||
|
executor_id bigint not null,
|
||||||
|
busy_interval interval not null
|
||||||
|
) on commit drop;
|
||||||
|
while (
|
||||||
|
select
|
||||||
|
coalesce(count(t1.*), 0) < 6
|
||||||
|
from
|
||||||
|
(select distinct
|
||||||
|
tmp.date
|
||||||
|
from
|
||||||
|
tmp) t1
|
||||||
|
) loop
|
||||||
|
for executor in (
|
||||||
|
select
|
||||||
|
executors.id
|
||||||
|
from
|
||||||
|
executor_specialties
|
||||||
|
left join executors on
|
||||||
|
executor_specialties.executor_id = executors.id
|
||||||
|
where
|
||||||
|
executor_specialties.category_id = _category_id and
|
||||||
|
executor_specialties.subcategory_id = _subcategory_id
|
||||||
|
) loop
|
||||||
|
for time_range in (
|
||||||
|
select
|
||||||
|
time_ranges.id,
|
||||||
|
time_ranges.start_time,
|
||||||
|
time_ranges.end_time
|
||||||
|
from
|
||||||
|
time_ranges
|
||||||
|
) loop
|
||||||
|
if _date = now()::date and time_range.start_time < now()::time then
|
||||||
|
continue;
|
||||||
|
end if;
|
||||||
|
_busy_interval := (
|
||||||
|
select
|
||||||
|
coalesce(sum(statistics.execution_time), '0s'::interval)
|
||||||
|
from
|
||||||
|
orders
|
||||||
|
left join statistics on
|
||||||
|
orders.category_id = statistics.category_id and
|
||||||
|
orders.subcategory_id = statistics.subcategory_id
|
||||||
|
where
|
||||||
|
orders.date = _date and
|
||||||
|
orders.time_range_id = time_range.id and
|
||||||
|
orders.executor_id = executor.id
|
||||||
|
);
|
||||||
|
if time_range.end_time - time_range.start_time - _busy_interval > avg_interval then
|
||||||
|
insert into tmp (
|
||||||
|
date,
|
||||||
|
time_range_id,
|
||||||
|
executor_id,
|
||||||
|
busy_interval
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
_date,
|
||||||
|
time_range.id,
|
||||||
|
executor.id,
|
||||||
|
_busy_interval
|
||||||
|
);
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
end loop;
|
||||||
|
_date = _date + '1 day'::interval;
|
||||||
|
end loop;
|
||||||
|
return query (
|
||||||
|
select
|
||||||
|
tmp.*
|
||||||
|
from
|
||||||
|
tmp
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
1
database/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .database import read_categories, read_subcategories, read_free_to_order, create_order, read_orders, update_order, delete_order, update_statistics, create_issue, read_issues, delete_issue
|
468
database/database.py
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
import config
|
||||||
|
from models import date, Category, Subcategory, Executor, TimeRange, FreeToOrder, Order, Issue
|
||||||
|
from psycopg import AsyncConnection, Connection
|
||||||
|
|
||||||
|
|
||||||
|
conninfo = 'dbname=%(dbname)s user=%(user)s password=%(password)s host=%(host)s port=%(port)d' % {
|
||||||
|
'dbname': config.Database.dbname,
|
||||||
|
'user': config.Database.user,
|
||||||
|
'password': config.Database.password,
|
||||||
|
'host': config.Database.host,
|
||||||
|
'port': config.Database.port,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def read_categories() -> tuple[Category]:
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
select distinct
|
||||||
|
categories.id,
|
||||||
|
categories.name,
|
||||||
|
categories.description
|
||||||
|
from
|
||||||
|
categories
|
||||||
|
right join executor_specialties on
|
||||||
|
categories.id = executor_specialties.category_id
|
||||||
|
order by
|
||||||
|
categories.id;
|
||||||
|
'''
|
||||||
|
await cursor.execute(sql)
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
return tuple(
|
||||||
|
Category(
|
||||||
|
id=category_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
for
|
||||||
|
category_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
in records
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_subcategories(category: Category) -> tuple[Subcategory]:
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
select distinct
|
||||||
|
categories.id,
|
||||||
|
categories.name,
|
||||||
|
categories.description,
|
||||||
|
subcategories.id,
|
||||||
|
subcategories.name,
|
||||||
|
subcategories.description
|
||||||
|
from
|
||||||
|
subcategories
|
||||||
|
left join categories on
|
||||||
|
subcategories.category_id = categories.id
|
||||||
|
right join executor_specialties on
|
||||||
|
categories.id = executor_specialties.category_id and
|
||||||
|
subcategories.id = executor_specialties.subcategory_id
|
||||||
|
where
|
||||||
|
categories.id = %(category_id)s
|
||||||
|
order by
|
||||||
|
subcategories.id;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'category_id': category.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
return tuple(
|
||||||
|
Subcategory(
|
||||||
|
category=Category(
|
||||||
|
id=category_id,
|
||||||
|
name=category_name,
|
||||||
|
description=category_description,
|
||||||
|
),
|
||||||
|
id=subcategory_id,
|
||||||
|
name=subcategory_name,
|
||||||
|
description=subcategory_description,
|
||||||
|
)
|
||||||
|
for
|
||||||
|
category_id,
|
||||||
|
category_name,
|
||||||
|
category_description,
|
||||||
|
subcategory_id,
|
||||||
|
subcategory_name,
|
||||||
|
subcategory_description,
|
||||||
|
in records
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_free_to_order(subcategory: Subcategory) -> tuple[FreeToOrder]:
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
select
|
||||||
|
sfx_read_free_to_order.date,
|
||||||
|
time_ranges.id,
|
||||||
|
time_ranges.start_time,
|
||||||
|
time_ranges.end_time,
|
||||||
|
executors.id,
|
||||||
|
executors.name,
|
||||||
|
sfx_read_free_to_order.busy_interval
|
||||||
|
from
|
||||||
|
sfx_read_free_to_order(
|
||||||
|
%(category_id)s::smallint,
|
||||||
|
%(subcategory_id)s::smallint
|
||||||
|
)
|
||||||
|
left join time_ranges on
|
||||||
|
sfx_read_free_to_order.time_range_id = time_ranges.id
|
||||||
|
left join executors on
|
||||||
|
sfx_read_free_to_order.executor_id = executors.id;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'category_id': subcategory.category.id,
|
||||||
|
'subcategory_id': subcategory.id
|
||||||
|
},
|
||||||
|
)
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
return tuple(
|
||||||
|
FreeToOrder(
|
||||||
|
date=free_to_order_date,
|
||||||
|
time_range=TimeRange(
|
||||||
|
id=time_range_id,
|
||||||
|
start_time=time_range_start_time,
|
||||||
|
end_time=time_range_end_time,
|
||||||
|
),
|
||||||
|
executor=Executor(
|
||||||
|
id=executor_id,
|
||||||
|
name=executor_name,
|
||||||
|
),
|
||||||
|
busy_interval=free_to_order_busy_interval,
|
||||||
|
)
|
||||||
|
for
|
||||||
|
free_to_order_date,
|
||||||
|
time_range_id,
|
||||||
|
time_range_start_time,
|
||||||
|
time_range_end_time,
|
||||||
|
executor_id,
|
||||||
|
executor_name,
|
||||||
|
free_to_order_busy_interval,
|
||||||
|
in records
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_order(
|
||||||
|
subcategory: Subcategory,
|
||||||
|
date: date,
|
||||||
|
time_range: TimeRange,
|
||||||
|
executor: Executor,
|
||||||
|
telegram_id: int,
|
||||||
|
email_address: str,
|
||||||
|
phone_number: str,
|
||||||
|
comment: str,
|
||||||
|
) -> int:
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
insert into orders (
|
||||||
|
category_id,
|
||||||
|
subcategory_id,
|
||||||
|
date,
|
||||||
|
time_range_id,
|
||||||
|
executor_id,
|
||||||
|
telegram_id,
|
||||||
|
email_address,
|
||||||
|
phone_number,
|
||||||
|
comment
|
||||||
|
) values (
|
||||||
|
%(category_id)s,
|
||||||
|
%(subcategory_id)s,
|
||||||
|
%(date)s,
|
||||||
|
%(time_range_id)s,
|
||||||
|
%(executor_id)s,
|
||||||
|
%(telegram_id)s,
|
||||||
|
%(email_address)s,
|
||||||
|
%(phone_number)s,
|
||||||
|
%(comment)s
|
||||||
|
) returning
|
||||||
|
orders.id;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'category_id': subcategory.category.id,
|
||||||
|
'subcategory_id': subcategory.id,
|
||||||
|
'date': date,
|
||||||
|
'time_range_id': time_range.id,
|
||||||
|
'executor_id': executor.id,
|
||||||
|
'telegram_id': telegram_id,
|
||||||
|
'email_address': email_address,
|
||||||
|
'phone_number': phone_number,
|
||||||
|
'comment': comment,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
record = await cursor.fetchone()
|
||||||
|
return record[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def read_orders(
|
||||||
|
telegram_id: int,
|
||||||
|
) -> tuple[Order]:
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
select
|
||||||
|
orders.id,
|
||||||
|
categories.id,
|
||||||
|
categories.name,
|
||||||
|
categories.description,
|
||||||
|
subcategories.id,
|
||||||
|
subcategories.name,
|
||||||
|
subcategories.description,
|
||||||
|
orders.date,
|
||||||
|
time_ranges.id,
|
||||||
|
time_ranges.start_time,
|
||||||
|
time_ranges.end_time,
|
||||||
|
executors.id,
|
||||||
|
executors.name,
|
||||||
|
orders.telegram_id,
|
||||||
|
orders.email_address,
|
||||||
|
orders.phone_number,
|
||||||
|
orders.comment,
|
||||||
|
orders.start_time,
|
||||||
|
orders.end_time
|
||||||
|
from
|
||||||
|
orders
|
||||||
|
left join categories on
|
||||||
|
orders.category_id = categories.id
|
||||||
|
left join subcategories on
|
||||||
|
orders.category_id = subcategories.category_id and
|
||||||
|
orders.subcategory_id = subcategories.id
|
||||||
|
left join time_ranges on
|
||||||
|
orders.time_range_id = time_ranges.id
|
||||||
|
left join executors on
|
||||||
|
orders.executor_id = executors.id
|
||||||
|
where
|
||||||
|
orders.telegram_id = %(telegram_id)s and
|
||||||
|
orders.start_time is null and
|
||||||
|
orders.end_time is null
|
||||||
|
order by
|
||||||
|
orders.id;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'telegram_id': telegram_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
return tuple(
|
||||||
|
Order(
|
||||||
|
id=order_id,
|
||||||
|
subcategory=Subcategory(
|
||||||
|
category=Category(
|
||||||
|
id=category_id,
|
||||||
|
name=category_name,
|
||||||
|
description=category_description,
|
||||||
|
),
|
||||||
|
id=subcategory_id,
|
||||||
|
name=subcategory_name,
|
||||||
|
description=subcategory_description,
|
||||||
|
),
|
||||||
|
date=order_date,
|
||||||
|
time_range=TimeRange(
|
||||||
|
id=time_range_id,
|
||||||
|
start_time=time_range_start_time,
|
||||||
|
end_time=time_range_end_time,
|
||||||
|
),
|
||||||
|
executor=Executor(
|
||||||
|
id=executor_id,
|
||||||
|
name=executor_name,
|
||||||
|
),
|
||||||
|
telegram_id=order_telegram_id,
|
||||||
|
email_address=order_email_address,
|
||||||
|
phone_number=order_phone_number,
|
||||||
|
comment=order_comment,
|
||||||
|
start_time=order_start_time,
|
||||||
|
end_time=order_end_time,
|
||||||
|
)
|
||||||
|
for
|
||||||
|
order_id,
|
||||||
|
category_id,
|
||||||
|
category_name,
|
||||||
|
category_description,
|
||||||
|
subcategory_id,
|
||||||
|
subcategory_name,
|
||||||
|
subcategory_description,
|
||||||
|
order_date,
|
||||||
|
time_range_id,
|
||||||
|
time_range_start_time,
|
||||||
|
time_range_end_time,
|
||||||
|
executor_id,
|
||||||
|
executor_name,
|
||||||
|
order_telegram_id,
|
||||||
|
order_email_address,
|
||||||
|
order_phone_number,
|
||||||
|
order_comment,
|
||||||
|
order_start_time,
|
||||||
|
order_end_time,
|
||||||
|
in records
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_order(
|
||||||
|
order_id: int,
|
||||||
|
date: date,
|
||||||
|
time_range: TimeRange,
|
||||||
|
executor: Executor,
|
||||||
|
):
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
update
|
||||||
|
orders
|
||||||
|
set
|
||||||
|
date = %(date)s,
|
||||||
|
time_range_id = %(time_range_id)s,
|
||||||
|
executor_id = %(executor_id)s
|
||||||
|
where
|
||||||
|
orders.id = %(order_id)s;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'order_id': order_id,
|
||||||
|
'date': date,
|
||||||
|
'time_range_id': time_range.id,
|
||||||
|
'executor_id': executor.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_order(
|
||||||
|
order_id: int,
|
||||||
|
):
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
delete from
|
||||||
|
orders
|
||||||
|
where
|
||||||
|
orders.id = %(order_id)s;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'order_id': order_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_statistics(
|
||||||
|
k: int,
|
||||||
|
):
|
||||||
|
with Connection.connect(conninfo) as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
select
|
||||||
|
sfx_update_statistics(
|
||||||
|
%(k)s
|
||||||
|
);
|
||||||
|
'''
|
||||||
|
cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'k': k,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_issue(
|
||||||
|
issue: Issue,
|
||||||
|
):
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
insert into issues (
|
||||||
|
id,
|
||||||
|
key,
|
||||||
|
status,
|
||||||
|
telegram_id
|
||||||
|
) values (
|
||||||
|
%(issue_id)s,
|
||||||
|
%(key)s,
|
||||||
|
%(status)s,
|
||||||
|
%(telegram_id)s
|
||||||
|
);
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'issue_id': issue.id,
|
||||||
|
'key': issue.key,
|
||||||
|
'status': issue.status,
|
||||||
|
'telegram_id': issue.telegram_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_issues(
|
||||||
|
telegram_id: int,
|
||||||
|
) -> tuple[Issue]:
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
select
|
||||||
|
issues.id,
|
||||||
|
issues.key,
|
||||||
|
issues.status,
|
||||||
|
issues.telegram_id
|
||||||
|
from
|
||||||
|
issues
|
||||||
|
where
|
||||||
|
issues.telegram_id = %(telegram_id)s and
|
||||||
|
issues.status != 'done'
|
||||||
|
order by
|
||||||
|
issues.id;
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'telegram_id': telegram_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
records = await cursor.fetchall()
|
||||||
|
return tuple(
|
||||||
|
Issue(
|
||||||
|
id=issue_id,
|
||||||
|
key=key,
|
||||||
|
status=status,
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
)
|
||||||
|
for
|
||||||
|
issue_id,
|
||||||
|
key,
|
||||||
|
status,
|
||||||
|
telegram_id,
|
||||||
|
in
|
||||||
|
records
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_issue(
|
||||||
|
issue: Issue,
|
||||||
|
):
|
||||||
|
async with await AsyncConnection.connect(conninfo) as connection:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
sql = '''
|
||||||
|
delete from
|
||||||
|
issues
|
||||||
|
where
|
||||||
|
issues.id = %(issue_id)s
|
||||||
|
'''
|
||||||
|
await cursor.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
'issue_id': issue.id,
|
||||||
|
},
|
||||||
|
)
|
1
errors/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .errors import Error
|
20
errors/errors.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
class Error(str):
|
||||||
|
errors = {
|
||||||
|
0x00000000: 'Неизвестная ошибка.',
|
||||||
|
0x00000001: 'Выберете из предложенного.',
|
||||||
|
0x00000002: 'Текст слишком длинный.',
|
||||||
|
0x00000003: 'Введите корректный адрес электронной почты.',
|
||||||
|
0x00000004: 'Введите корректный номер телефона.',
|
||||||
|
0x00000005: 'К сожалению, бот не способен обрабатывать заказы этой подкатегории.\n\nСвяжитесь с нами любым удобным для вас способом:\n+7 (4872) 70-02-70 — телефон в Туле;\n8-800-775-15-40 — по России бесплатно;\nsfx@sfx-tula.ru — электронная почта.',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __new__(cls, code):
|
||||||
|
self = str.__new__(Error)
|
||||||
|
self.code = code
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '<b>Ошибка 0x%.8X!</b>\n%s' % (
|
||||||
|
self.code,
|
||||||
|
self.errors[self.code],
|
||||||
|
)
|
1
fsm/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .fsm import FSM
|
22
fsm/fsm.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from aiogram.dispatcher.filters.state import StatesGroup, State
|
||||||
|
|
||||||
|
|
||||||
|
class FSM(StatesGroup):
|
||||||
|
start = State()
|
||||||
|
select_category = State()
|
||||||
|
select_subcategory = State()
|
||||||
|
select_date = State()
|
||||||
|
select_time_range = State()
|
||||||
|
input_email_address = State()
|
||||||
|
input_phone_number = State()
|
||||||
|
input_comment = State()
|
||||||
|
confirm_order = State()
|
||||||
|
success_sign_up = State()
|
||||||
|
select_order = State()
|
||||||
|
no_orders = State()
|
||||||
|
select_operation = State()
|
||||||
|
reschedule_order_select_date = State()
|
||||||
|
reschedule_order_select_time_range = State()
|
||||||
|
success_reschedule_order = State()
|
||||||
|
cancel_order = State()
|
||||||
|
success_cancel_order = State()
|
1
handlers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .handlers import start, select_category, select_subcategory, select_date, select_time_range, input_email_address, input_phone_number, input_comment, confirm_order, success_sign_up, select_order, no_orders, select_operation, reschedule_order_select_date, reschedule_order_select_time_range, success_reschedule_order, cancel_order, success_cancel_order
|
374
handlers/handlers.py
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
import api
|
||||||
|
import database
|
||||||
|
import models
|
||||||
|
from errors import Error
|
||||||
|
import logging
|
||||||
|
import messages
|
||||||
|
from models import Message, FSMContext
|
||||||
|
import psycopg
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
async def start(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Записаться':
|
||||||
|
return await messages.select_category(message, state)
|
||||||
|
if message.text == 'Мои обращения':
|
||||||
|
return await messages.select_order(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_category(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.start(message, state)
|
||||||
|
try:
|
||||||
|
category = tuple(
|
||||||
|
category
|
||||||
|
for category in await database.read_categories()
|
||||||
|
if category.name == message.text
|
||||||
|
)[0]
|
||||||
|
await state.update_data({
|
||||||
|
'category': category,
|
||||||
|
})
|
||||||
|
await messages.select_subcategory(message, state)
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_subcategory(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_category(message, state)
|
||||||
|
try:
|
||||||
|
data = await state.get_data()
|
||||||
|
subcategory = tuple(
|
||||||
|
subcategory
|
||||||
|
for subcategory in await database.read_subcategories(data['category'])
|
||||||
|
if subcategory.name == message.text
|
||||||
|
)[0]
|
||||||
|
try:
|
||||||
|
free_to_order = await database.read_free_to_order(subcategory)
|
||||||
|
await state.update_data({
|
||||||
|
'subcategory': subcategory,
|
||||||
|
'free_to_order': free_to_order,
|
||||||
|
})
|
||||||
|
await messages.select_date(message, state)
|
||||||
|
except psycopg.errors.RaiseException as error:
|
||||||
|
if error.diag.message_primary == '0x00000005':
|
||||||
|
logging.error('\n\tКатегория: %s\n\tПодкатегория: %s\n\n\tСредняя длительность выполнения заказа указанной подкатегории (таблица statistics) дольше, чем все временные промежутки (таблица time_ranges).' % (
|
||||||
|
subcategory.category.name,
|
||||||
|
subcategory.name,
|
||||||
|
))
|
||||||
|
return await message.answer(
|
||||||
|
text=Error(0x00000005),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_date(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_subcategory(message, state)
|
||||||
|
try:
|
||||||
|
data = await state.get_data()
|
||||||
|
free_to_order = tuple(
|
||||||
|
item
|
||||||
|
for item in data['free_to_order']
|
||||||
|
if item.date.strftime('%d.%m') == message.text
|
||||||
|
)[0]
|
||||||
|
await state.update_data({
|
||||||
|
'date': free_to_order.date,
|
||||||
|
})
|
||||||
|
await messages.select_time_range(message, state)
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_time_range(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_date(message, state)
|
||||||
|
try:
|
||||||
|
data = await state.get_data()
|
||||||
|
free_to_order = tuple(
|
||||||
|
item
|
||||||
|
for item in data['free_to_order']
|
||||||
|
if '%s - %s' % (
|
||||||
|
item.time_range.start_time.strftime('%H:%M'),
|
||||||
|
item.time_range.end_time.strftime('%H:%M'),
|
||||||
|
) == message.text
|
||||||
|
)[0]
|
||||||
|
await state.update_data({
|
||||||
|
'time_range': free_to_order.time_range,
|
||||||
|
})
|
||||||
|
await messages.input_email_address(message, state)
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def input_email_address(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_time_range(message, state)
|
||||||
|
match = re.match(models.email_address, message.text.strip())
|
||||||
|
if match is None:
|
||||||
|
return await message.answer(
|
||||||
|
text=Error(0x00000003),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
await state.update_data({
|
||||||
|
'email_address': match.string,
|
||||||
|
})
|
||||||
|
await messages.input_phone_number(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
async def input_phone_number(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.input_email_address(message, state)
|
||||||
|
phone_number = re.sub(models.phone_number, '', message.text)
|
||||||
|
if len(phone_number) not in range(8, 16):
|
||||||
|
return await message.answer(
|
||||||
|
text=Error(0x00000004),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
await state.update_data({
|
||||||
|
'phone_number': '+%s' % phone_number,
|
||||||
|
})
|
||||||
|
await messages.input_comment(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
async def input_comment(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.input_phone_number(message, state)
|
||||||
|
comment = message.text
|
||||||
|
if len(comment) > models.max_comment_length:
|
||||||
|
return await message.answer(
|
||||||
|
text=Error(0x00000002),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
await state.update_data({
|
||||||
|
'comment': comment,
|
||||||
|
})
|
||||||
|
await messages.confirm_order(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_order(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.input_comment(message, state)
|
||||||
|
if message.text == 'Подтвердить':
|
||||||
|
data = await state.get_data()
|
||||||
|
# executor = sorted(
|
||||||
|
# tuple(
|
||||||
|
# item
|
||||||
|
# for item in data['free_to_order']
|
||||||
|
# if item.date == data['date'] and item.time_range == data['time_range']
|
||||||
|
# ),
|
||||||
|
# key=lambda x: x.busy_interval,
|
||||||
|
# )[0].executor
|
||||||
|
# order_id = await database.create_order(
|
||||||
|
# subcategory=data['subcategory'],
|
||||||
|
# date=data['date'],
|
||||||
|
# time_range=data['time_range'],
|
||||||
|
# executor=executor,
|
||||||
|
# telegram_id=message.from_user.id,
|
||||||
|
# email_address=data['email_address'],
|
||||||
|
# phone_number=data['phone_number'],
|
||||||
|
# comment=data['comment'],
|
||||||
|
# )
|
||||||
|
# orders = await database.read_orders(message.from_user.id)
|
||||||
|
issue = await api.create_issue(
|
||||||
|
subcategory=data['subcategory'],
|
||||||
|
_date=data['date'],
|
||||||
|
time_range=data['time_range'],
|
||||||
|
email_address=data['email_address'],
|
||||||
|
phone_number=data['phone_number'],
|
||||||
|
comment=data['comment'],
|
||||||
|
firstname=message.from_user.first_name,
|
||||||
|
)
|
||||||
|
await state.update_data({
|
||||||
|
# 'executor': executor,
|
||||||
|
'issue_key': issue.key,
|
||||||
|
})
|
||||||
|
await database.create_issue(
|
||||||
|
issue=models.Issue(
|
||||||
|
id=issue.id,
|
||||||
|
key=issue.key,
|
||||||
|
status='new',
|
||||||
|
telegram_id=message.from_user.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return await messages.success_sign_up(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def success_sign_up(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Главное меню':
|
||||||
|
return await messages.start(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_order(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.start(message, state)
|
||||||
|
try:
|
||||||
|
issue = tuple(
|
||||||
|
issue
|
||||||
|
for issue in await database.read_issues(message.from_user.id)
|
||||||
|
if issue.key == message.text
|
||||||
|
)[0]
|
||||||
|
await state.update_data({
|
||||||
|
'issue': issue,
|
||||||
|
})
|
||||||
|
await messages.select_operation(message, state)
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def no_orders(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Главное меню':
|
||||||
|
return await messages.start(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_operation(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_order(message, state)
|
||||||
|
if message.text == 'Перенести':
|
||||||
|
data = await state.get_data()
|
||||||
|
free_to_order = await database.read_free_to_order(data['order'].subcategory)
|
||||||
|
await state.update_data({
|
||||||
|
'free_to_order': free_to_order,
|
||||||
|
})
|
||||||
|
return await messages.reschedule_order_select_date(message, state)
|
||||||
|
if message.text == 'Отменить':
|
||||||
|
return await messages.cancel_order(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def reschedule_order_select_date(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_operation(message, state)
|
||||||
|
try:
|
||||||
|
data = await state.get_data()
|
||||||
|
free_to_order = tuple(
|
||||||
|
item
|
||||||
|
for item in data['free_to_order']
|
||||||
|
if item.date.strftime('%d.%m') == message.text
|
||||||
|
)[0]
|
||||||
|
await state.update_data({
|
||||||
|
'date': free_to_order.date,
|
||||||
|
})
|
||||||
|
await messages.reschedule_order_select_time_range(message, state)
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def reschedule_order_select_time_range(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.reschedule_order_select_date(message, state)
|
||||||
|
try:
|
||||||
|
data = await state.get_data()
|
||||||
|
free_to_order = tuple(
|
||||||
|
item
|
||||||
|
for item in data['free_to_order']
|
||||||
|
if '%s - %s' % (
|
||||||
|
item.time_range.start_time.strftime('%H:%M'),
|
||||||
|
item.time_range.end_time.strftime('%H:%M'),
|
||||||
|
) == message.text
|
||||||
|
)[0]
|
||||||
|
executor = sorted(
|
||||||
|
tuple(
|
||||||
|
item
|
||||||
|
for item in data['free_to_order']
|
||||||
|
if item.date == data['date'] and item.time_range == free_to_order.time_range
|
||||||
|
),
|
||||||
|
key=lambda x: x.busy_interval,
|
||||||
|
)[0].executor
|
||||||
|
await state.update_data({
|
||||||
|
'executor': executor,
|
||||||
|
})
|
||||||
|
await database.update_order(
|
||||||
|
order_id=data['order'].id,
|
||||||
|
date=data['date'],
|
||||||
|
time_range=free_to_order.time_range,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
await messages.success_reschedule_order(message, state)
|
||||||
|
except IndexError:
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def success_reschedule_order(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Мои обращения':
|
||||||
|
await state.reset_data()
|
||||||
|
return await messages.select_order(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def cancel_order(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Назад':
|
||||||
|
return await messages.select_operation(message, state)
|
||||||
|
if message.text == 'Отменить заказ':
|
||||||
|
data = await state.get_data()
|
||||||
|
issue = await api.read_issue(
|
||||||
|
issue_id=data['issue'].id,
|
||||||
|
)
|
||||||
|
issue.delete()
|
||||||
|
await database.delete_issue(
|
||||||
|
issue=issue,
|
||||||
|
)
|
||||||
|
# await database.delete_order(
|
||||||
|
# order_id=data['issue'].id,
|
||||||
|
# )
|
||||||
|
return await messages.success_cancel_order(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def success_cancel_order(message: Message, state: FSMContext):
|
||||||
|
if message.text == 'Мои обращения':
|
||||||
|
await state.reset_data()
|
||||||
|
return await messages.select_order(message, state)
|
||||||
|
await message.answer(
|
||||||
|
text=Error(0x00000001),
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
1
keyboards/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .keyboards import start, categories, subcategories, dates, time_ranges, back, order_confirmation, main_menu, orders, operations, cancel_order, my_orders, issues
|
172
keyboards/keyboards.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import database
|
||||||
|
from models import date, ReplyKeyboardMarkup, KeyboardButton, Category, FreeToOrder, Order, Issue
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return ReplyKeyboardMarkup([
|
||||||
|
[
|
||||||
|
KeyboardButton('Записаться'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton('Мои обращения'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
async def categories():
|
||||||
|
keyboard = ReplyKeyboardMarkup()
|
||||||
|
for category in await database.read_categories():
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton(category.name),
|
||||||
|
)
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
async def subcategories(category: Category):
|
||||||
|
keyboard = ReplyKeyboardMarkup()
|
||||||
|
for subcategory in await database.read_subcategories(category):
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton(subcategory.name),
|
||||||
|
)
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def dates(free_to_order: tuple[FreeToOrder]):
|
||||||
|
_dates = tuple(
|
||||||
|
sorted(
|
||||||
|
set(
|
||||||
|
item.date
|
||||||
|
for item in free_to_order
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard = ReplyKeyboardMarkup()
|
||||||
|
for _date in _dates:
|
||||||
|
keyboard.insert(_date.strftime('%d.%m'))
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def time_ranges(free_to_order: tuple[FreeToOrder], _date: date):
|
||||||
|
_time_ranges = tuple(
|
||||||
|
sorted(
|
||||||
|
set(
|
||||||
|
item.time_range
|
||||||
|
for item in free_to_order
|
||||||
|
if item.date == _date
|
||||||
|
),
|
||||||
|
key=lambda x: x.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
keyboard = ReplyKeyboardMarkup()
|
||||||
|
for time_range in _time_ranges:
|
||||||
|
keyboard.insert('%s - %s' % (
|
||||||
|
time_range.start_time.strftime('%H:%M'),
|
||||||
|
time_range.end_time.strftime('%H:%M'),
|
||||||
|
))
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def back():
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def order_confirmation():
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton('Подтвердить'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main_menu():
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton('Главное меню'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def orders(_orders: tuple[Order]):
|
||||||
|
keyboard = ReplyKeyboardMarkup()
|
||||||
|
for order in _orders:
|
||||||
|
keyboard.insert(
|
||||||
|
KeyboardButton('№%d' % order.id),
|
||||||
|
)
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def operations():
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
# KeyboardButton('Перенести'),
|
||||||
|
KeyboardButton('Отменить'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_order():
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton('Отменить заказ'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def my_orders():
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton('Мои обращения'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def issues(_issues: tuple[Issue]):
|
||||||
|
keyboard = ReplyKeyboardMarkup()
|
||||||
|
for issue in _issues:
|
||||||
|
keyboard.insert(
|
||||||
|
KeyboardButton('%s' % issue.key),
|
||||||
|
)
|
||||||
|
keyboard.add(
|
||||||
|
KeyboardButton('Назад'),
|
||||||
|
)
|
||||||
|
return keyboard
|
26
main.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import asyncio
|
||||||
|
from aiogram import executor
|
||||||
|
import platform
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from bot import dispatcher
|
||||||
|
import ai
|
||||||
|
|
||||||
|
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
asyncio.set_event_loop_policy(
|
||||||
|
asyncio.WindowsSelectorEventLoopPolicy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
update_statistics_thread = Thread(
|
||||||
|
target=ai.update_statistics,
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
update_statistics_thread.start()
|
||||||
|
executor.start_polling(
|
||||||
|
dispatcher,
|
||||||
|
skip_updates=True,
|
||||||
|
)
|
1
messages/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .messages import start, select_category, select_subcategory, select_date, select_time_range, input_email_address, input_phone_number, input_comment, confirm_order, success_sign_up, select_order, select_operation, reschedule_order_select_date, reschedule_order_select_time_range, reschedule_order_select_time_range, success_reschedule_order, cancel_order, success_cancel_order
|
210
messages/messages.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import api
|
||||||
|
import database
|
||||||
|
from fsm import FSM
|
||||||
|
from models import iso_datetime, Message, FSMContext, InputFile
|
||||||
|
import keyboards
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
async def start(message: Message, state: FSMContext):
|
||||||
|
await state.reset_state()
|
||||||
|
await state.reset_data()
|
||||||
|
await message.answer(
|
||||||
|
text='Здравствуйте, %s!\nЧем могу помочь?' % message.from_user.first_name,
|
||||||
|
reply_markup=keyboards.start(),
|
||||||
|
)
|
||||||
|
await FSM.start.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_category(message: Message, state: FSMContext):
|
||||||
|
await message.answer(
|
||||||
|
text='Выберете категорию.',
|
||||||
|
reply_markup=await keyboards.categories(),
|
||||||
|
)
|
||||||
|
await FSM.select_category.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_subcategory(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='<b>%(name)s</b>\n\n%(description)s\n\nВыберете подкатегорию.' % dict(data['category']),
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=await keyboards.subcategories(data['category']),
|
||||||
|
)
|
||||||
|
await FSM.select_subcategory.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_date(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer_photo(
|
||||||
|
photo=InputFile('./static/images/%d.%d.png' % (
|
||||||
|
data['subcategory'].category.id,
|
||||||
|
data['subcategory'].id
|
||||||
|
)),
|
||||||
|
caption='<b>%(name)s</b>\n\n%(description)s\n\nВыберете дату.' % dict(data['subcategory']),
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.dates(data['free_to_order']),
|
||||||
|
)
|
||||||
|
await FSM.select_date.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_time_range(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='Выберете время.',
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.time_ranges(data['free_to_order'], data['date']),
|
||||||
|
)
|
||||||
|
await FSM.select_time_range.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def input_email_address(message: Message, state: FSMContext):
|
||||||
|
await message.answer(
|
||||||
|
text='Введите адрес электронной почты для связи.',
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.back(),
|
||||||
|
)
|
||||||
|
await FSM.input_email_address.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def input_phone_number(message: Message, state: FSMContext):
|
||||||
|
await message.answer(
|
||||||
|
text='Введите номер телефона для связи.',
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.back(),
|
||||||
|
)
|
||||||
|
await FSM.input_phone_number.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def input_comment(message: Message, state: FSMContext):
|
||||||
|
await message.answer(
|
||||||
|
text='Опишите вашу проблему.',
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.back(),
|
||||||
|
)
|
||||||
|
await FSM.input_comment.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_order(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='<b>Категория:</b> %s - %s.\n<b>Дата:</b> %s.\n<b>Время:</b> %s.\n<b>Адрес электронной почты:</b> %s.\n<b>Номер телефона:</b> %s.\n<b>Описание проблемы:</b>\n%s' % (
|
||||||
|
data['category'].name,
|
||||||
|
data['subcategory'].name,
|
||||||
|
data['date'].strftime('%d.%m.%Y'),
|
||||||
|
'%s - %s' % (
|
||||||
|
data['time_range'].start_time.strftime('%H:%M'),
|
||||||
|
data['time_range'].end_time.strftime('%H:%M'),
|
||||||
|
),
|
||||||
|
data['email_address'],
|
||||||
|
data['phone_number'],
|
||||||
|
data['comment'],
|
||||||
|
),
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.order_confirmation(),
|
||||||
|
)
|
||||||
|
await FSM.confirm_order.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def success_sign_up(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='<b>Обращение %s успешно оформлено!</b>\nВ указанное время с вами свяжется наш специалист.\nСпасибо за обращение!' % data['issue_key'],
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.main_menu(),
|
||||||
|
)
|
||||||
|
await FSM.success_sign_up.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_order(message: Message, state: FSMContext):
|
||||||
|
issues = await database.read_issues(message.from_user.id)
|
||||||
|
if len(issues) > 0:
|
||||||
|
await message.answer(
|
||||||
|
text='Выберете обращение.',
|
||||||
|
reply_markup=keyboards.issues(issues),
|
||||||
|
)
|
||||||
|
return await FSM.select_order.set()
|
||||||
|
await message.answer(
|
||||||
|
text='У вас нет активных обращений.',
|
||||||
|
reply_markup=keyboards.main_menu(),
|
||||||
|
)
|
||||||
|
await FSM.no_orders.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_operation(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
issue = await api.read_issue(data['issue'].id)
|
||||||
|
datetime_start = re.match(iso_datetime, issue.fields.customfield_10035)
|
||||||
|
datetime_end = re.match(iso_datetime, issue.fields.customfield_10036)
|
||||||
|
await message.answer(
|
||||||
|
text='<b>Категория:</b> %s.\n<b>Дата:</b> %s.\n<b>Время:</b> %s.\n<b>Адрес электронной почты:</b> %s.\n<b>Номер телефона:</b> %s.\n<b>Описание проблемы:</b>\n%s' % (
|
||||||
|
issue.fields.customfield_10033,
|
||||||
|
'%s.%s.%s' % (
|
||||||
|
datetime_start[3],
|
||||||
|
datetime_start[2],
|
||||||
|
datetime_start[1],
|
||||||
|
),
|
||||||
|
'%s:%s - %s:%s' % (
|
||||||
|
datetime_start[4],
|
||||||
|
datetime_start[5],
|
||||||
|
datetime_end[4],
|
||||||
|
datetime_end[5],
|
||||||
|
),
|
||||||
|
issue.fields.customfield_10037,
|
||||||
|
issue.fields.customfield_10038,
|
||||||
|
issue.fields.customfield_10039,
|
||||||
|
),
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.operations(),
|
||||||
|
)
|
||||||
|
await FSM.select_operation.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def reschedule_order_select_date(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='Выберете дату.',
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.dates(data['free_to_order']),
|
||||||
|
)
|
||||||
|
await FSM.reschedule_order_select_date.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def reschedule_order_select_time_range(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='Выберете время.',
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.time_ranges(data['free_to_order'], data['date']),
|
||||||
|
)
|
||||||
|
await FSM.reschedule_order_select_time_range.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def success_reschedule_order(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='<b>Обращение %s успешно изменено!</b>\nВ указанное время с вами свяжется наш специалист.\nСпасибо за обращение!' % data['order'].id,
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.my_orders(),
|
||||||
|
)
|
||||||
|
await FSM.success_reschedule_order.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def cancel_order(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='Отменить обращение %s?' % data['issue'].key,
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.cancel_order(),
|
||||||
|
)
|
||||||
|
await FSM.cancel_order.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def success_cancel_order(message: Message, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await message.answer(
|
||||||
|
text='Обращение %s успешно отменено!' % data['issue'].key,
|
||||||
|
parse_mode='HTML',
|
||||||
|
reply_markup=keyboards.my_orders(),
|
||||||
|
)
|
||||||
|
await FSM.success_cancel_order.set()
|
6
models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.types.reply_keyboard import KeyboardButton
|
||||||
|
from aiogram.dispatcher import FSMContext
|
||||||
|
from aiogram.types.input_file import InputFile
|
||||||
|
|
||||||
|
from .models import date, email_address, phone_number, iso_datetime, max_comment_length, ReplyKeyboardMarkup, Category, Subcategory, Executor, TimeRange, FreeToOrder, Order, Issue
|
79
models/models.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from aiogram.types.reply_keyboard import ReplyKeyboardMarkup as _ReplyKeyboardMarkup
|
||||||
|
from datetime import date, time, datetime, timedelta
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import re
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
email_address = re.compile(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)')
|
||||||
|
phone_number = re.compile(r'\D*')
|
||||||
|
iso_datetime = re.compile(r'(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).(\d{0,6})([+-]?\d{0,2}:?\d{0,2})')
|
||||||
|
max_comment_length = 1024
|
||||||
|
|
||||||
|
|
||||||
|
class ReplyKeyboardMarkup(_ReplyKeyboardMarkup):
|
||||||
|
def __init__(self, keyboard=None):
|
||||||
|
super().__init__(
|
||||||
|
keyboard=keyboard,
|
||||||
|
resize_keyboard=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Category(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class Subcategory(BaseModel):
|
||||||
|
category: Category
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class Executor(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRange(BaseModel):
|
||||||
|
id: int
|
||||||
|
start_time: time
|
||||||
|
end_time: time
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
|
||||||
|
class FreeToOrder(BaseModel):
|
||||||
|
date: date
|
||||||
|
time_range: TimeRange
|
||||||
|
executor: Executor
|
||||||
|
busy_interval: timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class Order(BaseModel):
|
||||||
|
id: int
|
||||||
|
subcategory: Subcategory
|
||||||
|
date: date
|
||||||
|
time_range: TimeRange
|
||||||
|
executor: Executor
|
||||||
|
telegram_id: int
|
||||||
|
email_address: str
|
||||||
|
phone_number: str
|
||||||
|
comment: str
|
||||||
|
start_time: datetime = None
|
||||||
|
end_time: datetime = None
|
||||||
|
|
||||||
|
|
||||||
|
class Issue(BaseModel):
|
||||||
|
id: int
|
||||||
|
key: str
|
||||||
|
status: Literal[
|
||||||
|
'undefined',
|
||||||
|
'new',
|
||||||
|
'indeterminate',
|
||||||
|
'done',
|
||||||
|
]
|
||||||
|
telegram_id: int
|
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
aiogram
|
||||||
|
jira
|
||||||
|
psycopg[binary]
|
||||||
|
pydantic
|
BIN
static/images/1.1.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/images/1.2.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/images/1.3.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
static/images/2.1.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
static/images/2.2.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/images/2.3.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
static/images/2.4.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
static/images/3.1.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
static/images/3.2.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
static/images/3.3.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
static/images/3.4.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/images/3.5.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
static/images/4.1.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/images/4.2.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/4.3.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
static/images/4.4.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
static/images/4.5.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/4.6.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
static/images/4.7.png
Normal file
After Width: | Height: | Size: 6.2 KiB |