Когда GitHub выстреливает вам в голову, создается новый фреймворк. Идея, концепция и реализация «Rutetider»

от автора

Привет, Хабрахабр! Готовое архитектурное решение для мобильных устройств, включая 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': #'Инженерия ПО'}}} 

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

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

Код на Objective-C

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) — цифре, обозначающей порядковый номер меню, на котором пользователь «застрял», значит, вернуться надо на индексную позицию ноль (один минус один равно ноль). И еще раз: индекс — какое меню предпоследнее, позиция пользователя — какое меню сейчас. То, как вы отображаете меню и где вы его храните, — дело вашего приложения, но получение позиции — уже работа фреймворка.

Заключительная часть архитектуры — статистика, здесь ничего сложного, но много полезного. Например, вы можете легко вести детальную статистику вашего приложения — записывать количество выбранного пользователями факультета, а потом с легкостью получать данную цифру и отображать в какую-нибудь админ-панель.

Код на Objective-C

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *