Разработан чат-бот
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 |