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