校園中的資訊系統

網頁的 / 手機的 / 會用的

呂紹榕 Louie Lu

我是

  • 呂紹榕
  • 國立高雄應用科技大學 資訊工程系 二年級
  • grapherd@gmail.com
  • http://grd.idv.tw

我是

  • 呂紹榕
  • 國立高雄應用科技大學 資訊工程系 二年級
  • grapherd@gmail.com
  • http://grd.idv.tw

Agenda

一些故事

使用的資源與技術

Q&A

第一個故事

校園中的資訊系統

同學們常用的系統

  • 數位學習平台

  • e-mail信箱

  • 校務行政資訊系統

  • 選課系統

  • 線上請假系統

  • 校車系統

  • ...etc

 

數位學習平台

http://ilearning.kuas.edu.tw

e-mail 信箱

http://mail.kuas.edu.tw

校務行政資訊系統

http://ap.kuas.edu.tw

選課系統

http://selcourse.kuas.edu.tw

線上請假審核系統

http://leave.kuas.edu.tw

校車系統

http://bus.kuas.edu.tw

大家的觀點來看
覺得這些系統如何

學生的觀點來看
覺得這些系統如何

使用者的觀點來看
覺得這些系統如何

系統容易操作嗎?

系統支援多瀏覽器嗎?

IE ONLY

IE ONLY

系統UI好看嗎?

CrossLink.tw

CrossLink.tw

校務行政資訊系統

時代向前了10年

校園中的資訊系統

卻停滯不前

WHY?

過了多久

我們才使用到新的校務系統?

整整一年

一年!!!

事後跟學校合作的時候

事後跟學校合作的時候

  • 人力不足
  • 排程很多
  • 很多新系統

事後跟學校合作的時候

  • 很多苦衷
  • 總之就是沒辦法弄

還有

They use ASP.NET

更重要的一點

產品開發已經變成更快速、更有彈性的流程,

明顯更好的產品不再是站在巨人的肩膀上打造出來

而是站在大量迭代(iterations)的肩膀上。

- Google模式 p30

遺憾的是,跟羅森柏格那個失敗的關卡式(Gate-based)產品開發架構一樣,現今許多公司採行的管理流程設計原理跟不上時代的趨勢變化 ...... ,這種方式旨在減緩速度,而它也的確非常有效地減緩速度,但這意味的是,當企業必須永久加速時,它們的架構卻會絆住它們,讓它們快不起來

- Google模式 p31

以海報為例

迭代

以海報為例

Very fast prototype

iterate, iterate, iterate

Each

Iterate

Get Responsive

From User

關卡

以海報為例
  • 提案
  • 層層上報
  • 審核
  • 層層審核
  • 討論
  • 層層討論
  • 開發
  • 開發開發開發
  • 檢查
  • 層層檢查
  • pre release
  • release

學校

很難聽到使用者的的需求

聽不到

就自己做吧

第二個故事

以開源軟體和行動裝置整合校園校務系統

HTML (with only IE)

JSON

HTML5

為什麼我們要做這個?

我正要成為

大一新鮮人的事情

IE Only

可是我用linux

那時候不會寫 APP,

就用 GreaseMonkey 解決這個問題

事情就這樣告一段落了

 

畢竟我的繳費單印完了,我也能不用 IE 就查成績

直到今年6月,

同學搞定Python登入的方式

import requests

AP_LOGIN_URL = "http://140.127.113.227/kuas/perchk.jsp"

def login(username, password):
    s = requests.session()
    data = {"uid": username, "pwd": password}
    
    r = s.post(AP_LOGIN_URL, data=data)
 
    return s

我們弄出了一個

API SERVER

API SERVER

  • /ap/login
  • /ap/query
  • /leave/query

有了API SERVER

還不夠滿足

Mobile First

使用者人數?

BUT

正常的, 要放假了

使用人數

@Google Play & App Store

所以,在校務通中,

我們用了什麼技術?

系統架構

中介系統

  • 無法修改原有系統
  • 寫死在 mobile app 不好
  • 靠中介系統處理資料

中介系統

中介系統

中介系統

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

中介系統

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

HTTP stuff

requests-python

requests.get("http://tao.kuas.cc")

requests.post("http://ap.kuas.edu.tw",
              data={"uid": "123",
                    "pwd": "123"}
              )

session = requests.Session()
session.get()
session.post()
session.cookies
def login(session, username, password):
    data = {"uid": username, "pwd": password}

    r = session.post(AP_LOGIN_URL, data=data, timeout=LOGIN_TIMEOUT)

    root = etree.HTML(r.text)

    try:
        is_login = not root.xpath("//script")[-1].text.startswith("alert")
    except:
        is_login = False


    return is_login
def login(session, username, password):
    data = {"uid": username, "pwd": password}

    r = session.post(AP_LOGIN_URL, data=data, timeout=LOGIN_TIMEOUT)

    root = etree.HTML(r.text)

    try:
        is_login = not root.xpath("//script")[-1].text.startswith("alert")
    except:
        is_login = False


    return is_login
<script language='javascript'>
    alert('[11] 無此帳號或密碼不正確,請重新輸入。');
    top.location.href='index.html';
</script>
@app.route('/ap/login', methods=['POST'])
@cross_origin(supports_credentials=True)
def login_post():
    if request.method == "POST":
        session.permanent = True

        # Start login
        username = request.form['username']
        password = request.form['password']
        
        s = requests.Session()
        is_login = function.login(s, username, password)

        if is_login:
            # Serialize cookies with domain 
            session['c'] = dump_cookies(s.cookies)
            session['username'] = username

            return "true"
        else:
            return "false"


    return render_template("login.html")
def dump_cookies(cookies_list):
    cookies = []
    for c in cookies_list:
        cookies.append({
            'name': c.name,
            'domain': c.domain,
            'value': c.value
            })

    return cookies

Multi domain cookies

def set_cookies(s, cookies):
    for c in cookies:
        s.cookies.set(
            c['name'], 
            c['value'], 
            domain=c['domain']
        )

Multi domain cookies

def dump_cookies(cookies_list):
    cookies = []
    for c in cookies_list:
        cookies.append({
            'name': c.name,
            'domain': c.domain,
            'value': c.value
            })

    return cookies

Authenticate

def authenticate(func):
    @wraps(func)
    def call(*args, **kwargs):
        if 'c' in session:
            return func(*args, **kwargs)
        else:
            return "false"

    return call

Before

Authenticate

@app.route("/ap/login_test")
def login_test():
    if 'c' not in session:
        return "false"

    return "You are login."

After

Authenticate

@app.route("/ap/login_test")
@authenticate
def login_test():
    return "You are login."
>>> import requests
>>> s = requests.Session()
>>> r = s.post("http://localhost:5000/ap/login", 
...             data={"username": "1102108133", "password": "111"}
...           )
>>> r.text
'true'
query_url = "http://140.127.113.227/kuas/%s_pro/%s.jsp?"

def query(session, qid=None, args=None):
    ls_random = random_number(session, RANDOM_ID)
    data = {"arg01": "", "arg02": "", "arg03": "",
                "fncid": "", "uid": "", "ls_randnum": ""}

    data['ls_randnum'] = ls_random
    data['fncid'] = qid

    for key in args:
        data[key] = args[key]

    try:
        content = session.post(
                      query_url % (qid[:2], qid), 
                      data=data, 
                      timeout=QUERY_TIMEOUT
                  ).content
    except requests.exceptions.ReadTimeout:
        content = ""

    return content
>>> r.text
'true'
>>> r = s.post("http://localhost:5000/ap/query", 
...            data={"fncid": "ag222", "arg01": "103", "arg02": "01"}
...        )
>>> r.text
<input type="button" class=button value="回上一頁" style="cursor:hand;height:20px" 
onclick="vbscript:history.go(-1)"><font style=\'font-style: normal; font-variant:
 normal; font-weight: normal; font-size: 9pt; font-family: 細明體\'>  學生:『<font
 color=\'blue\'>呂紹榕</font>』課表資料如下:</font><form name=thisform method=post ><
div align=center><font style="font-size: 9pt; font-family: 細明體" color="#000000">
【選 課 清 單】</font></div><table border="1" align="center"  cellspacing="0" cellpa
dding="4" width="100%"  bgcolor="#cccccc" bordercolor="#999999" bordercolordark="w
hite"><tr align=center bgcolor=\'#ebebeb\'><td align=\'left\' nowrap><font style=\
'font-style: normal; font-variant: normal; font-weight: normal; font-size: 9pt; font-family: 細明體\'>選課代號:</font></td><td

Parse raw data

def course(cont):
    """Parse raw kuas ap course data
    Return:
        parse data: json
        have_saturday: bool
        have_sunday: bool
        except_text: string
    """

    root = etree.HTML(cont)

    try:
        center = root.xpath("//center")[0]
        center_text = list(center.itertext())[0]
    except:
        center = ""
        center_text = ""


    # Return if no course data
    if center_text.startswith(u'學生目前無選課資料!'):
        return [[], False, False, center_text]


    tbody = root.xpath("//table")[-1]

    course_table = {}
    for r_index, r in enumerate(tbody[1:]):
        row = {}
        for index, c in enumerate(r.xpath("td")):
            r = list(filter(lambda x: x != u"\xa0", c.itertext()))
            
            if index == 0:
                row['time'] = r[0].replace(" ", "")
            else:
                row[index] = {
                        "course_name": "",
                        "course_teacher": "",
                        "course_classroom": ""
                    }

                if r:
                    while len(r) < 3:
                        r.append("")

                    row[index]["course_name_simple"] = r[0][:2]
                    row[index]["course_name"] = r[0]
                    row[index]["course_teacher"] = r[1]
                    row[index]["course_classroom"] = r[2]

        course_table[r_index] = row


    # Check if over 8th didn't have class
    token_b = False
    token_night = False


    for r in course_table:
        for c in course_table[r]:
            if r > 9 and c != "time" and course_table[r][c]['course_name']:
                if r == 10:
                    token_b = True
                else:
                    #print(course_table[r][c]['course_name'])
                    token_night = True

    #print(token_b, token_night)


    if token_night: 
        pass
    elif token_b:
        for i in [11, 12, 13, 14]:
            del course_table[i]
    else:
        for i in [10, 11, 12, 13, 14]:
            del course_table[i]

        

    # Check Saturday and Sunday class
    have_saturday = False
    have_sunday = False
    
    for r in course_table:
        if not isinstance(course_table[r], bool) and course_table[r][6]["course_name"]:
            have_saturday = True
        if not isinstance(course_table[r], bool) and course_table[r][7]["course_name"]:
            have_sunday = True


    return [course_table, have_saturday, have_sunday, False]
      
>>> r = s.post("http://localhost:5000/ap/query", 
...            data={"fncid": "ag222", "arg01": "103", "arg02": "01"}
...        )
>>> r.text
'[{"0": {"1": {"course_name": "", "course_teacher": "", "course_c...

Parse to json

流程

  1. 想辦法抓取原始資料
    • 有登入的地方要登入
    • 有其他加密?
    • Header
  2. 解析原始資料
    • HTML
    • JSON
    • ...etc
  3. 轉換成其他格式
    • JSON
    • YAML

POST?

e.g. Submit Leave or Submit Bus?

校車預定

session.post('http://bus.kuas.edu.tw/API/Reserves/add',
                data="{busId:"+ kid +"}",
                headers=headers,
                proxies=proxies
            )

JS Encryption?

Execjs - Python

Run js in python

def init(session):
    global js
    #session.get('http://bus.kuas.edu.tw/', headers=headers, proxies=proxies)
    session.head("http://bus.kuas.edu.tw")
    js = execjs.compile(
        js_function + session.get('http://bus.kuas.edu.tw/API/Scripts/a1', 
                headers=headers, 
                proxies=proxies
            ).content
        )
data['n'] = js.call('loginEncryption', str(uid), str(pwd))

Long response time

Make some cache

redis-python

  • Memory database
  • Key-Value
  • Fast
ap_query_key = qid + 
               hashlib.sha512(
                   str(username) + 
                   str(args) + 
                   SERECT_KEY
               ).hexdigest()
ag2221b8d45dd7722324855941fe59dc8c29c74f7a83a22518
     f54bea2d2b2cdcd61251121b7cb252d5d960b105757ec
     4a2d855a3906e0698e003a54080f5b628d1bd5
if not red.exists(ap_query_key):
    ap_query_content = parse.parse(qid, ap.query(session, qid, args))
    
    red.set(ap_query_key, json.dumps(ap_query_content))
    red.expire(ap_query_key, AP_QUERY_EXPIRE)
else:
    ap_query_content = json.loads(red.get(ap_query_key))

Mobile APP

Web app

  • No need native performance
  • Fast development

Apache Cordova

Bundle web to multi platform

  • Android
  • iOS
  • BlackBerry
  • Firefox extensions
  • Chrome extensions
  • ...etc

Prototype by JQM

因為 UI 關係

轉換到 Ionic UI framework

不同的螢幕大小

不同的 CSS Style

Responsive Web Design

Meda Queries

  • By Device Width
        - @media screen and (max-width: 320px)
     
  • By Device resolution
        - @media print and (min-resolution: 300dpi)
.course-table button {
    @extend .button-light;
    border: 0px;
    width: 100%;
}

.course-prompt-saturday, .course-prompt-sunday {
  margin-bottom: -13px;
}


@media screen and (max-width: 360px) {
  .course-table button {
    height: 40px;
    min-width: 40px;
  }

  .course-table-saturday {
    display: none;
  }
}

@media screen and (max-width: 430px) {
  .course-table-sunday {
    display: none;
  }
}

SASS

css with superpowers

變數

$link-color-blue: #0f0fff  
$link-font-size: 16px


.header a  
  color: $link-color-black
  font-size: $link-font-size

.menu a  
  color: $link-color-blue
  font-size: $link-font-size

計算

$link-color-blue: #0f0fff  
$link-font-size: 16px


.header a  
  color: $link-color-black
  font-size: $link-font-size * 2

.menu a  
  color: $link-color-blue
  font-size: $link-font-size * 0.8

@extend

.base-table {
    border-collapse: separate;
    border-spacing: 0;
    border: 2px solid #CCC;
    border-radius: 5px;
    -moz-border-radius: 5px;

    margin: 0px auto;
    margin-top: 20px;

    width: 90%;
}

@extend

.course-table {
    @extend .base-table

    width: 100%;
    font-size: 20px;
}

我們解決問題了嗎?

打造一個受歡迎的成功 app,最關鍵的三大部分是:

 

  • 問題:你的 app 真能解決人們在意的問題嗎?
  • 市場:有上述問題的使用者夠多嗎?
  • 產品:你的 app 真的有解決上述那群人所在意的問題嗎?

 - Perfectly executing the wrong plan  Tomer Sharon

http://www.inside.com.tw/2014/07/29/googles-6-reasons-why-people-do-not-use-your-app

還記得

標題「有用的」

SITCON

學生計算機年會

Live Demo

Q&A

Thanks!

Louie Lu

grapherd@gmail.com

Made with Slides.com