Привет, Хабрахабр! Готовое архитектурное решение для мобильных устройств, включая iOS, Android, Telegram-bots, а также платформы, поддерживающие обработку http-запросов, выступающее в роли пет-проекта автора статьи, будет интересно желающим реализовать «карманное» расписание занятий для своих университетов и школ.
Содержание публикации:
- Что предшествовало созданию фреймворка.
- Проблемы программистов, которые решаются с «Rutetider».
- Детали архитектурной структуры инструмента.
- О компонентах, являющихся основным каркасом, и модулях, улучшающих разработку, а также разнообразные примеры.
Введение
Для того, чтобы внести свою лепту в сообщество open-source по большей части и в меньшей — чтобы решить проблему недоступности расписания занятий университета на мобильных устройствах (по правде говоря, доступности, но крайне неадаптивной и «долгой») — пришлось воспользоваться самой лучшей возможностью — написать Telegram-bot`а (если интересно — статья на Хабрахабре), а чтобы решить проблему не только для своего университета — небольшой фреймворк.
Было принято базировать фреймворк на первом решении, с теми же инструментами, что и для бота, но не исключать возможности разработки на платформах, напрямую поддерживающих целостность мобильных приложений, — iOS, Android, да и в общем-то на любых других платформах (веб-приложение с адаптивной версткой под телефоны, к примеру).
Проще говоря, определилось два вида доступа к функционалу — REST-API и Python-библиотека для программистов, использующих непосредственно Python.
А еще Rutetider
Это набор методов и инструментов, базирующихся на шаблонной последовательности, которые позволят создать, возможно, не гибкое, но безусловно рабочее приложение. В первую очередь — это решение «здесь и сейчас»; если главной целью стоит развитие — напишите все с нуля самостоятельно и не используйте фреймворк.
Еще одним позитивным моментом можно выделить доступную документацию, наполненную не только объяснениями работы, но и иллюстрациями и инструкциями, значительно ускоряющими понимание и разработку.
Архитектура фреймворка
Основной принцип
Как упоминалось выше, очень трудно, не имея большого опыта программирования в целом, определить правильную и красивую структуру, поэтому пришлось упереться во что-то шаблонное, но со своими плюсами — достаточно очевидное и рабочее.
Если говорить от лица пользователя, то ему будет необходимо пройти ряд экранов: выбор главной опции (возможность получить расписание) среди множества возможных других, факультета, курса, затем группы и непосредственно даты (также среди множества других полезных фич).
Из схемы должно быть видно, что самому программисту нужно «ловить» расположение пользователя и отображать необходимые меню с разным контентом, а также вести статистику, если стоит такое условие.
Подробнее о необходимых методах
Чтобы не отрываться от контекста, продолжим со знакомого — записывать позицию пользователя необходимо на платформах без возможности использования какого-нибудь локального хранилища (как, например, телефон пользователя), потому что кнопка «Вернуться назад» сама по себе не знает куда возвращаться, ей нужно «скормить» эту же позицию. Еще один пример — знать, какие все-таки данные вводит студент, чтобы потом определить по факультету и курсу группу, а по группе выбрать расписание на соответствующую дату.
Кроме того, программист может рассчитывать на удобную работу с датами на сегодняшний и завтрашний день, то есть присутствует возможность как внести точные и актуальные значения, так и получить.
Пока остановились на внесении данных, стоит упомянуть, что фреймворк располагает методами, готовыми помочь дополнительно структурировать информацию о парах в университетах – от аудитории и времени до данных преподавателя.
Держите пример добавления параметров лекций:
from rutetider import Timetable timetable = Timetable(database_url) timetable.add_lesson('IT', '3', 'PD-31', '18.10', 'Литература', '451', '2', 'Шевченко Т.Г.') # params: faculty, course, group_name, lesson_date, lesson_title, # lesson_classroom, lesson_order, lesson_teacher
Я все еще не понимаю, как это работает
Я постарался добавить немного модульности в инструменты, чтобы некоторые платформы могли не использовать ненужный функционал, но с обратной стороны «сковал» наручниками каждого желающего использовать «Rutetider» — наличие сервера (скорее всего) и базы данных.
Необходимость в создании базы данных вызвана тем, что у автора недостаточно ресурсов для обеспечения каждого свободным местом для его расписания и прочей ценной информации, поэтому программисту придется оформлять собственную PostgreSQL и поделиться ссылкой на доступ (к счастью, бесплатных возможностей много, про одну из них я рассказываю здесь).
А вот поиск сервера, возможно, кому-то и не потребуется, но точно будет необходимым тем, чей университет обновляет расписание каждый день или каждую неделю — в этом случае создание инструмента для внесения расписания посредством парсера, чтения CSV или любого удобного способа — обязательный пункт.
И здесь нам всем здорово повезло, потому что общество информационных технологий поддерживает разработчиков: Heroku Cloud Platform для Python, Java, Node.js и Firebase, Parse, Polljoy — iOS (автор не использовал большинство предложений; если у вас есть дополнения или замечания на этот счет — сообщите).
На какой функционал можно рассчитывать
Лекции и пары — компонент общей структуры, отвечающий за работу с обработкой занятий. Если пример с добавлением пар вы видели, то посмотрите их получение.
schedule = timetable.get_lessons('PD-31', '18.10') # params: group_name, lesson_date print(schedule) # {'lessons': { # '3': {'lesson_teacher': 'Шевченко О.В.', 'lesson_classroom': # '451', 'lesson_order': '3', 'lesson_title': 'Литература'}, # '1': {'lesson_teacher': 'Шульга О.С.', 'lesson_classroom': '118', # 'lesson_order': '1', 'lesson_title': #'Математика'}, # '2': {'lesson_teacher': 'Ковальчук Н.О.', 'lesson_classroom': '200', # 'lesson_order': '2', 'lesson_title': #'Инженерия ПО'}}}
Подписка, но не на уведомления, что вполне может оказаться полезной фичей в будущем при актуальности фреймворка, а на получение расписания всего по одному клику.
По причине того, что архитектура обязывает нажимать несколько кнопок и видеть перед собой столько же экранов и что-то выбирать, данный функционал крайне полезен — пользователь должен один раз подписаться на определенную группу и больше ему не придется «париться».
import UIKit class ViewController: UIViewController { fileprivate let databaseURL = "postgres://nwritrny:VQJnfVmooh3S0TkAghEgA--YOxoaPJOR@stampy.db.elephantsql.com:5432/nwritrny" fileprivate let apiURL = "http://api.rutetiderframework.com" @IBAction func subscribeAction(_ sender: Any) { let headers = ["content-type": "application/x-www-form-urlencoded"] let postData = NSMutableData(data: "url=\(databaseURL)".data(using: .utf8)!) postData.append("&user_id=1251252".data(using: .utf8)!) postData.append("&group_name=PD-3431".data(using: .utf8)!) let request = NSMutableURLRequest(url: NSURL(string: "\(apiURL)/subscribers/add_subscriber")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "PUT" request.allHTTPHeaderFields = headers request.httpBody = postData as Data let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse) } }) dataTask.resume() } @IBAction func getSubscriptionInfoAction(_ sender: Any) { let headers = ["content-type": "application/x-www-form-urlencoded"] let postData = NSMutableData(data: "url=\(databaseURL)".data(using: .utf8)!) postData.append("&user_id=1251252".data(using: String.Encoding.utf8)!) let request = NSMutableURLRequest(url: NSURL(string: "\(apiURL)/subscribers/get_subscriber_group")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "POST" request.allHTTPHeaderFields = headers request.httpBody = postData as Data let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error) } else if let jsonData = data { do { let json = try JSONSerialization.jsonObject(with: jsonData) as? Dictionary<String, Any> print(json?["group"]) } catch let error{ print(error) } } }) dataTask.resume() } }
Текущие даты с возможностью внесения и получения расписания на сегодняшний и завтрашний день.
import requests import json api_url = 'http://api.rutetiderframework.com' database_url = 'postgres://nwritrny:VQJnfVmooh3S0TkAghEgA--YOxoaPJOR@stampy.db.elephantsql.com:5432/nwritrny' # Это тестовый параметр, в запросе должна быть ссылка на вашу рабочую базу данных r = requests.post(api_url + '/currentdates/', data=json.dumps({ 'url': database_url}), headers={'content-type': 'application/json'}) print(r.status_code) # 200 # Если вы работаете с компонентом впервые, вам необходимо проинициализировать необходимые таблицы, # то есть вызвать соответсвующий метод. r = requests.put('http://api.rutetiderframework.com/currentdates/add_current_dates', data=json.dumps({ 'url': database_url, 'today': '07.04', 'tomorrow': '08.04'}), headers={'content-type': 'application/json'}) r = requests.post('http://api.rutetiderframework.com/currentdates/get_current_dates', data=json.dumps({ 'url': database_url}), headers={'content-type': 'application/json'}) print(r.json()) # {'dates': ['07.04', '08.04']}
Важным, но не менее сложным для начального понимания, пунктом является позиция пользователя — из-за невозможности использования встроенных или других удобных средств.
Например, если пользователь выбирает группу, то нам необходимо знать, какой выбор пользователь уже сделал (факультет и курс), а если он ошибся курсом — то среагировать на нажатие кнопки «Вернуться назад».
@bot.message_handler(func=lambda mess: 'Вернуться назад' == mess.text, content_types=['text']) def handle_text(message): user_position = UserPosition(database_url).back_keyboard(str(message.chat.id)) if user_position == 1: UserPosition(database_url).cancel_getting_started(str(message.chat.id)) keyboard.main_menu(message) if user_position == 2: UserPosition(database_url).cancel_faculty(str(message.chat.id)) keyboard.get_all_faculties(message) if user_position == 3: UserPosition(database_url).cancel_course(str(message.chat.id)) faculty = UserPosition(database_url).verification(str(message.chat.id)) if faculty != "Загальні підрозділи" and faculty != 'Заочне навчання': keyboard.stable_six_courses(message) if faculty == "Загальні підрозділи": keyboard.stable_one_course(message) if faculty == "Заочне навчання": keyboard.stable_three_courses(message) if user_position == 4: UserPosition(database_url).cancel_group(str(message.chat.id)) faculty, course = UserPosition(database_url).get_faculty_and_course(str(message.chat.id)) groups_list = Timetable(database_url).get_all_groups(faculty, course) groups_list.sort() keyboard.group_list_by_faculty_and_group(groups_list, message)
Возвращение на одно меню назад реализовывается немного сложнее, поэтому давайте разберем это на схеме.
Чтобы знать, какое меню необходимо пользователю, если он хочет вернуться назад, нам нужно воспользоваться методом «back_keyboard», который подскажет, на какой позиции остановился пользователь. Из схемы видно, что позиция равна единице (1) — цифре, обозначающей порядковый номер меню, на котором пользователь «застрял», значит, вернуться надо на индексную позицию ноль (один минус один равно ноль). И еще раз: индекс — какое меню предпоследнее, позиция пользователя — какое меню сейчас. То, как вы отображаете меню и где вы его храните, — дело вашего приложения, но получение позиции — уже работа фреймворка.
Заключительная часть архитектуры — статистика, здесь ничего сложного, но много полезного. Например, вы можете легко вести детальную статистику вашего приложения — записывать количество выбранного пользователями факультета, а потом с легкостью получать данную цифру и отображать в какую-нибудь админ-панель.
func initializeDatabase() { let request = NSMutableURLRequest(url: NSURL(string: "\(apiURL)/statistics/")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "POST" request.allHTTPHeaderFields = headers let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: callback) dataTask.resume() } func addStatistic() { let body = ["url": databaseURL, "user_id": "1251252", "point": "faculty", "date": "06.04.2017"] var jsonBody: Data? do { jsonBody = try JSONSerialization.data(withJSONObject: body) } catch { } let request = NSMutableURLRequest(url: NSURL(string: "\(apiURL)/statistics/add_statistics")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "PUT" request.allHTTPHeaderFields = headers request.httpBody = jsonBody let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: callback) dataTask.resume() } func getStatistic() { let body = ["url": databaseURL, "user_id": "1251252"] var jsonBody: Data? do { jsonBody = try JSONSerialization.data(withJSONObject: body) } catch { } let request = NSMutableURLRequest(url: NSURL(string: "\(apiURL)/statistics/get_statistics_general")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "POST" request.allHTTPHeaderFields = headers request.httpBody = jsonBody let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: callback) dataTask.resume() } func callback(_ data: Data?, _ resp: URLResponse?, _ error: Error?) { printResponse(resp, error: error) parseResponse(data) } func parseResponse(_ data: Data?) { if let jsonData = data { do { let json = try JSONSerialization.jsonObject(with: jsonData) as? Dictionary<String, Any> print(json ?? "json is nil") } catch let error{ print(error) } } } func printResponse(_ response: URLResponse?, error: Error?) { if (error != nil) { print(error!) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse ?? "response is nil") } }
Спасибо
Надеюсь, что вы не только оценили мой подход к описанию проделанной работы и поток мыслей в общем, но и проявили более глубокий интерес. А если вас увлекло полностью, буду рад ответить на ваши вопросы или помочь с разработкой со своей стороны.
ссылка на оригинал статьи https://habrahabr.ru/post/326222/
Добавить комментарий