Pull to refresh

О том, как писал проект для личного пользования, но в итоге попал на конкурс

Оглавление


Этот пост будет о написании проекта сначала для личного пользования, а через некоторое время для более крупной аудитории(про более крупную аудиторию, если будет интересно, напишу следующим постом).

Предыстория


В году 2018, я захотел изучить Python. Это если кратко. Но было не интересно начинать пост с краткой предыстории. Поэтому вот, держите. Начал я листать всякие паблики ВК на тему программирования. И почему-то мне очень часто попадались посты про написание ботов. Именно с них я и начал обучение.

Это был канун Нового года. В школах РБ обычно меняется школьное расписание во втором полугодии. В добавок к этому, у нас школа очень большая, на столько, что сложно запомнить в каком кабинете у нас уроки. Да и расписание запоминать не хотелось. Именно тогда пришла идея для написания проекта.

Начало проекта


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

  • Добавление школьного расписания в БД
  • Получение школьного расписания на день
  • Получение информации о текущем уроке
  • Получение информации о следующем уроке

Разработка


Как и в большинстве Telegram ботов, я решил использовать библиотеку PyTelegramBotAPI. А для хранения использовал обычную для локальных проектов СУБД SQLite. Тогда я не знал о правилах использования БД через чистый Python, только из Django. Поэтому написал кривую библиотеку для взаимодействия с базой.

Интерфейс взаимодействия с БД
import sqlite3

class DataBase:
    def __init__(self, db):
        self.con = sqlite3.connect(db, check_same_thread=False)

# Выполняем все запросы кроме SELECT
    def query(self, sql):
        cursor = self.con.cursor()
        cursor.execute(sql)
        self.con.commit()
        return cursor

# Выбираем одну запись по запросу sql
    def find_one(self, sql, ret=True):
        cursor = self.con.cursor()
        cursor.execute(sql)
        data = cursor.fetchone()
        if (data is None) or (len(data)==0):
            return False
        else:
            if ret:
                return data
            else:
                return True

# Выбираем все записи по запросу sql
    def find_all(self, sql, ret=True):
        cursor = self.con.cursor()
        cursor.execute(sql)
        data = cursor.fetchall()
        if len(data) == 0:
            return False
        else:
            if ret:
                return data
            else:
                return True

    def __del__(self):
        self.con.close()


Теперь, после написания интерфейса для БД, можно подумать о структуре нашей БД.

Структура БД




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

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

Таблица teachers хранит данные об учителях, их ФИО.

Таблица lessons хранит названия всех возможных уроков.

И наконец, самая главная таблица, schedule хранит само расписание уроков. Какой урок, кто его ведет, какой по счету урок и в какой день недели.

P.S.

Для упрощенной работы с БД пришлось написать еще одну библиотеку. Ее код под спойлером

Код библиотеки
# Имеется ли в пользователь с UID
def is_there(db, uid):
    sql = f'SELECT cid FROM users WHERE uid = {uid}'
    return db.find_one(sql, ret=False)

# Создание пользователя в БД
def create_user(db, uid, form, school, cid):
    sql = f'INSERT INTO users VALUES ({uid},\'{form}\',{school},{cid})'
    if not is_there(db, cid):
        db.query(sql)

# Из какой школы пользователь с UID
def what_school(db, cid):
    sql = f'SELECT * FROM users WHERE cid = {cid}'
    user = db.find_one(sql)
    return user[2]

# В каком классе учится пользователь с UID
def what_form(db, cid):
    sql = f'SELECT * FROM users WHERE cid = {cid}'
    user = db.find_one(sql)
    return user[1]

# Какой учитель имеет ID
def what_teach(db, id):
    sql = f'SELECT name,fathname,surname FROM teachers WHERE id = {id}'
    teacher = db.find_one(sql)
    return teacher[0]+' '+teacher[1]+' '+teacher[2]

# Какой предмет имеет ID
def what_lesson(db, id):
    sql = f'SELECT name FROM lessons WHERE id = {id}'
    lesson = db.find_one(sql)
    return lesson[0]

# Авторизован ли пользователь в сервисе
def is_authorized(db, cid):
    sql = f'SELECT * FROM users WHERE cid = {cid}'
    data = db.find_one(sql)
    if data is False:
        return False
    else:
        return True


Непосредственно бот


Для авторизации пользователей нам потребуется информация о его школе, классе.

Авторизация будет выполнятся командой

/ucreate <Номер класса> <Буква класса(на русском)> <номер школы>

Код функции авторизации:

Авторизация пользователя
@bot.message_handler(commands=['ucreate'])
def user_create_handler(message):
    # Авторизован ли пользователь?
    if not dbmeth.is_authorized(database, message.chat.id):
        dict = message.text.split()
        # Хватает ли данных для создания пользователя?
        if len(dict) < 4:
            # Отправляем сообщение с требуемым форматом сообщения?
            bot.send_message(message.chat.id, constants.frmt['ucreate'])
            return
        # Если все отлично, то добавляем пользователя в базу
        dbmeth.create_user(db=database,
                           uid=message.chat.id,
                           cid=message.chat.id,
                           form=str(dict[1])+str(dict[2]),
                           school=str(dict[3]))


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

Вот код добавления учителей и звонков:

Код
# Добавление учителей в базу
@bot.message_handler(commands=['tcreate'])
def teacher_create_handler(message):
    # FORMAT OF MESSAGE
    # /tcreate <name> <fathname> <surname>

    if not dbmeth.is_authorized(database, message.chat.id):
        bot.send_message(message.chat.id, constants.autherr)
        return

    school = dbmeth.what_school(database, message.chat.id)
    words = message.text.split()

    if len(words) < 4:
        bot.send_message(message.chat.id, constants.frmt['tcreate'])
        return

    name = math.upper_first(words[1])
    surname = math.upper_first(words[3])
    fathname = math.upper_first(words[2])

    sql = f'INSERT INTO teachers (name, surname, fathname, school) VALUES (\'{name}\',\'{surname}\',\'{fathname}\',' \
          f'{school}) '

    database.query(sql)

# Добавление расписания звонков
@bot.message_handler(commands=['badd'])
def bell_add(message):
    # FORMAT OF MESSAGE
    # /badd <number of lesson> <begin time> <end time>
    # FORMAT OF TIME
    # hh:mm

    if not dbmeth.is_authorized(database, message.chat.id):
        bot.send_message(message.chat.id, constants.autherr)
        return

    words = message.text.split()

    if len(words) < 4:
        bot.send_message(message.chat.id, constants.frmt['badd'])
        return

    sql = f'SELECT * FROM users WHERE cid = {message.chat.id}'
    user = database.find_one(sql)
    school = user[2]

    begin = math.time_to_min(words[2])
    end = math.time_to_min(words[3])

    if int(words[1]) > 12 and int(words[1]) > 0:
        bot.send_message(message.chat.id, constants.err3)

    sql = f'INSERT INTO bells VALUES ({int(school)*100+int(words[1])},{begin},{end})'

    database.query(sql)



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

А вот код для этого:

Код добавления расписания уроков в базу
# Добавление всех предметов 
@bot.message_handler(commands=['ladd'])
def add_lessons(message):
    # FORMAT OF MESSAGE
    # /ladd <order> <day> <lesson_id> <teacher1_id> <teacher2_id> <teacher3_id>

    if not dbmeth.is_authorized(database, message.chat.id):
        bot.send_message(message.chat.id, constants.autherr)
        return

    words = message.text.split()

    if len(words) < 5:
        bot.send_message(message.chat.id, constants.frmt['ladd'])
        return

    school = dbmeth.what_school(database, message.chat.id)
    form = dbmeth.what_form(database, message.chat.id)
    order = int(words[1])

    if not (0 < order <= 12):
        return

    day = int(words[2])

    if not (0 < day <= 7):
        return

    lessons_id = int(words[3])
    teacher1_id = int(words[4])
    teacher2_id = 0
    teacher3_id = 0
    if len(words)>=6:
        teacher2_id = int(words[5])

    if len(words)==7:
        teacher3_id = int (words[6])

    sql1 = f'SELECT id FROM lessons WHERE id = {lessons_id}'
    sql2 = f'SELECT id FROM teachers WHERE id = {teacher1_id}'
    sql3 = f'SELECT id FROM teachers WHERE id = {teacher2_id}'
    sql4 = f'SELECT id FROM teachers WHERE id = {teacher3_id}'

    if not (database.find_one(sql1) is False) and not (database.find_one(sql2) is False) and \
    (not (database.find_one(sql3) is False) or teacher2_id == 0) and \
    (not (database.find_one(sql4) is False) or teacher3_id == 0):
        sql = f'INSERT INTO schedule (ord, lesson_id, teacher1_id, teacher2_id, teacher3_id, school, form, day) VALUES' \
              f'({school*100+int(order)},{lessons_id},{teacher1_id},{teacher2_id},{teacher3_id},{school},\'{form}\',{day})'
        database.query(sql)



База заполнена, что после этого?


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

Из всех команд самой простой в реализации была функция получения информации о текущем уроке. Для этого мы можем просто искать совпадение в расписании звонков с текущим временем. Собственно это и делает следующая функция:

/snow - информация о текущем уроке
# Получение информации о текущем уроке
@bot.message_handler(commands=['snow'])
def current_lesson(message):
    # Проверка, авторизован ли пользователь?
    if not dbmeth.is_authorized(database, message.chat.id):
        bot.send_message(message.chat.id, constants.autherr)
        return

    # Получение расписание звонков конкретной школы
    time = math.now()
    school = dbmeth.what_school(database, message.chat.id)
    print(time)
    sql = f'SELECT id FROM bells WHERE begin <= {time} AND end >= {time} AND id/100 = {school}'

    data = database.find_one(sql)
    print(data)

    # Если наше время не подходит в диапазон наших звонков
    if data is False:
	# Урочное время?
        if 480 <= time <= 1170:
	    # Если да, то сейчас перемена
            bot.send_message(message.chat.id, constants.breaks)
        else:
	    # Если нет, то уже учебный день уже закончился(или еще не начался)
            bot.send_message(message.chat.id, constants.endofday)
    else:
	# Иначе мы получили номер урока, остается только найти информацию о нем
        day = math.today()
        order = data[0] % 100
        print(order)
        print(day)
        form = dbmeth.what_form(database, message.chat.id)
        sql = f'SELECT * FROM schedule WHERE day = {day} AND ord = {data[0]} AND form = \'{form}\''
        print(sql)
        data = database.find_one(sql)
        answer = ""
	# Если в расписании уроков, нет урока с таким номером
        if data is False:
	    # Значит можно отдыхать
            bot.send_message(message.chat.id, 'Rest')
        else:
	    # Иначе выдаем информацию о нем
            lesson = dbmeth.what_lesson(database, data[1])
            teacher1 = dbmeth.what_teach(database, data[4])
            teachers = [teacher1]
            if data[5]!=None and int(data[5])!=0:
                teachers.append(dbmeth.what_teach(database, data[5]))
            if data[6]!=None and int(data[6])!=0:
                teachers.append(dbmeth.what_teach(database, data[6]))
            answer = generate_answer(order, lesson, teachers, day-1)
            bot.send_message(message.chat.id, answer)
            log(message, answer)


Но, если увидев этот код, вы подумали: «Если это было самое простое, что тогда сложное?», то я вам просто покажу код следующей функции.

/snext - информация о следующем уроке
# Обработчик самой команды ботом
@bot.message_handler(commands=['snext'])
def next_lesson(message):

    if not dbmeth.is_authorized(database, message.chat.id):
        bot.send_message(message.chat.id, constants.autherr)
        return

    time = math.now()
    school = dbmeth.what_school(database, message.chat.id)
    day = math.today()
    form = dbmeth.what_form(database, message.chat.id)

    # Получение всех звонков после текущего времени
    sql = f'SELECT id FROM bells WHERE begin > {time} AND id/100 = {school} ORDER BY id ASC'
    data = database.find_one(sql)

    # Если конец дня и звонков на сегодня больше нет
    if data is False:
	# Вывод первого урока следующего дня
        next_day_lesson(message)
    else:
	# Получаем все уроки которые идут после текущего времени
        order = data[0]
        sql = f'SELECT * FROM schedule WHERE day = {day} AND school = {school} AND ord = {order} AND' \
              f' form = \'{form}\''
        data = database.find_one(sql)
        answer = ""
	# Если таких нет
        if data is False:
	    # Выводим первый урок следующего дня
            next_day_lesson(message)
        else:
	    # Выводим первый урок, который нам подходит
            lesson = dbmeth.what_lesson(database, data[1])
            teacher1 = dbmeth.what_teach(database, data[4])
            teachers = [teacher1]
            if data[5]!=None and int(data[5])!=0:
                teachers.append(dbmeth.what_teach(database, data[5]))
            if data[6]!=None and int(data[6])!=0:
                teachers.append(dbmeth.what_teach(database, data[6]))
            answer = generate_answer(order%100, lesson, teachers, day-1)
            bot.send_message(message.chat.id, answer)
            log(message, answer)


Почитав вышеописанный код, может возникнуть вполне резонный вопрос, а где функция next_day_lesson()?

А вот и она:

Код
def next_day_lesson(msg):
    day = math.today()
    school = dbmeth.what_school(database, msg.chat.id)
    form = dbmeth.what_form(database, msg.chat.id)

    sql = f'SELECT * FROM schedule WHERE day > {day} AND school = {school} AND form = \'{form}\'' \
          f' ORDER BY ord ASC, day ASC'

    resp = database.find_one(sql)
    # Есть ли уроки после текущего дня?
    if resp is False:
	# Нет, значит выводим первый в расписании урок первого дня
        sql = f'SELECT * FROM schedule WHERE day = 1 AND school = {school} AND form = \'{form}\'' \
              f'ORDER BY ord ASC'
        resp = database.find_one(sql)

        
        lesson = dbmeth.what_lesson(database, resp[1])
        teacher1 = dbmeth.what_teach(database, resp[4])
        teachers = [teacher1]
        if resp[5]!=None and int(resp[5])!=0:
            teachers.append(dbmeth.what_teach(database, resp[5]))
        if resp[6]!=None and int(resp[6])!=0:
            teachers.append(dbmeth.what_teach(database, resp[6]))
        order = resp[3]
        answer = generate_answer(int(order)%100, lesson, teachers, 0)
        bot.send_message(msg.chat.id, answer)
        log(msg, answer)
    else:
	# Иначе, просто выводим первый урок следующих дней
        lesson = dbmeth.what_lesson(database, resp[1])
        teacher1 = dbmeth.what_teach(database, resp[4])
        teachers = [teacher1]
        if resp[5]!=None and int(resp[5])!=0:
            teachers.append(dbmeth.what_teach(database, resp[5]))
        if resp[6]!=None and int(resp[6])!=0:
            teachers.append(dbmeth.what_teach(database, resp[6]))
        order = resp[3]
        answer = generate_answer(int(order)%100, lesson, teachers, int(resp[8])-1)
        bot.send_message(msg.chat.id, answer)
        log(msg, answer)


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

Хорошо, бота мы написали, но при чем тут конкурс?


Все просто, у нас есть ежегодный конкурс от БРСМ (союз молодежи). В котором можно представлять проекты, как связанные с высокими технологиями, так и другие. Но все они должны быть ориентированы на принесение обществу пользы.

Именно таким, я посчитал свой проект. Поэтому я и решил поучаствовать в конкурсе.
О самом конкурсе, подробно я рассказывать не буду. Ибо мне не очень повезло там. Но на этом останавливаться не будем.

Впрочем, так была написана первая версия проекта. Что было дальше с этим проектом я напишу в следующем посте, если вам будет интересно это.

Выводы


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

Вот и все что я хотел вам рассказать.

Если будут какие-то вопросы о самом проекте, можно писать мне в ВК
vk.com/vova_velikovich. Постараюсь удовлетворить вас ответами на ваши вопросы.

Спасибо за внимание!
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.
Change theme settings