Привет, Хабрахабр! Готовое архитектурное решение для мобильных устройств, включая 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': #'Инженерия ПО'}}}
Подписка, но не на уведомления, что вполне может оказаться полезной фичей в будущем при актуальности фреймворка, а на получение расписания всего по одному клику.
По причине того, что архитектура обязывает нажимать несколько кнопок и видеть перед собой столько же экранов и что-то выбирать, данный функционал крайне полезен — пользователь должен один раз подписаться на определенную группу и больше ему не придется «париться».
Код на Swift
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) — цифре, обозначающей порядковый номер меню, на котором пользователь «застрял», значит, вернуться надо на индексную позицию ноль (1-1=0). И еще раз: индекс — какое меню предпоследнее, позиция пользователя — какое меню сейчас. То, как вы отображаете меню и где вы его храните, — дело вашего приложения, но получение позиции — уже работа фреймворка.
Заключительная часть архитектуры — статистика, здесь ничего сложного, но много полезного. Например, вы можете легко вести детальную статистику вашего приложения — записывать количество выбранного пользователями факультета, а потом с легкостью получать данную цифру и отображать в какую-нибудь админ-панель.
Код на Swift
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")
}
}
Спасибо
Надеюсь, что вы не только оценили мой подход к описанию проделанной работы и поток мыслей в общем, но и проявили более глубокий интерес. А если вас увлекло полностью, буду рад ответить на ваши вопросы или помочь с разработкой со своей стороны.
Поделиться с друзьями
dotter
А на скриншотах расписание из родного ГУТ :)