When GitHub shoots you in the head, a new framework is created. The idea, concept and implementation of Rutetider


    Hi, Habrahabr! A ready-made architectural solution for mobile devices, including iOS , Android , Telegram-bots , as well as platforms that support the processing of http-requests , acting as a pet-project of the author of the article, will be interesting for those wishing to implement a “pocket” class schedule for their universities and schools.

    Content of publication:

    • What preceded the creation of the framework.
    • Problems of programmers who are solved with "Rutetider".
    • Details of the architectural structure of the instrument.
    • About the components that are the main framework, and modules that improve development, as well as a variety of examples.

    Introduction


    In order to contribute to the open-source community, for the most part and to a lesser extent, to solve the problem of the unavailability of the university’s timetable on mobile devices (in truth, accessibility, but extremely non-adaptive and “long”), I had to use the best opportunity - write a Telegram bot (if it’s interesting - an article on Habrahabr ), and to solve the problem not only for your university - a small framework.


    It was decided to base the framework on the first solution, with the same tools as for the bot, but do not exclude the possibility of development on platforms that directly support the integrity of mobile applications - iOS, Android, and, in general, on any other platforms (web an application with adaptive layout for phones, for example).

    Simply put, two types of access to the functionality were defined - the REST-API and the Python library for programmers using Python directly.

    And also Rutetider


    This is a set of methods and tools based on a template sequence that will allow you to create a possibly not flexible, but certainly a working application . First of all, it is the decision “here and now” ; if the main goal is development - write everything from scratch yourself and do not use the framework.

    Another positive point is the available documentation , filled not only with explanations of the work, but also with illustrations and instructions, which greatly accelerate understanding and development.

    Framework architecture


    The basic principle


    As mentioned above, it is very difficult, without a lot of programming experience in general, to determine the correct and beautiful structure, so I had to run into something template, but with its advantages it was quite obvious and working.


    Speaking on behalf of the user, he will need to go through a number of screens: selecting the main option (the ability to get a schedule ) among the many possible others, faculty , course , then the group and the date itself (also among many other useful features).

    It should be seen from the diagram that the programmer himself needs to “catch” the user's location and display the necessary menus with different content, as well as keep statistics if such a condition exists.


    Learn more about the required methods.


    In order not to be out of context, let's continue with a friend - it’s necessary to record the user's position on platforms without the possibility of using any local storage (such as the user's phone), because the “Go Back” button itself does not know where to return, it needs to "Feed" the same position. Another example is to know what kind of data a student enters, in order to later determine the group by faculty and course, and select the schedule for the corresponding date by group.

    In addition, the programmer can count on convenient work with dates for today and tomorrow, that is, there is the opportunity to both make accurate and relevant values, and get.

    While we’ve stopped on the introduction of data, it is worth mentioning that the framework has methods ready to help further structure the information about the pairs in universities - from the audience and time to the data of the teacher.

    Keep an example of adding lecture options:

    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
    

    I still don't understand how it works


    I tried to add a bit of modularity to the tools so that some platforms could not use unnecessary functionality, but on the reverse side I handcuffed everyone who wanted to use Rutetider - the presence of a server (most likely) and a database.

    The need to create a database is caused by the fact that the author does not have enough resources to provide everyone with free space for his schedule and other valuable information, so the programmer will have to draw up his own PostgreSQL and share a link to access (fortunately, there are a lot of free features, about one of them I telling here ).





    But searching for a server may not be necessary for someone, but it will certainly be necessary for those whose university updates the schedule every day or every week - in this case, creating a tool for scheduling by parser, reading CSV or any convenient method is a must .

    And here we are all very lucky because the information technology society supports developers: Heroku Cloud Platform for Python, Java, Node.js and Firebase , Parse , Polljoy - iOS (the author did not use most of the suggestions; if you have any additions or comments on this account - inform).

    What functionality can you count on


    Lectures and pairs - a component of the overall structure responsible for working with the processing of classes. If you saw an example with the addition of pairs, then look at their receipt.

    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': #'Инженерия ПО'}}}
    

    Subscription , but not for notifications, which may turn out to be a useful feature in the future with the relevance of the framework, but to receive a schedule with just one click.


    Due to the fact that the architecture requires you to press several buttons and see as many screens in front of you and choose something, this functionality is extremely useful - the user must subscribe to a specific group once and no longer have to “take a steam bath”.

    Swift Code
    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
                        print(json?["group"])
                    } catch let error{
                        print(error)
                    }
                }
            })
            dataTask.resume()
        }
    }
    


    Current dates with the ability to make and receive schedules for today and tomorrow.

    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']}
    

    An important, but no less difficult for an initial understanding, point is the user's position - due to the inability to use built-in or other convenient means.


    For example, if a user selects a group, then we need to know what choice the user has already made (faculty and course), and if he made a mistake, then respond to pressing the "Go back" button.

    @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)
    

    Going back one menu is a little more complicated, so let's look at it in the diagram.

    To know what menu the user needs, if he wants to go back, we need to use the “back_keyboard” method, which will tell what position the user has stopped at. It can be seen from the diagram that the position is equal to unity (1) - a digit denoting the serial number of the menu on which the user is "stuck", which means that you must return to the index position zero (1-1 = 0). And again: the index - which menu is the last but one, the user's position - which menu is now. How you display the menu and where you store it is the business of your application, but getting the position is already the work of the framework.

    The final piece of architecture is statistics., there’s nothing complicated, but a lot of useful things. For example, you can easily keep detailed statistics of your application - record the number of faculty selected by users, and then easily receive this figure and display it in some admin panel.

    Swift Code
    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
                    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")
            }
        }
    


    thanks


    I hope that you not only appreciated my approach to describing the work done and the stream of thoughts in general, but also showed a deeper interest. And if you are completely fascinated, I will be glad to answer your questions or help with the development on my part.

    Also popular now: