├── .gitattributes ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.md ├── README.md ├── caddy └── Caddyfile.example ├── docker-compose.yml ├── docs ├── Makefile └── source │ ├── api_doc.rst │ ├── apis.rst │ ├── conf.py │ ├── index.rst │ ├── install.rst │ └── quick_start.rst ├── env.example ├── requirements.txt └── src ├── ap_test.py ├── cert ├── cert.pem └── key.pem ├── config.py ├── gunicorn_cfg.py ├── kuas_api ├── __init__.py ├── kuas │ ├── __init__.py │ ├── ap.py │ ├── back.py │ ├── bus.py │ ├── cache.py │ ├── job.py │ ├── kalendar.py │ ├── leave.py │ ├── news.py │ ├── notification.py │ ├── parse.py │ └── user.py ├── modules │ ├── __init__.py │ ├── const.py │ ├── error.py │ ├── json.py │ └── stateless_auth.py ├── news_db.sqlite ├── templates │ ├── login.html │ └── query.html └── views │ ├── __init__.py │ ├── latest │ └── __init__.py │ ├── v1 │ └── __init__.py │ └── v2 │ ├── __init__.py │ ├── ap.py │ ├── bus.py │ ├── doc.py │ ├── leave.py │ ├── news.py │ ├── notifications.py │ └── utils.py ├── test ├── __init__.py ├── legacy.py ├── test_api │ ├── __init__.py │ ├── test_ap.py │ ├── test_bus.py │ ├── test_leave.py │ ├── test_news.py │ ├── test_notifications.py │ └── test_utils.py ├── test_kuas │ ├── __init__.py │ ├── test_ap.py │ ├── test_bus.py │ ├── test_leave.py │ ├── test_news.py │ ├── test_parse.py │ └── test_user.py └── test_modules │ ├── __init__.py │ ├── test_json.py │ └── test_stateless_auth.py └── web-server.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ################# 3 | ## Eclipse 4 | ################# 5 | 6 | *.pydevproject 7 | .project 8 | .metadata 9 | bin/ 10 | tmp/ 11 | *.tmp 12 | *.bak 13 | *.swp 14 | *~.nib 15 | local.properties 16 | .classpath 17 | .settings/ 18 | .loadpath 19 | 20 | # External tool builders 21 | .externalToolBuilders/ 22 | 23 | # Locally stored "Eclipse launch configurations" 24 | *.launch 25 | 26 | # CDT-specific 27 | .cproject 28 | 29 | # PDT-specific 30 | .buildpath 31 | 32 | 33 | ################# 34 | ## Visual Studio 35 | ################# 36 | 37 | ## Ignore Visual Studio temporary files, build results, and 38 | ## files generated by popular Visual Studio add-ons. 39 | 40 | # User-specific files 41 | *.suo 42 | *.user 43 | *.sln.docstates 44 | 45 | # Build results 46 | 47 | [Dd]ebug/ 48 | [Rr]elease/ 49 | x64/ 50 | build/ 51 | [Bb]in/ 52 | [Oo]bj/ 53 | 54 | # MSTest test Results 55 | [Tt]est[Rr]esult*/ 56 | [Bb]uild[Ll]og.* 57 | 58 | *_i.c 59 | *_p.c 60 | *.ilk 61 | *.meta 62 | *.obj 63 | *.pch 64 | *.pdb 65 | *.pgc 66 | *.pgd 67 | *.rsp 68 | *.sbr 69 | *.tlb 70 | *.tli 71 | *.tlh 72 | *.tmp 73 | *.tmp_proj 74 | *.log 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.log 80 | *.scc 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | 102 | # TeamCity is a build add-in 103 | _TeamCity* 104 | 105 | # DotCover is a Code Coverage Tool 106 | *.dotCover 107 | 108 | # NCrunch 109 | *.ncrunch* 110 | .*crunch*.local.xml 111 | 112 | # Installshield output folder 113 | [Ee]xpress/ 114 | 115 | # DocProject is a documentation generator add-in 116 | DocProject/buildhelp/ 117 | DocProject/Help/*.HxT 118 | DocProject/Help/*.HxC 119 | DocProject/Help/*.hhc 120 | DocProject/Help/*.hhk 121 | DocProject/Help/*.hhp 122 | DocProject/Help/Html2 123 | DocProject/Help/html 124 | 125 | # Click-Once directory 126 | publish/ 127 | 128 | # Publish Web Output 129 | *.Publish.xml 130 | *.pubxml 131 | 132 | # NuGet Packages Directory 133 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 134 | #packages/ 135 | 136 | # Windows Azure Build Output 137 | csx 138 | *.build.csdef 139 | 140 | # Windows Store app package directory 141 | AppPackages/ 142 | 143 | # Others 144 | sql/ 145 | *.Cache 146 | ClientBin/ 147 | [Ss]tyle[Cc]op.* 148 | ~$* 149 | *~ 150 | *.dbmdl 151 | *.[Pp]ublish.xml 152 | *.pfx 153 | *.publishsettings 154 | 155 | # RIA/Silverlight projects 156 | Generated_Code/ 157 | 158 | # Backup & report files from converting an old project file to a newer 159 | # Visual Studio version. Backup files are not needed, because we have git ;-) 160 | _UpgradeReport_Files/ 161 | Backup*/ 162 | UpgradeLog*.XML 163 | UpgradeLog*.htm 164 | 165 | # SQL Server files 166 | App_Data/*.mdf 167 | App_Data/*.ldf 168 | 169 | ############# 170 | ## Windows detritus 171 | ############# 172 | 173 | # Windows image file caches 174 | Thumbs.db 175 | ehthumbs.db 176 | 177 | # Folder config file 178 | Desktop.ini 179 | 180 | # Recycle Bin used on file shares 181 | $RECYCLE.BIN/ 182 | 183 | # Mac crap 184 | .DS_Store 185 | 186 | 187 | ############# 188 | ## Python 189 | ############# 190 | 191 | *.py[co] 192 | 193 | # Packages 194 | *.egg 195 | *.egg-info 196 | dist/ 197 | build/ 198 | eggs/ 199 | parts/ 200 | var/ 201 | sdist/ 202 | develop-eggs/ 203 | .installed.cfg 204 | 205 | # Installer logs 206 | pip-log.txt 207 | 208 | # Unit test / coverage reports 209 | .coverage 210 | .tox 211 | 212 | #Translations 213 | *.mo 214 | 215 | #Mr Developer 216 | .mr.developer.cfg 217 | 218 | #################### 219 | ## Project Setting 220 | #################### 221 | 222 | app.py 223 | config.xml 224 | leave_test.py 225 | test_page.html 226 | html/ 227 | src/config.py 228 | src/gunicorn_cfg.py 229 | 230 | \.vscode/ 231 | 232 | \.idea/ 233 | caddy/Caddyfile 234 | \.env 235 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.6" 5 | 6 | install: 7 | - | 8 | python -m venv venv 9 | ./venv/bin/python -m pip install -r requirements.txt 10 | cd src 11 | 12 | script: 13 | - ../venv/bin/python -m unittest discover test 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM python:3.6 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | COPY . /usr/src/app 8 | 9 | RUN pip3 install -r ./requirements.txt 10 | 11 | RUN apt-get update && \ 12 | apt-get install -y nodejs 13 | 14 | WORKDIR /usr/src/app/src -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 高科大資訊研習社 (NKUST-ITC) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kuastw/AP-API.svg?branch=master)](https://travis-ci.org/kuastw/AP-API) 2 | [![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) 3 | 4 | NKUST - API 5 | ========== 6 | 7 | 高雄科技大學 API Server NKUST API Server 8 | --------------------------- 9 | Requirement 10 | --- 11 | - Ubuntu (18.04 or previous version) 12 | - Python 3.6 13 | - Redis server 14 | - NodeJS (if host by python venv) 15 | 16 | How to use? 17 | --- 18 | ### Clone project 19 | ``` 20 | $ git clone https://github.com/NKUST-ITC/AP-API 21 | $ cd AP-API 22 | ``` 23 | By python venv 24 | --- 25 | ``` 26 | $ python -m venv venv 27 | $ source venv/bin/activate 28 | $ pip install -r requirements.txt 29 | $ redis-server & 30 | $ python src/web-server.py 31 | ``` 32 | By docker 33 | --- 34 | Requirement 35 | * Redis instance running on localhost 36 | 37 | Need to add environment variable **-e REDIS_URL=redis://127.0.0.1:6379/0** 38 | 39 | or by export 40 | 41 | ``` 42 | $ export REDIS_URL=redis://127.0.0.1:6379/0 43 | ``` 44 | 45 | And let docker run host network need add **--network="host"** 46 | 47 | Otherwise redis config by docker network(see docker-compose.yml config) 48 | 49 | Arguments **gunicorn_cfg.py web-server:app** is production flask uWSGI 50 | ``` 51 | $ sudo docker run --network="host" nkustitc/ap-api:latest gunicorn -c gunicorn_cfg.py web-server:app 52 | ``` 53 | or replace by **python3 web-server.py** 54 | ``` 55 | $ sudo docker run --network="host" nkustitc/ap-api:latest python3 web-server.py 56 | ``` 57 | By docker-compose 58 | --- 59 | Copy .env example 60 | - CADDY_HOST_HTTPS_PORT -> caddy https host port 61 | - REDIS_URL -> python request redis url 62 | ``` 63 | $ cp env.example .env 64 | ``` 65 | Copy caddy host config example 66 | ``` 67 | $ cd caddy 68 | $ cp Caddyfile.example Caddyfile 69 | ``` 70 | Edit **Caddyfile**'s host config**(Production)** 71 | - line 1 **0.0.0.0:2087** replace by you want host domain and port 72 | ``` 73 | 0.0.0.0:2087 { 74 | proxy / https://web:14769 { 75 | transparent 76 | insecure_skip_verify 77 | } 78 | gzip 79 | tls example@gmail.com 80 | } 81 | ``` 82 | start docker-compose (if need re download package, can add **--build** build by Dockerfile) 83 | ``` 84 | $ sudo docker-compose up 85 | ``` 86 | --- 87 | Fixed APIBlueprint 88 | --- 89 | You must fixed manually about flask_apiblueprint 90 | 91 | ``` 92 | site-packages/flask_apiblueprint/flask_apiblueprint.py 93 | ``` 94 | Change .iteritems() to .items() in two place 95 | 96 | 97 | 98 | 99 | Demo 100 | --- 101 | https://kuas.grd.idv.tw:14769/v2/token 102 | 103 | 104 | 105 | Donate 106 | --- 107 | [![BitCoin donate 108 | button](http://img.shields.io/bitcoin/donate.png?color=yellow)](https://coinbase.com/checkouts/aa7cf80a2a85b4906cb98fc7b2aad5c5 "Donate 109 | once-off to this project using BitCoin") 110 | 111 | 112 | -------------------------------------------------------------------------------- /caddy/Caddyfile.example: -------------------------------------------------------------------------------- 1 | 0.0.0.0:2087 { 2 | proxy / https://web:14769 { 3 | transparent 4 | insecure_skip_verify 5 | } 6 | gzip 7 | tls example@gmail.com 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | web: 4 | image: "nkustitc/ap-api:latest" 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - .:/usr/src/app 10 | environment: 11 | - REDIS_URL=${REDIS_URL} 12 | - "TZ=Asia/Taipei" 13 | command: [ "gunicorn","-c","gunicorn_cfg.py","web-server:app"] 14 | networks: 15 | - redis-net 16 | - front-end 17 | depends_on: 18 | - redis 19 | redis: 20 | image: "redis:alpine" 21 | volumes: 22 | - redis-data:/data 23 | networks: 24 | - redis-net 25 | caddy: 26 | image: "abiosoft/caddy:latest" 27 | volumes: 28 | - ./caddy/Caddyfile:/etc/Caddyfile 29 | - ./caddy/path:/root/.caddy 30 | ports: 31 | - "2015:2015" 32 | - "80:80" 33 | - "443:443" 34 | - "${CADDY_HOST_HTTPS_PORT}:2087" 35 | environment: 36 | - ACME_AGREE=true 37 | depends_on: 38 | - web 39 | networks: 40 | - front-end 41 | networks: 42 | redis-net: 43 | front-end: 44 | volumes: 45 | redis-data: 46 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) -E $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/KUASAPAPI.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/KUASAPAPI.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/KUASAPAPI" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/KUASAPAPI" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/source/api_doc.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ===================== 3 | 4 | API Overviews 5 | --- 6 | 7 | 8 | .. toctree:: 9 | 10 | apis -------------------------------------------------------------------------------- /docs/source/apis.rst: -------------------------------------------------------------------------------- 1 | APIs 2 | ==== 3 | 4 | 5 | Utils 6 | ------ 7 | 8 | .. autoflask:: web-server:app 9 | :endpoints: latest.get_auth_token 10 | 11 | 12 | .. autoflask:: web-server:app 13 | :endpoints: latest.device_version 14 | 15 | .. autoflask:: web-server:app 16 | :endpoints: latest.servers_status 17 | 18 | 19 | Bus 20 | ---- 21 | 22 | .. autoflask:: web-server:app 23 | :endpoints: latest.timetables 24 | 25 | 26 | .. http:get:: /bus/reservations 27 | 28 | Get user's bus reservations. 29 | 30 | :reqheader Authorization: Using Basic Auth 31 | :statuscode 200: no error 32 | 33 | 34 | **Request** 35 | 36 | .. sourcecode:: http 37 | 38 | GET /latest/bus/reservations HTTP/1.1 39 | Host: kuas.grd.idv.tw:14769 40 | Authorization: Basic xxxxxxxxxxxxx= 41 | 42 | .. sourcecode:: shell 43 | 44 | curl -u username:password -X GET https://kuas.grd.idv.tw:14769/v2/bus/reservations 45 | 46 | 47 | **Response** 48 | 49 | .. sourcecode:: http 50 | 51 | HTTP/1.0 200 OK 52 | Content-Type: application/json 53 | 54 | 55 | { 56 | "reservation":[ 57 | { 58 | "endTime":"2017-08-06 16:50", 59 | "end":"燕巢", 60 | "cancelKey":"1559062", 61 | "time":"2017-08-07 07:50" 62 | } 63 | ] 64 | } 65 | 66 | 67 | .. http:put:: /bus/reservations/(int:bus_id) 68 | 69 | Make a reservations for user. 70 | 71 | :reqheader Authorization: Using Basic Auth 72 | :query int bus_id: Bus identifier 73 | :statuscode 200: no error 74 | 75 | 76 | **Request** 77 | 78 | .. sourcecode:: http 79 | 80 | PUT /latest/bus/reservations/36065 HTTP/1.1 81 | Host: kuas.grd.idv.tw:14769 82 | Authorization: Basic xxxxxxxxxxxxx= 83 | 84 | .. sourcecode:: shell 85 | 86 | curl -u username:password -X PUT https://kuas.grd.idv.tw:14769/v2/bus/reservations/36065 87 | 88 | 89 | **Response** 90 | 91 | .. sourcecode:: http 92 | 93 | HTTP/1.0 200 OK 94 | Content-Type: application/json 95 | 96 | 97 | { 98 | "success":true, 99 | "code":200, 100 | "count":0, 101 | "message":"預約成功", 102 | "data":{ 103 | "budId":36065, 104 | "startTime":"/Date(1502355600000)/" 105 | } 106 | } 107 | 108 | 109 | .. http:delete:: /bus/reservations/(int:cancel_key) 110 | 111 | Delete a reservations for user. 112 | 113 | :reqheader Authorization: Using Basic Auth 114 | :query int cancel_key: Bus cancel key 115 | :statuscode 200: no error 116 | 117 | 118 | **Request** 119 | 120 | .. sourcecode:: http 121 | 122 | DELETE /latest/bus/timetables/1559063 HTTP/1.1 123 | Host: kuas.grd.idv.tw:14769 124 | Authorization: Basic xxxxxxxxxxxxx= 125 | 126 | .. sourcecode:: shell 127 | 128 | curl -u username:password -X DELETE https://kuas.grd.idv.tw:14769/v2/bus/reservations/1559063 129 | 130 | 131 | **Response** 132 | 133 | .. sourcecode:: http 134 | 135 | 136 | 137 | Notifications 138 | --------------- 139 | 140 | .. autoflask:: web-server:app 141 | :endpoints: latest.notification 142 | 143 | 144 | AP 145 | --------------- 146 | 147 | .. autoflask:: web-server:app 148 | :endpoints: latest.ap_user_info 149 | 150 | .. autoflask:: web-server:app 151 | :endpoints: latest.ap_user_picture 152 | 153 | .. autoflask:: web-server:app 154 | :endpoints: latest.get_coursetables 155 | 156 | .. autoflask:: web-server:app 157 | :endpoints: latest.get_score 158 | 159 | .. autoflask:: web-server:app 160 | :endpoints: latest.ap_semester 161 | 162 | Leave 163 | --------------- 164 | 165 | .. autoflask:: web-server:app 166 | :endpoints: latest.get_leave 167 | 168 | .. autoflask:: web-server:app 169 | :endpoints: latest.leave_submit 170 | 171 | News 172 | --------------- 173 | 174 | .. autoflask:: web-server:app 175 | :endpoints: latest.news_all 176 | 177 | .. autoflask:: web-server:app 178 | :endpoints: latest.news 179 | 180 | 181 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # KUAS AP API documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jun 13 18:26:01 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | import sphinx_rtd_theme 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | sys.path.insert(0, os.path.abspath('../../src')) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.doctest', 38 | 'sphinx.ext.intersphinx', 39 | 'sphinx.ext.ifconfig', 40 | 'sphinx.ext.viewcode', 41 | 'sphinxcontrib.autohttp.flask', 42 | # 'numpydoc' 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The encoding of source files. 54 | #source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'KUAS AP API' 61 | copyright = '2015, Louie Lu' 62 | author = 'Louie Lu' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = '2.0.0' 70 | # The full version, including alpha/beta/rc tags. 71 | release = '2.0.0' 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = None 79 | 80 | # There are two options for replacing |today|: either, you set today to some 81 | # non-false value, then it is used: 82 | #today = '' 83 | # Else, today_fmt is used as the format for a strftime call. 84 | #today_fmt = '%B %d, %Y' 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | exclude_patterns = [] 89 | 90 | # The reST default role (used for this markup: `text`) to use for all 91 | # documents. 92 | #default_role = None 93 | 94 | # If true, '()' will be appended to :func: etc. cross-reference text. 95 | #add_function_parentheses = True 96 | 97 | # If true, the current module name will be prepended to all description 98 | # unit titles (such as .. function::). 99 | #add_module_names = True 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | #show_authors = False 104 | 105 | # The name of the Pygments (syntax highlighting) style to use. 106 | pygments_style = 'sphinx' 107 | 108 | # A list of ignored prefixes for module index sorting. 109 | #modindex_common_prefix = [] 110 | 111 | # If true, keep warnings as "system message" paragraphs in the built documents. 112 | #keep_warnings = False 113 | 114 | # If true, `todo` and `todoList` produce output, else they produce nothing. 115 | todo_include_todos = False 116 | 117 | 118 | # -- Options for HTML output ---------------------------------------------- 119 | 120 | # The theme to use for HTML and HTML Help pages. See the documentation for 121 | # a list of builtin themes. 122 | #html_theme = 'alabaster' 123 | #html_theme = 'classic' 124 | html_theme = "sphinx_rtd_theme" 125 | 126 | # Theme options are theme-specific and customize the look and feel of a theme 127 | # further. For a list of options available for each theme, see the 128 | # documentation. 129 | #html_theme_options = {} 130 | 131 | # Add any paths that contain custom themes here, relative to this directory. 132 | #html_theme_path = [] 133 | htlm_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 134 | 135 | # The name for this set of Sphinx documents. If None, it defaults to 136 | # " v documentation". 137 | #html_title = None 138 | 139 | # A shorter title for the navigation bar. Default is the same as html_title. 140 | #html_short_title = None 141 | 142 | # The name of an image file (relative to this directory) to place at the top 143 | # of the sidebar. 144 | #html_logo = None 145 | 146 | # The name of an image file (within the static path) to use as favicon of the 147 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 148 | # pixels large. 149 | #html_favicon = None 150 | 151 | # Add any paths that contain custom static files (such as style sheets) here, 152 | # relative to this directory. They are copied after the builtin static files, 153 | # so a file named "default.css" will overwrite the builtin "default.css". 154 | html_static_path = ['_static'] 155 | 156 | # Add any extra paths that contain custom files (such as robots.txt or 157 | # .htaccess) here, relative to this directory. These files are copied 158 | # directly to the root of the documentation. 159 | #html_extra_path = [] 160 | 161 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 162 | # using the given strftime format. 163 | #html_last_updated_fmt = '%b %d, %Y' 164 | 165 | # If true, SmartyPants will be used to convert quotes and dashes to 166 | # typographically correct entities. 167 | #html_use_smartypants = True 168 | 169 | # Custom sidebar templates, maps document names to template names. 170 | #html_sidebars = {} 171 | 172 | # Additional templates that should be rendered to pages, maps page names to 173 | # template names. 174 | #html_additional_pages = {} 175 | 176 | # If false, no module index is generated. 177 | #html_domain_indices = True 178 | 179 | # If false, no index is generated. 180 | #html_use_index = True 181 | 182 | # If true, the index is split into individual pages for each letter. 183 | #html_split_index = False 184 | 185 | # If true, links to the reST sources are added to the pages. 186 | #html_show_sourcelink = True 187 | 188 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 189 | #html_show_sphinx = True 190 | 191 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 192 | #html_show_copyright = True 193 | 194 | # If true, an OpenSearch description file will be output, and all pages will 195 | # contain a tag referring to it. The value of this option must be the 196 | # base URL from which the finished HTML is served. 197 | #html_use_opensearch = '' 198 | 199 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 200 | #html_file_suffix = None 201 | 202 | # Language to be used for generating the HTML full-text search index. 203 | # Sphinx supports the following languages: 204 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 205 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 206 | #html_search_language = 'en' 207 | 208 | # A dictionary with options for the search language support, empty by default. 209 | # Now only 'ja' uses this config value 210 | #html_search_options = {'type': 'default'} 211 | 212 | # The name of a javascript file (relative to the configuration directory) that 213 | # implements a search results scorer. If empty, the default will be used. 214 | #html_search_scorer = 'scorer.js' 215 | 216 | # Output file base name for HTML help builder. 217 | htmlhelp_basename = 'KUASAPAPIdoc' 218 | 219 | # -- Options for LaTeX output --------------------------------------------- 220 | 221 | latex_elements = { 222 | # The paper size ('letterpaper' or 'a4paper'). 223 | #'papersize': 'letterpaper', 224 | 225 | # The font size ('10pt', '11pt' or '12pt'). 226 | #'pointsize': '10pt', 227 | 228 | # Additional stuff for the LaTeX preamble. 229 | #'preamble': '', 230 | 231 | # Latex figure (float) alignment 232 | #'figure_align': 'htbp', 233 | } 234 | 235 | # Grouping the document tree into LaTeX files. List of tuples 236 | # (source start file, target name, title, 237 | # author, documentclass [howto, manual, or own class]). 238 | latex_documents = [ 239 | (master_doc, 'KUASAPAPI.tex', 'KUAS AP API Documentation', 240 | 'Louie Lu', 'manual'), 241 | ] 242 | 243 | # The name of an image file (relative to this directory) to place at the top of 244 | # the title page. 245 | #latex_logo = None 246 | 247 | # For "manual" documents, if this is true, then toplevel headings are parts, 248 | # not chapters. 249 | #latex_use_parts = False 250 | 251 | # If true, show page references after internal links. 252 | #latex_show_pagerefs = False 253 | 254 | # If true, show URL addresses after external links. 255 | #latex_show_urls = False 256 | 257 | # Documents to append as an appendix to all manuals. 258 | #latex_appendices = [] 259 | 260 | # If false, no module index is generated. 261 | #latex_domain_indices = True 262 | 263 | 264 | # -- Options for manual page output --------------------------------------- 265 | 266 | # One entry per manual page. List of tuples 267 | # (source start file, name, description, authors, manual section). 268 | man_pages = [ 269 | (master_doc, 'kuasapapi', 'KUAS AP API Documentation', 270 | [author], 1) 271 | ] 272 | 273 | # If true, show URL addresses after external links. 274 | #man_show_urls = False 275 | 276 | 277 | # -- Options for Texinfo output ------------------------------------------- 278 | 279 | # Grouping the document tree into Texinfo files. List of tuples 280 | # (source start file, target name, title, author, 281 | # dir menu entry, description, category) 282 | texinfo_documents = [ 283 | (master_doc, 'KUASAPAPI', 'KUAS AP API Documentation', 284 | author, 'KUASAPAPI', 'One line description of project.', 285 | 'Miscellaneous'), 286 | ] 287 | 288 | # Documents to append as an appendix to all manuals. 289 | #texinfo_appendices = [] 290 | 291 | # If false, no module index is generated. 292 | #texinfo_domain_indices = True 293 | 294 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 295 | #texinfo_show_urls = 'footnote' 296 | 297 | # If true, do not generate a @detailmenu in the "Top" node's menu. 298 | #texinfo_no_detailmenu = False 299 | 300 | 301 | # -- Options for Epub output ---------------------------------------------- 302 | 303 | # Bibliographic Dublin Core info. 304 | epub_title = project 305 | epub_author = author 306 | epub_publisher = author 307 | epub_copyright = copyright 308 | 309 | # The basename for the epub file. It defaults to the project name. 310 | #epub_basename = project 311 | 312 | # The HTML theme for the epub output. Since the default themes are not optimized 313 | # for small screen space, using the same theme for HTML and epub output is 314 | # usually not wise. This defaults to 'epub', a theme designed to save visual 315 | # space. 316 | #epub_theme = 'epub' 317 | 318 | # The language of the text. It defaults to the language option 319 | # or 'en' if the language is not set. 320 | #epub_language = '' 321 | 322 | # The scheme of the identifier. Typical schemes are ISBN or URL. 323 | #epub_scheme = '' 324 | 325 | # The unique identifier of the text. This can be a ISBN number 326 | # or the project homepage. 327 | #epub_identifier = '' 328 | 329 | # A unique identification for the text. 330 | #epub_uid = '' 331 | 332 | # A tuple containing the cover image and cover page html template filenames. 333 | #epub_cover = () 334 | 335 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 336 | #epub_guide = () 337 | 338 | # HTML files that should be inserted before the pages created by sphinx. 339 | # The format is a list of tuples containing the path and title. 340 | #epub_pre_files = [] 341 | 342 | # HTML files shat should be inserted after the pages created by sphinx. 343 | # The format is a list of tuples containing the path and title. 344 | #epub_post_files = [] 345 | 346 | # A list of files that should not be packed into the epub file. 347 | epub_exclude_files = ['search.html'] 348 | 349 | # The depth of the table of contents in toc.ncx. 350 | #epub_tocdepth = 3 351 | 352 | # Allow duplicate toc entries. 353 | #epub_tocdup = True 354 | 355 | # Choose between 'default' and 'includehidden'. 356 | #epub_tocscope = 'default' 357 | 358 | # Fix unsupported image types using the Pillow. 359 | #epub_fix_images = False 360 | 361 | # Scale large images. 362 | #epub_max_image_width = 0 363 | 364 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 365 | #epub_show_urls = 'inline' 366 | 367 | # If false, no index is generated. 368 | #epub_use_index = True 369 | 370 | 371 | # Example configuration for intersphinx: refer to the Python standard library. 372 | intersphinx_mapping = {'https://docs.python.org/': None} 373 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. KUAS AP API documentation master file, created by 2 | sphinx-quickstart on Sat Jun 13 18:26:01 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to KUAS AP API's documentation! 7 | ======================================= 8 | 9 | v2 api is currently in develop. 10 | There is only android 1.6.0 using this api and api will still be change. 11 | 12 | Contents: 13 | 14 | .. toctree:: 15 | :maxdepth: 5 16 | :glob: 17 | 18 | install 19 | quick_start 20 | api_doc 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Installing KUAS API is a simple job. follow the instruction and you can easily install it. 5 | 6 | .. note:: 7 | KUAS API is currently using Python 3. 8 | 9 | 10 | First, obtain Python_3_ and virtualenv_ if you do not already have them. Using a 11 | virtual environment will make the installation easier, and will help to avoid 12 | clutter in your system-wide libraries. You will also need Git_ in order to 13 | clone the repository. 14 | 15 | .. _Python_3: http://www.python.org/ 16 | .. _virtualenv: http://pypi.python.org/pypi/virtualenv 17 | .. _Git: http://git-scm.com/ 18 | 19 | 20 | First, clone the repo from github:: 21 | 22 | $ git clone https://github.com/johnsounder/ap-api 23 | $ cd ap-api 24 | 25 | Once you finish, create a virtual environment somewhere on your disk, then 26 | activate it:: 27 | 28 | $ virtualenv .env --python=python3.4 29 | $ source .env/bin/activate 30 | 31 | You can deactivate virtual environment with:: 32 | 33 | (.env)$ deactive 34 | 35 | Now you are using a virtual environment with Python 3. 36 | We must change to branch v2 and install requirements module:: 37 | 38 | (.env)$ git checkout v2 39 | (.env)$ pip install -r requirements.txt 40 | (.env)$ yarout -S redis 41 | 42 | After install all requirements module, you will need to fixed the python2/3 43 | version problem about flask-APIABlueprint:: 44 | 45 | (.env)$ sed -i -- 's/iteritems/items/g' .env/lib/python3.4/site-packages/flask_apiblueprint/apiblueprint.py 46 | 47 | 48 | Then, the installation is done. You can run KUAS API now with this:: 49 | 50 | (.env)$ redis-server & 51 | (.env)$ python src/web-server.py 52 | 53 | Or like this:: 54 | 55 | (.env)$ gunicorn -b 0.0.0.0:5001 web-server:app 56 | 57 | -------------------------------------------------------------------------------- /docs/source/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | ============ 3 | 4 | **Authenication** 5 | 6 | .. sourcecode:: shell 7 | 8 | curl -u username:password https://kuas.grd.idv.tw:14769/v2/token 9 | 10 | 11 | 12 | .. sourcecode:: http 13 | 14 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | CADDY_HOST_HTTPS_PORT=2087 2 | 3 | REDIS_URL=redis://redis:6379/0 4 | # if use docker => redis://redis:6379/0 5 | # else => redis://localhost:6379/0 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask<1.0 2 | flask_apiblueprint 3 | flask_autodoc 4 | flask_cors 5 | flask_httpauth 6 | flask_admin 7 | flask_sqlalchemy 8 | flask_compress 9 | redis 10 | requests 11 | lxml 12 | pyexecjs 13 | sphinxcontrib-httpdomain 14 | gunicorn 15 | pyopenssl -------------------------------------------------------------------------------- /src/ap_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import requests 4 | 5 | USERNAME = "1102108133" 6 | PASSWORD = "111" 7 | 8 | #URL = "http://api.grd.idv.tw:14768/" 9 | #URL = "http://kuas.grd.idv.tw:14768/" 10 | URL = "http://localhost:5001/" 11 | 12 | def process(): 13 | s = requests.Session() 14 | 15 | try: 16 | r = s.get(URL) 17 | print("[*] Server found, ready to test!") 18 | except Exception as e: 19 | print("[-] Server Error!!! Must to reset!!!") 20 | print(e) 21 | return 22 | 23 | try: 24 | r = s.post(URL + "ap/login", data={"username": USERNAME, "password": PASSWORD}) 25 | if r.text.startswith("true"): 26 | print("[*] Login Success") 27 | else: 28 | print("[-] Login fail, incorrect username or password") 29 | return 30 | except Exception as e: 31 | print("[-] Server Error!!! Fail to login") 32 | print(e) 33 | return 34 | 35 | try: 36 | r = s.post(URL + "ap/query", data={"arg01": "103", "arg02": "01", "arg03": "1102108133", "fncid": "ag008"}) 37 | if not r.text.startswith("false"): 38 | print("[*] AP Query success") 39 | print(" - %s" % r.text[:100]) 40 | else: 41 | print("[-] AP Query fail") 42 | return 43 | except Exception as e: 44 | print("[-] AP Query fatal error") 45 | print(e) 46 | #return 47 | 48 | 49 | try: 50 | r = s.post(URL + "bus/query", data={"date": "2014-10-30"}) 51 | print("[*] Bus Query success") 52 | print(" - %s" % r.text[:100]) 53 | except Exception as e: 54 | print("[-] Bus Query fatal error") 55 | print(e) 56 | #return 57 | 58 | try: 59 | r = s.post(URL + "leave", data={"arg01": "103", "arg02": "01"}) 60 | if not r.text.startswith("false"): 61 | print("[*] Leave Query success") 62 | print(" - %s" % r.text) 63 | else: 64 | print("[-] Leave Query fail") 65 | return 66 | except Exception as e: 67 | print("[-] Leave Query fatal error") 68 | print(e) 69 | #return 70 | 71 | 72 | 73 | print("[*] Pass test :)") 74 | 75 | if __name__ == "__main__": 76 | process() 77 | -------------------------------------------------------------------------------- /src/cert/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEUjCCArqgAwIBAgIQKIdZXn4Uzzcwr6Wwj3SPMDANBgkqhkiG9w0BAQsFADB/ 3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKjAoBgNVBAsMIXJhaW52 4 | aXNpdG9yQFJheS1NYWNCb29rLVByby5sb2NhbDExMC8GA1UEAwwobWtjZXJ0IHJh 5 | aW52aXNpdG9yQFJheS1NYWNCb29rLVByby5sb2NhbDAeFw0xOTAyMDUwOTE2MTha 6 | Fw0yOTAyMDUwOTE2MThaMFcxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj 7 | ZXJ0aWZpY2F0ZTEsMCoGA1UECwwjcmFpbnZpc2l0b3JAUmF5LU1hY0Jvb2stUHJv 8 | LTcubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC036vGvUvo 9 | 4sDPKmbPogPpEAlZm3wBrxqNpLmpwzLUpdLcNNQc76hJdIA0vQr3PTwrtJNKHSwk 10 | 2glgNNCvOPm2tkoTQ0nFUQftamcec5WFpEh9fIeSCQYp148ppCbrKFZzHjiaw5nv 11 | KNi49nFvGl9lpiHaCpiX/EEblS1RVDFbfgR+i74YlKEm/+hgtYExuoBLpNY5wmM3 12 | qe7oc0+H3qNH1YTIU33nF/w5IZy+3n+2oQ9CouO/I3sWVcBldRwaJm79QYcyPP8R 13 | ubr/HfZXFmOh2FH4iPzjED8o+RZpJlWj5GV/nh9JbrS1ty7Nrm8rWRaPPcP0SUEp 14 | 3J5vjH220FZ9AgMBAAGjcjBwMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr 15 | BgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFHdyNMG+3niwN+1Xxdbv 16 | J6P0LPijMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF 17 | AAOCAYEAo4SrbX1gmZKUhP77W25TFiNyKXt9nGz2/QB3IKU++/MP3kLk0WduNkHT 18 | HfAj//NHKcJSBsQ3HWTuLe2Qeei/qfeNUM8GTm2va0eOZKs4YynkILm05orfnbDb 19 | bndz3VQ2Ee/5kvjX0xWAjouyV1Z/3z2nbhK6UNFZvc0oMYi/vkXQ7227Pc+MOs9K 20 | KJcvyTwnOUDDLKpmrGi83XpiJKsaCm/7vwQ4V9Pom47mT0lFJ+YXUPBzCkLGpFiP 21 | ax2tsJgdIaqkZK79Vs2B8f/vtgf+mshPUZ+7OFGcdlXHcK3LlzmpuoDJaz+4rvKu 22 | Ox7JK0WU20tMNnYL36Pe9PV7CuTJpiwbZuOAtNbuaHy8E9QoHLeKmu8z2oQzWz17 23 | C3CuTPaj7mhSEvcQoxywZJ0+y+CPPx5BNOHTfV+IHYIMSFymGDrJh9ge6McsUvP3 24 | w8Jy8j30guFmgTA0Q3I2ohLe8uPhN0Fk9sSrV+I/sEYcIbQkrr2Z0bNOfISPl5fs 25 | xgKfEVpC 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /src/cert/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC036vGvUvo4sDP 3 | KmbPogPpEAlZm3wBrxqNpLmpwzLUpdLcNNQc76hJdIA0vQr3PTwrtJNKHSwk2glg 4 | NNCvOPm2tkoTQ0nFUQftamcec5WFpEh9fIeSCQYp148ppCbrKFZzHjiaw5nvKNi4 5 | 9nFvGl9lpiHaCpiX/EEblS1RVDFbfgR+i74YlKEm/+hgtYExuoBLpNY5wmM3qe7o 6 | c0+H3qNH1YTIU33nF/w5IZy+3n+2oQ9CouO/I3sWVcBldRwaJm79QYcyPP8Rubr/ 7 | HfZXFmOh2FH4iPzjED8o+RZpJlWj5GV/nh9JbrS1ty7Nrm8rWRaPPcP0SUEp3J5v 8 | jH220FZ9AgMBAAECggEAVv28gDlK6Rcl5H1gNTyW5ODxnkdJvQWan8U6Bov7Ror6 9 | fy5pVgFtzuZZQwQo4gBxkBOpQ1wEfzTejYbZV2zvrRC/T8RtFpmCVo12Sw8MOtpo 10 | gvIBwhrU/ArQsBZjIXalHXjLgKPSxVO/6DWfGPB2MU1Vuqid+3s3VSzKPvNfScDj 11 | Go64l1FiVD0rMOw4jVcjMNVOSB/HVfaaexHX+CeHaZ9cKHkGxxDUscBbJieKp2nc 12 | dz4pFDZBsVVQx/jn1uF8p5FqLweJw5yMhVXovYZfQd/06ImDppTzHn/JUoadmMy+ 13 | U3eZOERIIm6tSw0kOWNuUTrPOth8ezRxuOiPG5DPgQKBgQDcuacCnPmy++7mYVLv 14 | iH9q+AnMB4LfFjqpxW0nnESeyJtwIL1y6+vCx4WdsBLU5ia7mt3fWpCiIg9ZCzXP 15 | YFyA+VGjm6SNkuxjrMIUD+crMpAJqUL23HcBUqPDUhXBRrhNiTpGOhOzn6sH0IaM 16 | XgPKZA/B+07TUgMAoCpap+1Y1wKBgQDRx5uLnMqLGerOruLhwxfc3Sch3i/L8iZd 17 | bHmn+BSxczdvS6t9mHq/h8onSgfTwcHEcTwMTO2iUFojioJ8KLCPmUOxKhLp/2aM 18 | 2HJheHFCw37vnYQKUDXAaICqAeIvybnPcvaWZal9heDhpZ1+8rJHUpwzb/L2Y8xB 19 | spTAheu8ywKBgQCTgK2PHX/wkFOyOU/HKxybS26gnlRi6OQDGCA93DwvMDhP0lFI 20 | P0iqPdOY8VVkWPmBXZjv7gHBl6lSBB/NmcO3nOVlxFlPEuROJ+D6rzX4tC11h1ts 21 | xR/yDlvJ500KgEwh5JbA34bS/ty4uC1yGFHIKt4s79hZd/DxthcXxijiuQKBgH35 22 | CswE5JAxiRKCbNY7rInB+CzbWwJysF0rtcaLMAn7cU+RNjMerJ91cIy1ZQvhb3WC 23 | theA3ra439g15fOfD5+73q114ZPI/hEYLV+gzwrTkNddVJxI3G5lktYEeYpO7hjI 24 | JZHdDLHHAmseY/yGy04PKqOs107kURUmozMVeKGPAoGAS+jcQ8cTUZDEH4L11SY2 25 | Md5jmA7NMkPsz0Iz1xr6yDLMl4tc0NDLDcMp1P7ctZJlEV9gU88cKMhMfXxOJ+U7 26 | 1PBJJa5tRsYVzgXcbQX56Hmbte6PbQpWcmQ5Cu1ZP8kdmNL/QNmHxHNkWDto73NC 27 | WHz8l7Z0xZnmpZO6amvlIrM= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | DEBUG = True 4 | SECRET_KEY = "change secret key in production" 5 | SESSION_COOKIE_HTTPONLY = True 6 | SESSION_COOKIE_SECURE = True 7 | #PERMANENT_SESSION_LIFETIME = timedelta(minutes=10) 8 | 9 | UNITTEST_USERNAME = os.environ.get('USERNAME', '') 10 | UNITTEST_PASSWORD = os.environ.get('PASSWORD', '') 11 | -------------------------------------------------------------------------------- /src/gunicorn_cfg.py: -------------------------------------------------------------------------------- 1 | # DEBUGGING 2 | reload = True 3 | 4 | # Bind IP 5 | bind = "0.0.0.0:14769" 6 | 7 | # SSL 8 | certfile="./cert/cert.pem" 9 | keyfile="./cert/key.pem" 10 | 11 | # Performance 12 | workers = 3 13 | worker_class = "sync" 14 | worker_connections = 1000 15 | timeout = 30 16 | keepalive = 5 17 | 18 | # Logger 19 | accesslog = "-" 20 | access_logformat = "[api.v2] %(h)s %(l)s %(u)s %(t)s .%(r)s. %(s)s %(b)s .%(f)s. .%(a)s. conn=\"%({Connection}i)s\"" 21 | 22 | -------------------------------------------------------------------------------- /src/kuas_api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 08/29/2015 4 | @Author: Louie Lu 5 | """ 6 | 7 | from flask import Flask 8 | import flask_admin as admin 9 | from flask_sqlalchemy import SQLAlchemy 10 | from flask_compress import Compress 11 | import os 12 | 13 | __version__ = "2.0" 14 | 15 | app = Flask(__name__) 16 | app.config.from_object("config") 17 | 18 | # Add admin 19 | admin = admin.Admin(app, name="KUAS-API News", template_mode="bootstrap3") 20 | 21 | # Add db 22 | app.config["DATABASE_FILE"] = "news_db.sqlite" 23 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + \ 24 | app.config["DATABASE_FILE"] 25 | app.config["SQLALCHEMY_ECHO"] = True 26 | news_db = SQLAlchemy(app) 27 | 28 | # Let secret key go in 29 | import redis 30 | red = redis.StrictRedis.from_url(url= os.environ['REDIS_URL'],db=2) 31 | 32 | red.set("SECRET_KEY", str(app.config["SECRET_KEY"])) 33 | 34 | 35 | # Compress please 36 | compress = Compress() 37 | compress.init_app(app) 38 | 39 | 40 | from kuas_api.views.v2.doc import auto, doc 41 | auto.init_app(app) 42 | 43 | 44 | from kuas_api.views.v1 import api_v1 45 | app.register_blueprint(api_v1) 46 | 47 | 48 | # I'm lazy 49 | from kuas_api.views.v2 import api_v2 50 | app.register_blueprint(api_v2) 51 | 52 | 53 | # Lazy about it 54 | from kuas_api.views.latest import latest 55 | app.register_blueprint(latest) 56 | 57 | # register doc 58 | app.register_blueprint(doc) 59 | 60 | 61 | if __name__ == '__main__': 62 | app.run(host="0.0.0.0", port=5001) 63 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """This module provide three kuas online system api. 4 | AP system, bus system, and leave system. 5 | 6 | Module AP 7 | ========= 8 | .. automodule:: kuas_api.kuas.ap 9 | :members: 10 | 11 | Module Bus 12 | ========== 13 | .. automodule:: kuas_api.kuas.bus 14 | :members: 15 | 16 | """ 17 | 18 | __license__ = "MIT" 19 | __docformat__ = "reStructuredText" 20 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/ap.py: -------------------------------------------------------------------------------- 1 | # -*- encoding=utf-8 -*- 2 | """This module `ap` provide manipulate of kuas AP system. 3 | """ 4 | 5 | __version__ = 2.0 6 | 7 | import requests 8 | from lxml import etree 9 | # AP URL Setting 10 | #: AP sytem base url 11 | AP_BASE_URL = "https://webap.nkust.edu.tw" 12 | 13 | #: AP system login url 14 | AP_LOGIN_URL = AP_BASE_URL + "/nkust/perchk.jsp" 15 | 16 | #: AP system general query url, with two args, 17 | # first: prefix of qid, second: qid 18 | AP_QUERY_URL = AP_BASE_URL + "/nkust/%s_pro/%s.jsp" 19 | 20 | #: AP guest account 21 | AP_GUEST_ACCOUNT = "guest" 22 | 23 | #: AP guest password 24 | AP_GUEST_PASSWORD = "123" 25 | 26 | # Timeout Setting 27 | #: Login timeout 28 | LOGIN_TIMEOUT = 5.0 29 | #: Query timeout 30 | QUERY_TIMEOUT = 5.0 31 | 32 | 33 | def status(): 34 | """Return AP server status code 35 | 36 | :rtype: int 37 | :returns: A HTTP status code 38 | 39 | >>> status() 40 | 200 41 | """ 42 | try: 43 | ap_status_code = requests.head( 44 | AP_BASE_URL, 45 | timeout=LOGIN_TIMEOUT).status_code 46 | except requests.exceptions.Timeout: 47 | ap_status_code = 408 48 | 49 | return ap_status_code 50 | 51 | 52 | def login(session, username, password, timeout=LOGIN_TIMEOUT): 53 | """Login to KUAS AP system. 54 | 55 | :param session: requests session object 56 | :type session: class requests.sessions.Session 57 | :param username: username of kuas ap system, actually your kuas student id 58 | :type username: str or int 59 | :param password: password of kuas ap system. 60 | :type password: str or int 61 | :param timeout: login timeout 62 | :type timeout: int 63 | 64 | :return: login status 65 | :rtype: bool 66 | 67 | 68 | Login with correct username and password 69 | 70 | >>> s = requests.Session() 71 | >>> login(s, "guest", "123") 72 | True 73 | 74 | 75 | Login with bad username or password 76 | 77 | >>> login(s, "guest", "777") 78 | False 79 | """ 80 | 81 | payload = {"uid": username, "pwd": password} 82 | 83 | # If timeout, return false 84 | try: 85 | r = session.post(AP_LOGIN_URL, data=payload, timeout=timeout) 86 | except requests.exceptions.Timeout: 87 | return False 88 | 89 | root = etree.HTML(r.text) 90 | 91 | try: 92 | is_login = not root.xpath("//script")[-1].text.startswith("alert") 93 | except: 94 | is_login = False 95 | 96 | return is_login 97 | 98 | 99 | def get_semester_list(): 100 | """Get semester list from ap system. 101 | 102 | :rtype: dict 103 | 104 | >>> get_semester_list()[-1]['value'] 105 | '92,2' 106 | """ 107 | 108 | s = requests.Session() 109 | login(s, AP_GUEST_ACCOUNT, AP_GUEST_PASSWORD) 110 | 111 | content = cache.ap_query(s, "ag304_01") 112 | if len(content) < 3000: 113 | return False 114 | root = etree.HTML(content) 115 | 116 | #options = root.xpath("id('yms_yms')/option") 117 | try: 118 | options = map(lambda x: {"value": x.values()[0].replace("#", ","), 119 | "selected": 1 if "selected" in x.values() else 0, 120 | "text": x.text}, 121 | root.xpath("id('yms_yms')/option") 122 | ) 123 | except: 124 | return False 125 | 126 | options = list(options) 127 | 128 | return options 129 | 130 | 131 | def query(session, qid, args={}): 132 | """Query AP system page by qid and args 133 | 134 | :param session: requests session object, the session must login first. 135 | :type session: class requests.sessions.Session 136 | :param qid: query id of ap system page 137 | :type qid: str 138 | :param args: arguments of query post 139 | :type args: dict 140 | 141 | :return" content of query page 142 | :rtype: str 143 | 144 | You must login first when using query 145 | Otherwise ap system won't let you use it. 146 | 147 | >>> s = requests.Session() 148 | >>> content = query(s, "ag222", {"arg01": "103", "arg02": "2"}) 149 | >>> "Please Logon" in content 150 | True 151 | 152 | 153 | Login to guest 154 | 155 | >>> login(s, "guest", "123") 156 | True 157 | 158 | Query course data (ag202) 159 | 160 | >>> args = {"yms_yms": "103#2", "dgr_id": "14", "unt_id": "UC02", \ 161 | "clyear": "", "sub_name": "", "teacher": "", "week": 2, \ 162 | "period": 4, "reading": "reading"} 163 | >>> content = query(s, "ag202", args) 164 | >>> "內部控制暨稽核制度" in content 165 | True 166 | """ 167 | 168 | data = {"arg01": "", "arg02": "", "arg03": "", 169 | "fncid": "", "uid": ""} 170 | 171 | data['fncid'] = qid 172 | if args != None: 173 | for key in args: 174 | data[key] = args[key] 175 | 176 | try: 177 | resp = session.post(AP_QUERY_URL % (qid[:2], qid), 178 | data=data, 179 | timeout=QUERY_TIMEOUT 180 | ) 181 | resp.encoding = "utf-8" 182 | content = resp.text 183 | except requests.exceptions.ReadTimeout: 184 | content = "" 185 | return content 186 | 187 | 188 | if __name__ == "__main__": 189 | #import doctest 190 | # doctest.testmod() 191 | print(get_semester_list()) 192 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/back.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | from multiprocessing import Process 3 | import collections 4 | import threading 5 | import datetime 6 | import requests 7 | import execjs 8 | import json 9 | import os.path 10 | 11 | js_function =""" 12 | function baseEncryption(e) { 13 | function h(b, a) { var d, c, e, f, g; e = b & 2147483648; f = a & 2147483648; d = b & 1073741824; c = a & 1073741824; g = (b & 14 | 1073741823) + (a & 1073741823); return d & c ? g ^ 2147483648 ^ e ^ f : d | c ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e 15 | ^ f : g ^ e ^ f } function g(b, a, d, c, e, f, g) { b = h(b, h(h(a & d | ~a & c, e), g)); return h(b << f | b >>> 32 - f, a) } function i(b, 16 | a, d, c, e, f, g) { b = h(b, h(h(a & c | d & ~c, e), g)); return h(b << f | b >>> 32 - f, a) } function j(b, a, c, d, e, f, g) { b = h(b, 17 | h(h(a ^ c ^ d, e), g)); return h(b << f | b >>> 32 - f, a) } function k(b, a, c, d, e, f, g) { 18 | b = h(b, h(h(c ^ 19 | (a | ~d), e), g)); return h(b << f | b >>> 32 - f, a) 20 | } function l(b) { var a = "", c = "", d; for (d = 0; 3 >= d; d++) c = b >>> 8 * d & 255, c = "0" + c.toString(16), a += 21 | c.substr(c.length - 2, 2); return a } var f = [], m, n, o, p, b, a, d, c, f = function (b) { var a, c = b.length; a = c + 8; for (var d = 16 22 | * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;) a = (g - g % 4) / 4, f = 8 * (g % 4), e[a] |= b.charCodeAt(g) << f, g++; 23 | a = (g - g % 4) / 4; e[a] |= 128 << 8 * (g % 4); e[d - 2] = c << 3; e[d - 1] = c >>> 29; return e } (e); b = 1732584193; a = 4023233417; d = 24 | 2562383102; c = 271733878; for (e = 0; e < f.length; e += 16) m = b, n = a, o = d, p = c, b = g(b, a, d, c, f[e + 25 | 0], 7, 3614090360), c = g(c, b, a, d, f[e + 1], 12, 3905402710), d = g(d, c, b, a, f[e + 2], 17, 606105819), a = g(a, d, c, 26 | b, f[e + 3], 22, 3250441966), b = g(b, a, d, c, f[e + 4], 7, 4118548399), c = g(c, b, a, d, f[e + 5], 12, 1200080426), d = g(d, c, b, a, f[e 27 | + 6], 17, 2821735955), a = g(a, d, c, b, f[e + 7], 22, 4249261313), b = g(b, a, d, c, f[e + 8], 7, 1770035416), c = g(c, b, a, d, f[e + 9], 28 | 12, 2336552879), d = g(d, c, b, a, f[e + 10], 17, 4294925233), a = g(a, d, c, b, f[e + 11], 22, 2304563134), b = g(b, a, d, c, f[e + 12], 7, 29 | 1804603682), c = g(c, b, a, d, f[e + 13], 12, 4254626195), d = g(d, c, b, a, f[e + 14], 17, 2792965006), a = g(a, d, 30 | c, b, f[e + 15], 22, 1236535329), b = i(b, a, d, c, f[e + 1], 5, 4129170786), c = i(c, b, a, d, f[e + 6], 9, 3225465664), d = 31 | i(d, c, b, a, f[e + 11], 14, 643717713), a = i(a, d, c, b, f[e + 0], 20, 3921069994), b = i(b, a, d, c, f[e + 5], 5, 3593408605), c = i(c, b, 32 | a, d, f[e + 10], 9, 38016083), d = i(d, c, b, a, f[e + 15], 14, 3634488961), a = i(a, d, c, b, f[e + 4], 20, 3889429448), b = i(b, a, d, c, 33 | f[e + 9], 5, 568446438), c = i(c, b, a, d, f[e + 14], 9, 3275163606), d = i(d, c, b, a, f[e + 3], 14, 4107603335), a = i(a, d, c, b, f[e + 34 | 8], 20, 1163531501), b = i(b, a, d, c, f[e + 13], 5, 2850285829), c = i(c, b, a, d, f[e + 2], 9, 4243563512), d = i(d, 35 | c, b, a, f[e + 7], 14, 1735328473), a = i(a, d, c, b, f[e + 12], 20, 2368359562), b = j(b, a, d, c, f[e + 5], 4, 4294588738), 36 | c = j(c, b, a, d, f[e + 8], 11, 2272392833), d = j(d, c, b, a, f[e + 11], 16, 1839030562), a = j(a, d, c, b, f[e + 14], 23, 4259657740), b = 37 | j(b, a, d, c, f[e + 1], 4, 2763975236), c = j(c, b, a, d, f[e + 4], 11, 1272893353), d = j(d, c, b, a, f[e + 7], 16, 4139469664), a = j(a, d, 38 | c, b, f[e + 10], 23, 3200236656), b = j(b, a, d, c, f[e + 13], 4, 681279174), c = j(c, b, a, d, f[e + 0], 11, 3936430074), d = j(d, c, b, a, 39 | f[e + 3], 16, 3572445317), a = j(a, d, c, b, f[e + 6], 23, 76029189), b = j(b, a, d, c, f[e + 9], 4, 3654602809), 40 | c = j(c, b, a, d, f[e + 12], 11, 3873151461), d = j(d, c, b, a, f[e + 15], 16, 530742520), a = j(a, d, c, b, f[e + 2], 23, 41 | 3299628645), b = k(b, a, d, c, f[e + 0], 6, 4096336452), c = k(c, b, a, d, f[e + 7], 10, 1126891415), d = k(d, c, b, a, f[e + 14], 15, 42 | 2878612391), a = k(a, d, c, b, f[e + 5], 21, 4237533241), b = k(b, a, d, c, f[e + 12], 6, 1700485571), c = k(c, b, a, d, f[e + 3], 10, 43 | 2399980690), d = k(d, c, b, a, f[e + 10], 15, 4293915773), a = k(a, d, c, b, f[e + 1], 21, 2240044497), b = k(b, a, d, c, f[e + 8], 6, 44 | 1873313359), c = k(c, b, a, d, f[e + 15], 10, 4264355552), d = k(d, c, b, a, f[e + 6], 15, 2734768916), a = k(a, d, c, b, f[e + 13], 21, 45 | 1309151649), b = k(b, a, d, c, f[e + 4], 6, 4149444226), c = k(c, b, a, d, f[e + 11], 10, 3174756917), d = k(d, c, b, a, f[e 46 | + 2], 15, 718787259), a = k(a, d, c, b, f[e + 9], 21, 3951481745), b = h(b, m), a = h(a, n), d = h(d, o), c = h(c, p); return (l(b) + l(a) + 47 | l(d) + l(c)).toLowerCase() 48 | } 49 | loginEncryption = function (e, h) { 50 | var g = Math.floor(1163531501 * Math.random()) + 15441, i = Math.floor(1163531502 * Math.random()) + 0, j = 51 | Math.floor(1163531502 * Math.random()) + 0, k = Math.floor(1163531502 * Math.random()) + 0, g = baseEncryption("J" + g), i = 52 | baseEncryption("E" + i), j = baseEncryption("R" + j), k = baseEncryption("Y" + k), e = baseEncryption(e + encA1(g)), h = baseEncryption(e + h 53 | + "JERRY" + encA1(i)), l = baseEncryption(e + h + "KUAS" + encA1(j)), l = baseEncryption(l + e + encA1("ITALAB") + encA1(k)), l = 54 | baseEncryption(l + h + "MIS" + k); return '{ a:"' + l + '",b:"' + 55 | g + '",c:"' + i + '",d:"' + j + '",e:"' + k + '",f:"' + h + '" }' 56 | }; function encA2(e) { return baseEncryption(e) }; 57 | function encA1(e) {var r = e;r = encA2(e+ '77460');r = encA2('78398' + e);r = encA2(e+ '9F0E75318F99D12A92FB1F4BA3507B7B');r 58 | = encA2(e+ '8991');return r;}; 59 | function gets(pwd){ 60 | return loginEncryption(pwd , new Date().getTime()); 61 | } 62 | function getTime(){ 63 | return new Date().getTime(); 64 | } 65 | """ 66 | def getRealTime(timestamp): 67 | return datetime.datetime.fromtimestamp(int(timestamp)/10000000 - 62135596800).strftime("%Y-%m-%d %H:%M") 68 | 69 | def login(session, uid, pwd): 70 | data = {} 71 | data['account'] = uid 72 | data['password'] = pwd 73 | try: 74 | data['n'] = js.call('loginEncryption', str(uid), str(pwd)) 75 | except: 76 | return False 77 | res = session.post('http://bus.kuas.edu.tw/API/Users/login', data=data) 78 | return True 79 | 80 | def query(session, y, m, d, operation="全部"): 81 | data = { 82 | 'data':'{"y": \'%s\',"m": \'%s\',"d": \'%s\'}' % (y, m, d), 83 | 'operation': operation, 84 | 'page':1, 85 | 'start':0, 86 | 'limit':90 87 | } 88 | res = session.post('http://bus.kuas.edu.tw/API/Frequencys/getAll', data=data) 89 | resource = json.loads(res.content) 90 | returnData = [] 91 | 92 | if not resource['data']: 93 | return [] 94 | 95 | for i in resource['data']: 96 | d = "%s,%s,%s" % (i['busId'], getRealTime(i['runDateTime']), i['endStation']) 97 | returnData.append(d) 98 | 99 | return returnData 100 | 101 | def init(session): 102 | global js 103 | session.get('http://bus.kuas.edu.tw/') 104 | js = execjs.compile(js_function + session.get('http://bus.kuas.edu.tw/API/Scripts/a1').content) 105 | 106 | def check(session): 107 | print "Checking data" 108 | data = collections.OrderedDict() 109 | for n in xrange(0, 16): 110 | Date = datetime.datetime.strptime(str(datetime.date.today()), "%Y-%m-%d") 111 | EndDate = Date + datetime.timedelta(days=n) 112 | result = query(session, *str(EndDate)[0:10].split('-')) 113 | subdata = [] 114 | for i in result: 115 | subdata.append(i) 116 | data[str(EndDate)] = subdata 117 | if not os.path.isfile("./tmp"): 118 | print "Database create" 119 | with open('tmp', 'a') as tmp: 120 | tmp.write(json.dumps(data)) 121 | else: 122 | files = open('tmp', 'rb') 123 | content = files.readlines() 124 | if not json.dumps(data) == content[0]: 125 | print "Data rewrite" 126 | with open('tmp', 'wb') as tmp: 127 | tmp.write(json.dumps(data)) 128 | else: 129 | print "No Change, Wait next check" 130 | Process(target=check, args={session}).start() 131 | 132 | if __name__ == '__main__': 133 | session = requests.session() 134 | init(session) 135 | login(session, '1102108131', '111') 136 | Process(target=check, args={session}).start() -------------------------------------------------------------------------------- /src/kuas_api/kuas/bus.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | import requests 4 | import execjs 5 | import json 6 | import time 7 | import datetime 8 | 9 | 10 | js_function = """ 11 | function baseEncryption(e) { 12 | function h(b, a) { var d, c, e, f, g; e = b & 2147483648; f = a & 2147483648; d = b & 1073741824; c = a & 1073741824; g = (b & 13 | 1073741823) + (a & 1073741823); return d & c ? g ^ 2147483648 ^ e ^ f : d | c ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e 14 | ^ f : g ^ e ^ f } function g(b, a, d, c, e, f, g) { b = h(b, h(h(a & d | ~a & c, e), g)); return h(b << f | b >>> 32 - f, a) } function i(b, 15 | a, d, c, e, f, g) { b = h(b, h(h(a & c | d & ~c, e), g)); return h(b << f | b >>> 32 - f, a) } function j(b, a, c, d, e, f, g) { b = h(b, 16 | h(h(a ^ c ^ d, e), g)); return h(b << f | b >>> 32 - f, a) } function k(b, a, c, d, e, f, g) { 17 | b = h(b, h(h(c ^ 18 | (a | ~d), e), g)); return h(b << f | b >>> 32 - f, a) 19 | } function l(b) { var a = "", c = "", d; for (d = 0; 3 >= d; d++) c = b >>> 8 * d & 255, c = "0" + c.toString(16), a += 20 | c.substr(c.length - 2, 2); return a } var f = [], m, n, o, p, b, a, d, c, f = function (b) { var a, c = b.length; a = c + 8; for (var d = 16 21 | * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;) a = (g - g % 4) / 4, f = 8 * (g % 4), e[a] |= b.charCodeAt(g) << f, g++; 22 | a = (g - g % 4) / 4; e[a] |= 128 << 8 * (g % 4); e[d - 2] = c << 3; e[d - 1] = c >>> 29; return e } (e); b = 1732584193; a = 4023233417; d = 23 | 2562383102; c = 271733878; for (e = 0; e < f.length; e += 16) m = b, n = a, o = d, p = c, b = g(b, a, d, c, f[e + 24 | 0], 7, 3614090360), c = g(c, b, a, d, f[e + 1], 12, 3905402710), d = g(d, c, b, a, f[e + 2], 17, 606105819), a = g(a, d, c, 25 | b, f[e + 3], 22, 3250441966), b = g(b, a, d, c, f[e + 4], 7, 4118548399), c = g(c, b, a, d, f[e + 5], 12, 1200080426), d = g(d, c, b, a, f[e 26 | + 6], 17, 2821735955), a = g(a, d, c, b, f[e + 7], 22, 4249261313), b = g(b, a, d, c, f[e + 8], 7, 1770035416), c = g(c, b, a, d, f[e + 9], 27 | 12, 2336552879), d = g(d, c, b, a, f[e + 10], 17, 4294925233), a = g(a, d, c, b, f[e + 11], 22, 2304563134), b = g(b, a, d, c, f[e + 12], 7, 28 | 1804603682), c = g(c, b, a, d, f[e + 13], 12, 4254626195), d = g(d, c, b, a, f[e + 14], 17, 2792965006), a = g(a, d, 29 | c, b, f[e + 15], 22, 1236535329), b = i(b, a, d, c, f[e + 1], 5, 4129170786), c = i(c, b, a, d, f[e + 6], 9, 3225465664), d = 30 | i(d, c, b, a, f[e + 11], 14, 643717713), a = i(a, d, c, b, f[e + 0], 20, 3921069994), b = i(b, a, d, c, f[e + 5], 5, 3593408605), c = i(c, b, 31 | a, d, f[e + 10], 9, 38016083), d = i(d, c, b, a, f[e + 15], 14, 3634488961), a = i(a, d, c, b, f[e + 4], 20, 3889429448), b = i(b, a, d, c, 32 | f[e + 9], 5, 568446438), c = i(c, b, a, d, f[e + 14], 9, 3275163606), d = i(d, c, b, a, f[e + 3], 14, 4107603335), a = i(a, d, c, b, f[e + 33 | 8], 20, 1163531501), b = i(b, a, d, c, f[e + 13], 5, 2850285829), c = i(c, b, a, d, f[e + 2], 9, 4243563512), d = i(d, 34 | c, b, a, f[e + 7], 14, 1735328473), a = i(a, d, c, b, f[e + 12], 20, 2368359562), b = j(b, a, d, c, f[e + 5], 4, 4294588738), 35 | c = j(c, b, a, d, f[e + 8], 11, 2272392833), d = j(d, c, b, a, f[e + 11], 16, 1839030562), a = j(a, d, c, b, f[e + 14], 23, 4259657740), b = 36 | j(b, a, d, c, f[e + 1], 4, 2763975236), c = j(c, b, a, d, f[e + 4], 11, 1272893353), d = j(d, c, b, a, f[e + 7], 16, 4139469664), a = j(a, d, 37 | c, b, f[e + 10], 23, 3200236656), b = j(b, a, d, c, f[e + 13], 4, 681279174), c = j(c, b, a, d, f[e + 0], 11, 3936430074), d = j(d, c, b, a, 38 | f[e + 3], 16, 3572445317), a = j(a, d, c, b, f[e + 6], 23, 76029189), b = j(b, a, d, c, f[e + 9], 4, 3654602809), 39 | c = j(c, b, a, d, f[e + 12], 11, 3873151461), d = j(d, c, b, a, f[e + 15], 16, 530742520), a = j(a, d, c, b, f[e + 2], 23, 40 | 3299628645), b = k(b, a, d, c, f[e + 0], 6, 4096336452), c = k(c, b, a, d, f[e + 7], 10, 1126891415), d = k(d, c, b, a, f[e + 14], 15, 41 | 2878612391), a = k(a, d, c, b, f[e + 5], 21, 4237533241), b = k(b, a, d, c, f[e + 12], 6, 1700485571), c = k(c, b, a, d, f[e + 3], 10, 42 | 2399980690), d = k(d, c, b, a, f[e + 10], 15, 4293915773), a = k(a, d, c, b, f[e + 1], 21, 2240044497), b = k(b, a, d, c, f[e + 8], 6, 43 | 1873313359), c = k(c, b, a, d, f[e + 15], 10, 4264355552), d = k(d, c, b, a, f[e + 6], 15, 2734768916), a = k(a, d, c, b, f[e + 13], 21, 44 | 1309151649), b = k(b, a, d, c, f[e + 4], 6, 4149444226), c = k(c, b, a, d, f[e + 11], 10, 3174756917), d = k(d, c, b, a, f[e 45 | + 2], 15, 718787259), a = k(a, d, c, b, f[e + 9], 21, 3951481745), b = h(b, m), a = h(a, n), d = h(d, o), c = h(c, p); return (l(b) + l(a) + 46 | l(d) + l(c)).toLowerCase() 47 | } 48 | loginEncryption = function (e, h) { 49 | var g = Math.floor(1163531501 * Math.random()) + 15441, i = Math.floor(1163531502 * Math.random()) + 0, j = 50 | Math.floor(1163531502 * Math.random()) + 0, k = Math.floor(1163531502 * Math.random()) + 0, g = baseEncryption("J" + g), i = 51 | baseEncryption("E" + i), j = baseEncryption("R" + j), k = baseEncryption("Y" + k), e = baseEncryption(e + encA1(g)), h = baseEncryption(e + h 52 | + "JERRY" + encA1(i)), l = baseEncryption(e + h + "KUAS" + encA1(j)), l = baseEncryption(l + e + encA1("ITALAB") + encA1(k)), l = 53 | baseEncryption(l + h + "MIS" + k); return '{ a:"' + l + '",b:"' + 54 | g + '",c:"' + i + '",d:"' + j + '",e:"' + k + '",f:"' + h + '" }' 55 | }; function encA2(e) { return baseEncryption(e) }; 56 | function encA1(e) {var r = e;r = encA2(e+ '77460');r = encA2('78398' + e);r = encA2(e+ '9F0E75318F99D12A92FB1F4BA3507B7B');r 57 | = encA2(e+ '8991');return r;}; 58 | function gets(password){ 59 | return loginEncryption(password , new Date().getTime()); 60 | } 61 | function getTime(){ 62 | return new Date().getTime(); 63 | } 64 | """ 65 | 66 | 67 | headers = {"User-Agnet": 68 | "Mozilla/5.0 (X11; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/35.0"} 69 | 70 | 71 | # Bus url setting 72 | BUS_URL = "http://bus.kuas.edu.tw" 73 | BUS_SCRIPT_URL = "http://bus.kuas.edu.tw/API/Scripts/a1" 74 | BUS_API_URL = "http://bus.kuas.edu.tw/API/" 75 | BUS_LOGIN_URL = BUS_API_URL + "Users/login" 76 | BUS_FREQ_URL = BUS_API_URL + "Frequencys/getAll" 77 | BUS_RESERVE_URL = BUS_API_URL + "Reserves/getOwn" 78 | BUS_BOOK_URL = BUS_API_URL + "Reserves/add" 79 | BUS_UNBOOK_URL = BUS_API_URL + "Reserves/remove" 80 | 81 | # Bus timeout setting 82 | BUS_TIMEOUT = 1.0 83 | 84 | 85 | def _get_real_time(timestamp): 86 | return datetime.datetime.fromtimestamp(int(timestamp) / 10000000 - 62135596800).strftime("%Y-%m-%d %H:%M") 87 | 88 | 89 | def status(): 90 | """Return Bus server status code 91 | 92 | :rtype: int 93 | :returns: A HTTP status code 94 | 95 | >>> status() 96 | 200 97 | """ 98 | 99 | try: 100 | bus_status_code = requests.head( 101 | BUS_URL, timeout=BUS_TIMEOUT).status_code 102 | except requests.exceptions.Timeout: 103 | bus_status_code = 408 104 | 105 | return bus_status_code 106 | 107 | 108 | def init(session): 109 | session.head(BUS_URL) 110 | script_content = session.get(BUS_SCRIPT_URL, headers=headers).text 111 | 112 | js = execjs.compile(js_function + script_content) 113 | 114 | return js 115 | 116 | 117 | def login(session, username, password): 118 | """Login to KUAS Bus system. 119 | 120 | :param session: requests session object 121 | :type session: class requests.sessions.Session 122 | :param username: username of kuas bus system, actually your kuas student id 123 | :type username: str or int 124 | :param password: password of kuas ap system. 125 | :type password: str or int 126 | 127 | :return: login status 128 | :rtype: bool 129 | """ 130 | 131 | data = {'account': username, 'password': password} 132 | 133 | try: 134 | js = init(session) 135 | data['n'] = js.call('loginEncryption', str(username), str(password)) 136 | except: 137 | return False 138 | 139 | content = session.post(BUS_LOGIN_URL, 140 | data=data, 141 | headers=headers, 142 | timeout=BUS_TIMEOUT 143 | ).text 144 | 145 | resp = json.loads(content) 146 | 147 | return resp['success'] 148 | 149 | 150 | def query(session, y, m, d, operation="全部"): 151 | """ 152 | Query kuas bus timetable 153 | 154 | :param session: requests session object 155 | :type session: class requests.sessions.Session 156 | :param y: year, using common era 157 | :type y: int 158 | :param m: month 159 | :type m: int 160 | :param d: day 161 | :type d: int 162 | :param operation: choosing bus start from yanchao or jiangong, or all. 163 | :type operation: str 164 | 165 | >>> s = requests.Session() 166 | >>> login(s, "1102108133", "111") 167 | True 168 | 169 | >>> type(query(s, "2015", "6", "15")) 170 | 171 | 172 | >>> type(query(s, *'2014-10-08'.split("-"))) 173 | 174 | 175 | """ 176 | data = { 177 | 'data': '{"y": "%s","m": "%s","d": "%s"}' % (y, m, d), 178 | 'operation': operation, 179 | 'page': 1, 180 | 'start': 0, 181 | 'limit': 90 182 | } 183 | 184 | resp = session.post(BUS_FREQ_URL, data=data, headers=headers) 185 | 186 | resource = json.loads(resp.text) 187 | returnData = [] 188 | 189 | if not resource['data']: 190 | return [] 191 | 192 | for i in resource['data']: 193 | Data = {} 194 | Data['EndEnrollDateTime'] = _get_real_time(i['EndEnrollDateTime']) 195 | Data['runDateTime'] = _get_real_time(i['runDateTime']) 196 | Data['Time'] = Data['runDateTime'][-5:] 197 | Data['endStation'] = i['endStation'] 198 | Data['busId'] = i['busId'] 199 | Data['reserveCount'] = i['reserveCount'] 200 | Data['limitCount'] = i['limitCount'] 201 | Data['isReserve'] = int(i['isReserve']) + 1 202 | Data['SpecialTrain'] = i['SpecialTrain'] 203 | Data['SpecialTrainRemark'] = i['SpecialTrainRemark'] 204 | 205 | returnData.append(Data) 206 | 207 | return returnData 208 | 209 | 210 | def reserve(session): 211 | """Query user reserve bus. 212 | """ 213 | 214 | data = { 215 | 'page': 1, 216 | 'start': 0, 217 | 'limit': 90 218 | } 219 | 220 | content = session.post(BUS_RESERVE_URL, data=data, headers=headers).text 221 | 222 | resource = json.loads(content) 223 | 224 | rd = [] 225 | 226 | if(resource['data'] is not None): 227 | for i in resource['data']: 228 | data = {} 229 | data['time'] = _get_real_time(i['time']) 230 | data['endTime'] = _get_real_time(i['endTime']) 231 | data['cancelKey'] = i['key'] 232 | data['end'] = i['end'] 233 | rd.append(data) 234 | 235 | result = sorted(rd, key=lambda k: k['time']) 236 | 237 | return result 238 | 239 | 240 | def book(session, kid, action=None): 241 | if not action: 242 | res = session.post(BUS_BOOK_URL, 243 | data="{busId: %s}" % (kid), 244 | headers=headers, 245 | ) 246 | else: 247 | # Then compare users reserve bus, 248 | # if kid is same as time, then found the correct bus, 249 | # then we can unbook this bus. 250 | 251 | res = session.post(BUS_UNBOOK_URL, 252 | data="{reserveId: %d}" % (kid) + "}", 253 | headers=headers, 254 | ) 255 | 256 | resource = json.loads(str(res.content, "utf-8")) 257 | 258 | return resource 259 | 260 | 261 | if __name__ == '__main__': 262 | #import doctest 263 | #doctest.testmod() 264 | #exit() 265 | 266 | session = requests.session() 267 | init(session) 268 | login(session, '1102108133', '111') 269 | 270 | t = time.time() 271 | print(query(session, *'2014-10-08'.split("-"))) 272 | print(time.time() - t) 273 | exit() 274 | #book(session, '22868', '') 275 | 276 | print("---------------------") 277 | print(reserve(session)) 278 | book(session, '741583', 'un') 279 | print(reserve(session)) 280 | """ 281 | result = query('2014', '6', '27') 282 | for i in result: 283 | if book(i['busId']) : 284 | print "Book Success" 285 | result = reserve() 286 | for i in result: 287 | if book(i['key'], "Un") : 288 | print "UnBook Success" 289 | """ 290 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | import redis 6 | import hashlib 7 | import requests 8 | from werkzeug.contrib.cache import SimpleCache 9 | 10 | import kuas_api.kuas.ap as ap 11 | import kuas_api.kuas.leave as leave 12 | import kuas_api.kuas.parse as parse 13 | import kuas_api.kuas.bus as bus 14 | import kuas_api.kuas.notification as notification 15 | import kuas_api.kuas.news as news 16 | from lxml import etree 17 | 18 | AP_QUERY_EXPIRE = 3600 19 | BUS_EXPIRE_TIME = 0 20 | SERVER_STATUS_EXPIRE_TIME = 180 21 | NOTIFICATION_EXPIRE_TIME = 3600 22 | 23 | BUS_QUERY_TAG = "bus" 24 | NOTIFICATION_TAG = "notification" 25 | 26 | #: AP guest account 27 | AP_GUEST_ACCOUNT = "guest" 28 | 29 | #: AP guest password 30 | AP_GUEST_PASSWORD = "123" 31 | 32 | s_cache = SimpleCache() 33 | red = redis.StrictRedis.from_url(url=os.environ['REDIS_URL'], db=2) 34 | SECRET_KEY = red.get("SECRET_KEY") if red.exists( 35 | "SECRET_KEY") else str(os.urandom(32)) 36 | # Only use in cache.login , get encoded data from redis. 37 | # get data from redis should be able use without any decode or encode action. 38 | red_auth = redis.StrictRedis.from_url( 39 | url=os.environ['REDIS_URL'], db=2, charset="utf-8", decode_responses=True) 40 | 41 | 42 | def dump_session_cookies(session, is_login): 43 | """Dumps cookies to list 44 | """ 45 | 46 | cookies = [] 47 | for c in session.cookies: 48 | cookies.append({ 49 | 'name': c.name, 50 | 'domain': c.domain, 51 | 'value': c.value}) 52 | 53 | return {'is_login': is_login, 'cookies': cookies} 54 | 55 | 56 | def login(username, password): 57 | session = requests.Session() 58 | is_login = {} 59 | 60 | if red_auth.exists(username): 61 | user_redis_cookies = red_auth.get(username) 62 | return json.loads(user_redis_cookies) 63 | 64 | # AP Login 65 | try: 66 | is_login["ap"] = ap.login(session, username, password) 67 | except: 68 | is_login["ap"] = False 69 | 70 | # Login bus system 71 | try: 72 | bus.init(session) 73 | is_login["bus"] = bus.login(session, username, password) 74 | except: 75 | is_login["bus"] = False 76 | 77 | # Login leave system 78 | try: 79 | is_login["leave"] = leave.login(session, username, password) 80 | except: 81 | is_login["leave"] = False 82 | if is_login["ap"]: 83 | return dump_session_cookies(session, is_login) 84 | else: 85 | return False 86 | 87 | 88 | def ap_query(session, qid=None, args=None, 89 | username=None, expire=AP_QUERY_EXPIRE): 90 | ap_query_key_tag = str(username) + str(args) + str(SECRET_KEY) 91 | ap_query_key = qid + \ 92 | hashlib.sha512( 93 | bytes(ap_query_key_tag, "utf-8")).hexdigest() 94 | 95 | if not red.exists(ap_query_key): 96 | ap_query_content = parse.parse(qid, ap.query(session, qid, args)) 97 | 98 | red.set(ap_query_key, json.dumps(ap_query_content, ensure_ascii=False)) 99 | red.expire(ap_query_key, expire) 100 | else: 101 | ap_query_content = json.loads(red.get(ap_query_key)) 102 | 103 | return ap_query_content 104 | 105 | 106 | def leave_query(session, year="102", semester="2"): 107 | return leave.getList(session, year, semester) 108 | 109 | 110 | def leave_submit(session, start_date, end_date, 111 | reason_id, reason_text, section): 112 | leave_dict = {"reason_id": reason_id, 113 | "reason_text": reason_text, "section": section} 114 | 115 | return leave.submitLeave(session, start_date, end_date, leave_dict) 116 | 117 | 118 | def bus_query(session, date): 119 | bus_cache_key = BUS_QUERY_TAG + date.replace("-", "") 120 | 121 | if not red.exists(bus_cache_key): 122 | bus_q = bus.query(session, *date.split("-")) 123 | 124 | red.set(bus_cache_key, json.dumps(bus_q, ensure_ascii=False)) 125 | red.expire(bus_cache_key, BUS_EXPIRE_TIME) 126 | else: 127 | bus_q = json.loads(red.get(bus_cache_key)) 128 | 129 | # Check if have reserve, and change isReserve value to 0 130 | reserve = bus_reserve_query(session) 131 | 132 | for q in bus_q: 133 | q['isReserve'] = 0 134 | q['cancelKey'] = 0 135 | 136 | for r in reserve: 137 | if (r['time'] == q['runDateTime'] and 138 | r['end'] == q['endStation']): 139 | q['isReserve'] = 1 140 | q['cancelKey'] = r['cancelKey'] 141 | break 142 | 143 | return bus_q 144 | 145 | 146 | def bus_reserve_query(session): 147 | return bus.reserve(session) 148 | 149 | 150 | def bus_booking(session, busId, action): 151 | return bus.book(session, busId, action) 152 | 153 | 154 | def notification_query(page=1): 155 | notification_page = NOTIFICATION_TAG + str(page) 156 | red_query = red.get(notification_page) 157 | red_query = False if red_query is None or red_query == '[]' else True 158 | 159 | if not red_query: 160 | notification_content = notification.get(page) 161 | 162 | red.set(notification_page, 163 | json.dumps(notification_content, ensure_ascii=False)) 164 | red.expire(notification_page, NOTIFICATION_EXPIRE_TIME) 165 | else: 166 | notification_content = json.loads(red.get(notification_page)) 167 | 168 | return notification_content 169 | 170 | 171 | def news_query(): 172 | return news.news() 173 | 174 | 175 | def news_status(): 176 | return news.news_status() 177 | 178 | 179 | def server_status(): 180 | if not s_cache.get("server_status"): 181 | ap_status = ap.status() 182 | leave_status = leave.status() 183 | bus_status = bus.status() 184 | 185 | server_status = [ap_status, leave_status, bus_status] 186 | 187 | s_cache.set( 188 | "server_status", server_status, timeout=SERVER_STATUS_EXPIRE_TIME) 189 | else: 190 | server_status = s_cache.get("server_status") 191 | 192 | return server_status 193 | 194 | 195 | def get_semester_list(): 196 | """Get semester list from ap system. 197 | 198 | :rtype: dict 199 | 200 | >>> get_semester_list()[-1]['value'] 201 | '92,2' 202 | """ 203 | 204 | s = requests.Session() 205 | ap.login(s, AP_GUEST_ACCOUNT, AP_GUEST_PASSWORD) 206 | 207 | content = ap_query(s, "ag304_01") 208 | if len(content) < 3000: 209 | return False 210 | root = etree.HTML(content) 211 | 212 | #options = root.xpath("id('yms_yms')/option") 213 | try: 214 | options = map(lambda x: {"value": x.values()[0].replace("#", ","), 215 | "selected": 1 if "selected" in x.values() else 0, 216 | "text": x.text}, 217 | root.xpath("id('yms_yms')/option") 218 | ) 219 | except: 220 | return False 221 | 222 | options = list(options) 223 | 224 | return options 225 | 226 | 227 | if __name__ == "__main__": 228 | s = requests.Session() 229 | is_login = login(s, "guest", "123") 230 | 231 | print(is_login) 232 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/job.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | 3 | import requests 4 | import re 5 | import time 6 | import lxml 7 | import json 8 | from bs4 import BeautifulSoup 9 | import ap 10 | from pprint import pprint 11 | 12 | #Configuration 13 | session = requests.session() 14 | username = "" 15 | password = "" 16 | page_Num = 1 17 | show_Full = True 18 | url = "http://140.127.113.136/StuPartTime/Announce/stu_announce_view.aspx?VCHASID=" 19 | 20 | if not ap.login(session, username, password): 21 | print "登入失敗" 22 | 23 | 24 | #用lxml 解析 25 | def viewPage_lxml(page_Num, show_Full, username): 26 | response = session.get(url+username) 27 | tree = lxml.etree.HTML(response.content) 28 | 29 | #不是第一頁的話,需要抓取google 分析的input 30 | if page_Num != 1: 31 | X = tree.xpath(u"//input[@name='__VIEWSTATE']")[0].values()[3] 32 | Y = tree.xpath(u"//input[@name='__EVENTVALIDATION']")[0].values()[3] 33 | form = { 34 | "__EVENTTARGET":"GridView1", 35 | "__EVENTARGUMENT":"Page$%s"%page_Num, 36 | "__VIEWSTATE":X, 37 | "__EVENTVALIDATION":Y 38 | } 39 | response = session.post(url, data=form) 40 | tree = lxml.etree.HTML(response.content) 41 | 42 | #tree.xpath(u"//table[@id='GridView1']//tr//td//span[contains(concat(' ', @id, ' '), 'Label1')]") 43 | 44 | 45 | 46 | 47 | for x in table[0]: 48 | print x.text 49 | return 0 50 | if not show_Full: 51 | if x.text != "Y": 52 | id_list.append(x['id']) 53 | else: 54 | id_list.append(x['id']) 55 | 56 | 57 | for x in id_list: 58 | index = str(x).replace("lblFull", "") 59 | 60 | data = [] 61 | #單號 62 | #data.append(bs.find('span', id=index+"Label1").text) 63 | #刊登日 64 | #data.append(bs.find('span', id=index+"Label2").text) 65 | #人數 取得刊登日parent.next 66 | print bs.find('span', id=index+"Label2").next_sibling 67 | #工作時間 68 | #data.append(bs.find('span', id=index+"Label3").text) 69 | #條件 70 | #data.append(bs.find('span', id=index+"Label4").text) 71 | #需求單位 取得條件parent.next 72 | for x in data: 73 | print x.encode("utf8") 74 | 75 | print "==========================" 76 | 77 | #print tree.xpath(u"//span[@id=re.compile(r'Label2$')]") 78 | 79 | 80 | #".//div[starts-with(@id,'comment-')" 81 | 82 | 83 | #以下開始 84 | # viewPage_lxml(1, show_Full, username) 85 | response = session.get(url+username) 86 | tree = lxml.etree.HTML(response.content) 87 | result = [] 88 | 89 | 90 | #抓取公告編號 91 | ID = [] 92 | for x in tree.xpath(u"//table[@id='GridView1']//tr//td//span[contains(concat(' ', @id, ' '), 'Label1')]"): 93 | ID.append(x.text) 94 | 95 | #抓取刊登時間以及需求人數 96 | post_date = [] 97 | person = [] 98 | for x in tree.xpath(u"//table[@id='GridView1']//tr//td//span[contains(concat(' ', @id, ' '), 'Label2')]"): 99 | post_date.append(x.text) 100 | person.append(x.getparent().getparent().getnext().getchildren()[0].text) 101 | 102 | #抓取時間 103 | work_time = [] 104 | for x in tree.xpath(u"//table[@id='GridView1']//tr//td//span[contains(concat(' ', @id, ' '), 'Label3')]"): 105 | work_time.append(x.text) 106 | 107 | #抓取需求、聯絡人、電話、需求單位 108 | work_required = [] 109 | contact_name = [] 110 | contact_number = [] 111 | contact_org = [] 112 | for x in tree.xpath(u"//table[@id='GridView1']//tr//td//span[contains(concat(' ', @id, ' '), 'Label4')]"): 113 | #這個是工作需求,但是中文還沒搞定 114 | work_required.append(x.text) 115 | 116 | #因為聯絡人、電話、需求單位沒有特徵可以直接取得,所以使用以下方法 117 | contact_name_tag = x.getparent().getparent().getnext() 118 | #聯絡人姓名,但是中文還沒搞定 119 | contact_name.append(contact_name_tag.getchildren()[0].text) 120 | 121 | #取得電話 122 | contact_number_tag = contact_name_tag.getnext() 123 | contact_number.append(contact_number_tag.getchildren()[0].text) 124 | 125 | #取得需求單位,但是中文還沒搞定 126 | contact_org_tag = contact_number_tag.getnext() 127 | contact_org.append(contact_org_tag.getchildren()[0].text) 128 | 129 | 130 | total = [ID, post_date, person, work_time, work_required, contact_name, contact_number, contact_org] 131 | 132 | 133 | for i, v in enumerate(total): 134 | total[i] = eval(str(v).replace("u\'", "\'")) 135 | 136 | total = json.dumps(total) 137 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/kalendar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf -*- 2 | 3 | import requests 4 | from lxml import etree 5 | 6 | 7 | def get_wtf_data(): 8 | r = requests.post('http://active.kuas.edu.tw/EPortfolio/Activity/UnitBsCalendar.aspx') 9 | 10 | root = etree.HTML(r.text) 11 | 12 | result = {} 13 | for i in root.xpath("//input[@type='hidden']"): 14 | result[i.values()[-2]] = i.values()[-1] 15 | 16 | return result 17 | 18 | 19 | def get_admin_calendar(year, semester): 20 | data = { 21 | "ContentPlaceHolder1_ToolkitScriptManager1_HiddenField": ";;AjaxControlToolkit,+Version=4.1.60919.0,+Culture=neutral,+PublicKeyToken=28f01b0e84b6d53e:zh-TW:ee051b62-9cd6-49a5-87bb-93c07bc43d63:de1feab2:f9cec9bc:a0b0f951:a67c2700:fcf0e993:f2c8e708:720a52bf:589eaa30:698129cf:fb9b4c57:ccb96cf9", 22 | "__EVENTTARGET": "", 23 | "__EVENTARGUMENT": "", 24 | "ContentPlaceHolder1_TabContainer1_ClientState": '{"ActiveTabIndex":1,"TabEnabledState":[true,true,true,true,true],"TabWasLoadedOnceState":[true,true,false,false,false]}', 25 | "__LASTFOCUS": "", 26 | "__VIEWSTATE": "/wEPDwUKMTI1OTYxMzEwMA8WAh4XRGVmYXVsdFNjaFllYXJTZW1TdHJpbmcFBTEwMy0yFgJmD2QWAgIED2QWAgIBD2QWBgIBD2QWAgIBD2QWBAIBDw8WAh4HVmlzaWJsZWhkFgQCAw8PFgIeBFRleHQFD+iri+i8uOWFpeW4s+iZn2RkAgcPDxYEHgRNb2RlCyolU3lzdGVtLldlYi5VSS5XZWJDb250cm9scy5UZXh0Qm94TW9kZQAfAgUP6KuL6Ly45YWl5a+G56K8ZGQCAw8PFgIfAWdkFgQCAQ8PFgIfAgUZ5Zub6LOH5bel5LiJ55SyLeWRgue0ueamlWRkAgMPDxYCHwIFHeatoei/juS9v+eUqCDmpa3li5nooYzkuovmm4YhZGQCAw9kFgICAQ88KwANAQwUKwACBQcyOjAsMDowFCsAAhYEHwIFD+alreWLmeihjOS6i+abhh4FVmFsdWUFD+alreWLmeihjOS6i+abhmQWAmYPZBYCZg8VAQ/mpa3li5nooYzkuovmm4ZkAgUPZBYCAgEPZBYCAgEPZBYCAgMPDxYEHghUYWJJbmRleAEAAB4STGFzdEFjdGl2ZVRhYkluZGV4AgFkFgZmD2QWAmYPZBYCAgEPZBYEAgEPEA8WBh4NRGF0YVRleHRGaWVsZAUQU2NoWWVhclNlbVN0cmluZx4ORGF0YVZhbHVlRmllbGQFD1NjaFllYXJTZW1WYWx1ZR4LXyFEYXRhQm91bmRnZBAVBAcxMDQg5LiLBzEwNCDkuIoHMTAzIOS4iwcxMDMg5LiKFQQFMTA0LTIFMTA0LTEFMTAzLTIFMTAzLTEUKwMEZ2dnZ2RkAgcPZBYCAgEPFgIeA3NyYwVRaHR0cDovL2FjdGl2ZS5rdWFzLmVkdS50dy9FUG9ydGZvbGlvL0FjdGl2aXR5L0RvY3VtZW50L0JzL0NhbGVuZGFyL0JzXzEwMy3kuIsucGRmZAIBD2QWAmYPZBYCAgEPZBYEAgEPEA8WBh8HBRBTY2hZZWFyU2VtU3RyaW5nHwgFD1NjaFllYXJTZW1WYWx1ZR8JZ2QQFQQHMTA0IOS4iwcxMDQg5LiKBzEwMyDkuIsHMTAzIOS4ihUEBTEwNC0yBTEwNC0xBTEwMy0yBTEwMy0xFCsDBGdnZ2dkZAIDDxAPFgYfBwUKdXBVbml0TmFtZR8IBQh1cFVuaXRpZB8JZ2QQFRMM5YWo6YOo6aGv56S6CeaVmeWLmeiZlQnkuLvoqIjlrqQP6YCy5L+u5o6o5buj6JmVDOmAsuS/ruWtuOmZognnuL3li5nomZUY55Kw5aKD5a6J5YWo6KGb55Sf5Lit5b+DGOioiOeul+apn+iIh+e2sui3r+S4reW/gwnlnJbmm7jppKgJ5Lq65LqL5a6kCemrlOiCsuWupAzlia/moKHplbflrqQJ56CU55m86JmVD+Wci+mam+S6i+WLmeiZlRjmoKHlj4voga/ntaHogbfmtq/kuK3lv4MJ5a245YuZ6JmVCeenmOabuOWupBLpgJrorZjmlZnogrLkuK3lv4MP54eV5bei5qCh5YuZ6YOoFRMBQQRBQTAwBEFDMDAEQUUwMARBVDAwBEdBMDAER0IwMARJQzAwBExCMDAEUEUwMARQSDAwBFBTMDAEUkEwMARSQjAwBFJEMDAEU0EwMARTRTAwBFhDMDAEWUQwMBQrAxNnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZGQCBA9kFgJmD2QWAgIBD2QWDAIFDxBkZBYBZmQCBw8QZGQWAGQCEQ9kFgICBQ9kFgQCAQ8QZGQWAQIIZAIDDxBkZBYBZmQCEw9kFgICBQ9kFgQCAQ8QZGQWAQIIZAIDDxBkZBYBZmQCFQ8PFgIfAgUS5qWt5YuZ5rS75YuV5p+l6KmiZGQCGQ88KwARAQEQFgAWABYAZBgDBR5fX0NvbnRyb2xzUmVxdWlyZVBvc3RCYWNrS2V5X18WAQUnY3RsMDAkQ29udGVudFBsYWNlSG9sZGVyMSRUYWJDb250YWluZXIxBSdjdGwwMCRDb250ZW50UGxhY2VIb2xkZXIxJFRhYkNvbnRhaW5lcjEPD2QCAWQFTGN0bDAwJENvbnRlbnRQbGFjZUhvbGRlcjEkVGFiQ29udGFpbmVyMSRUYWJQYW5lbEJzU2VhcmNoJGd2Rm9yQnNBY3RpdmVTZWFyY2gPZ2RyQEeUQZjPVmQxmQjXEE2h5a2Akm5FOox6NGaKCQuRSg==", 27 | "__EVENTVALIDATION": "/wEWNQLQjpnOBwLe9pSEBwLRovlJAqCG2dcHAqGG2dcHAr/xv7kNArzxv7kNAueA6J4LAtqnpbUMAoaIxIIDAoeIxIIDApn/ouwJApr/ouwJAoOR2+EEAqXQ+IUIAqXQ8IUIAqXQiIYIAqXQzIUIAq/Q+IUIAq/Q9IUIAt3R8IUIAqLQ9IUIAtbRiIYIAtbR3IUIAtbRsIUIAtTR+IUIAtTR9IUIAtTRjIYIAtvR+IUIAtvRiIYIAs7R8IUIAs3RjIYIApW90PwFAuKa6aQNAoWPzooGAqzGkOoMAreinuYKAtSP93EC0pDX/QEC9czo6g0ChJvWOwKOnbn6BALd9PzVDALm9IzLDAKV0ZqFCQLswvqaBgKNu8fRDwKH9+z4CAKM5rDmAQLrtJjFBALpvue2BwLE4euUCQKVtruvDVM6q8J60kK+Rs+JNbFQOsCQU/juUwZnaTkYwDCI5LJO", 28 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBs$DropDownListForBsFormalSchYearSem": "103-2", 29 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsGov$DropDownListForBsGovSchYearSem": "103-2", 30 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsGov$DropDownListForBsGovUnit": "A", 31 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsGov$ButtonSearchForBsGov": "查詢", 32 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsSearch$TextBoxForKeyword": "", 33 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsSearch$DDLForGovOrTeach": "選擇部門", 34 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsSearch$DateTimeBoxForStart$DateFieldBox": "2015/9/11", 35 | "ctl00$ContentPlaceHolder1$TabContainer1$TabPanelBsSearch$DateTimeBoxForEnd$DateFieldBox": "2015/9/11", 36 | } 37 | 38 | hidden_value = get_wtf_data() 39 | for key in hidden_value: 40 | data[key] = hidden_value[key] 41 | 42 | print(data) 43 | 44 | cookies = { 45 | # '_ga': 'GA1.3.518100922.1435057856', 46 | # 'ASP.NET_SessionId': 'jyeffb1tbujhgzju0mpclnaz', 47 | # '_gat': '1', 48 | } 49 | 50 | headers = { 51 | 'Host': 'active.kuas.edu.tw', 52 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101 Firefox/40.0', 53 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 54 | 'Accept-Language': 'en-US,en;q=0.7,zh-TW;q=0.3', 55 | 'Referer': 'http://active.kuas.edu.tw/EPortfolio/Activity/UnitBsCalendar.aspx', 56 | 'Connection': 'keep-alive', 57 | } 58 | 59 | r = requests.post('http://active.kuas.edu.tw/EPortfolio/Activity/UnitBsCalendar.aspx', headers=headers, cookies=cookies, data=data) 60 | 61 | f = open("/tmp/test", "w") 62 | f.write(r.text) 63 | 64 | root = etree.HTML(r.text) 65 | #root.xpath("id('ContentPlaceHolder1_TabContainer1_TabPanelBsGov_TDContent_109')")[0].xpath("a")[7].values() 66 | print(r.text) 67 | td = root.xpath("id('ContentPlaceHolder1_TabContainer1_TabPanelBsGov_TDContent_279')")[0] 68 | td = list(td.itertext())[::2] 69 | a = td.xpath("a") 70 | 71 | for i, j in (td, a): 72 | print(i, j.values()) 73 | 74 | 75 | if __name__ == "__main__": 76 | get_admin_calendar(104, 1) 77 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/leave.py: -------------------------------------------------------------------------------- 1 | #-*- encoding=utf-8 2 | 3 | import requests 4 | from lxml import etree 5 | 6 | s = requests.session() 7 | 8 | SUBMIT_LEAVE_URL = "http://leave.nkust.edu.tw/CK001MainM.aspx" 9 | 10 | TIMEOUT = 5.0 11 | 12 | 13 | def status(): 14 | leave_status = 400 15 | 16 | try: 17 | leave_status = requests.head( 18 | "http://leave.nkust.edu.tw/", timeout=TIMEOUT).status_code 19 | except: 20 | pass 21 | 22 | return leave_status 23 | 24 | 25 | def login(session, username, password): 26 | try: 27 | session.headers.update({ 28 | 'Origin': 'http://leave.nkust.edu.tw', 29 | 'Upgrade-Insecure-Requests': '1', 30 | 'Content-Type': 'application/x-www-form-urlencoded', 31 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 32 | 'Referer': 'http://leave.nkust.edu.tw/LogOn.aspx', 33 | 'Accept-Encoding': 'gzip, deflate', 34 | 'Accept-Language': 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6' 35 | }) 36 | r = session.get("http://leave.nkust.edu.tw/LogOn.aspx", timeout=TIMEOUT) 37 | except requests.exceptions.ReadTimeout: 38 | return False 39 | root = etree.HTML(r.text) 40 | 41 | form = {} 42 | for i in root.xpath("//input"): 43 | form[i.attrib['name']] = "" 44 | if "value" in i.attrib: 45 | form[i.attrib['name']] = i.attrib['value'] 46 | 47 | form['Login1$UserName'] = username 48 | form['Login1$Password'] = password 49 | form['__EVENTTARGET '] = '' 50 | form['__EVENTARGUMENT ']='' 51 | 52 | 53 | r = session.post('http://leave.nkust.edu.tw/LogOn.aspx', data=form) 54 | 55 | root = etree.HTML(r.text) 56 | 57 | if root.xpath("//td[@align='center' and @style='color:Red;' and @colspan='2']"): 58 | return False 59 | else: 60 | return True 61 | 62 | 63 | def getList(session, year="102", semester="2"): 64 | root = etree.HTML( 65 | session.get("http://leave.nkust.edu.tw/AK002MainM.aspx").text) 66 | 67 | form = {} 68 | for i in root.xpath("//input"): 69 | form[i.attrib["name"]] = i.attrib[ 70 | "value"] if "value" in i.attrib else "" 71 | #print(form) 72 | del form['ctl00$ButtonLogOut'] 73 | 74 | form[ 75 | 'ctl00$ContentPlaceHolder1$SYS001$DropDownListYms'] = "%s-%s" % (year, semester) 76 | 77 | r = session.post( 78 | "http://leave.nkust.edu.tw/AK002MainM.aspx", data=form) 79 | root = etree.HTML(r.text) 80 | 81 | tr = root.xpath("//table")[-1] 82 | 83 | leave_list = [] 84 | 85 | # Delete row id, leave id, teacher quote 86 | for r_index, r in enumerate(tr): 87 | r = list(map(lambda x: x.replace("\r", ""). 88 | replace("\n", ""). 89 | replace("\t", ""). 90 | replace(u"\u3000", ""). 91 | replace(" ", ""), 92 | r.itertext() 93 | )) 94 | 95 | if not r[0]: 96 | del r[0] 97 | if not r[-1]: 98 | del r[-1] 99 | 100 | leave_list.append(r) 101 | result = [] 102 | for r in leave_list[1:]: 103 | i = len(r)-15 104 | for approved in range(4,i): 105 | r[3]+= ' , '+r[approved] 106 | leave = { 107 | "leave_sheet_id": r[1].replace("\xa0", ""), 108 | "date": r[2], 109 | "instructors_comment": r[3], 110 | "leave_sections": [ 111 | {"section": leave_list[0][index + 4], "reason": s} 112 | for index,s in enumerate(r[i:]) 113 | ] 114 | } 115 | 116 | leave["leave_sections"] = list( 117 | filter(lambda x: x["reason"], leave["leave_sections"])) 118 | result.append(leave) 119 | return result 120 | 121 | 122 | def submitLeave(session, start_date, end_date, leave_dict): 123 | """Submit leave data to leave.kaus.edu.tw:446 124 | session: The session include login cookies 125 | start_date: Start date for leave 126 | end_date: End date for leave 127 | leave_dict: A dict with data include which section were leave 128 | reason_id: String, 21 ~ 26. 129 | reason_text: String, a reason why leave. 130 | section: List, the number which count on it. 131 | 132 | return (success, value) 133 | success: Bool 134 | value: String 135 | """ 136 | 137 | # First page 138 | r = session.get(SUBMIT_LEAVE_URL) 139 | 140 | root = etree.HTML(r.text) 141 | 142 | d = {i.attrib['name']: i.attrib['value'] for i in root.xpath("//input")} 143 | del d['ctl00$ButtonLogOut'] 144 | 145 | # Setting start date and end date 146 | r = session.post(SUBMIT_LEAVE_URL, data=d) 147 | root = etree.HTML(r.text) 148 | 149 | d = {i.attrib['name']: i.attrib['value'] for i in root.xpath("//input[starts-with(@id, '__')]")} 150 | d["ctl00$ContentPlaceHolder1$CK001$DateUCCBegin$text1"] = start_date 151 | d["ctl00$ContentPlaceHolder1$CK001$DateUCCEnd$text1"] = end_date 152 | d["ctl00$ContentPlaceHolder1$CK001$ButtonCommit"] = u"下一步" 153 | 154 | # Setting leaving section 155 | r = session.post(SUBMIT_LEAVE_URL, data=d) 156 | root = etree.HTML(r.text) 157 | 158 | reason_map = {"21": u"事", "22": u"病", "23": u"公", "24": u"喪", "26": u"產"} 159 | 160 | # Setting reason id 161 | d = {i.attrib['name']: i.attrib['value'] for i in root.xpath("//input[starts-with(@id, '__')]")} 162 | d['ctl00$ContentPlaceHolder1$CK001$RadioButtonListOption'] = leave_dict[ 163 | "reason_id"] 164 | d['ctl00$ContentPlaceHolder1$CK001$TextBoxReason'] = "" 165 | r = session.post(SUBMIT_LEAVE_URL, data=d) 166 | 167 | # Get Teacher id 168 | teacher_id = root.xpath("//option[@selected='selected']")[0].values()[-1] 169 | 170 | # Setting leaving button 171 | button = root.xpath( 172 | "//input[starts-with(@id, 'ContentPlaceHolder1_CK001_GridViewMain_Button_')]") 173 | 174 | for i in leave_dict["section"]: 175 | root = etree.HTML(r.text) 176 | d = {i.attrib['name']: i.attrib['value'] for i in root.xpath("//input[starts-with(@id, '__')]")} 177 | d['ctl00$ContentPlaceHolder1$CK001$RadioButtonListOption'] = leave_dict[ 178 | "reason_id"] 179 | d['ctl00$ContentPlaceHolder1$CK001$TextBoxReason'] = leave_dict[ 180 | 'reason_text'] 181 | d['ctl00$ContentPlaceHolder1$CK001$DropDownListTeacher'] = root.xpath( 182 | "//option[@selected='selected']")[0].values()[-1] 183 | d[button[int(i)].attrib['name']] = '' 184 | d['__ASYNCPOST'] = "ture" 185 | r = session.post(SUBMIT_LEAVE_URL, data=d) 186 | 187 | # Send to last step 188 | root = etree.HTML(r.text) 189 | d = {i.attrib['name']: i.attrib['value'] for i in root.xpath("//input[starts-with(@id, '__')]")} 190 | d['ctl00$ContentPlaceHolder1$CK001$TextBoxReason'] = leave_dict[ 191 | 'reason_text'] 192 | d['ctl00$ContentPlaceHolder1$CK001$ButtonCommit2'] = "下一步" 193 | r = session.post(SUBMIT_LEAVE_URL, data=d) 194 | 195 | # Save leaving submit 196 | root = etree.HTML(r.text) 197 | d = {i.attrib['name']: i.attrib['value'] for i in root.xpath("//input[starts-with(@id, '__')]")} 198 | d['ctl00$ContentPlaceHolder1$CK001$ButtonSend'] = '存檔' 199 | files = {"ctl00$ContentPlaceHolder1$CK001$FileUpload1": 200 | (" ", "", "application/octet-stream")} 201 | 202 | # Send to server and save the submit 203 | r = session.post(SUBMIT_LEAVE_URL, files=files, data=d) 204 | root = etree.HTML(r.text) 205 | 206 | try: 207 | return_value = root.xpath("//script")[-1].text 208 | return_value = return_value[ 209 | return_value.index('"') + 1: return_value.rindex('"')] 210 | except: 211 | return_value = "Error..." 212 | 213 | return_success = True if return_value == u'假單存檔成功,請利用假單查詢進行後續作業。' else False 214 | 215 | return (return_success, return_value) 216 | 217 | 218 | if __name__ == '__main__': 219 | s = requests.session() 220 | login(s, "", "") 221 | #print(submitLeave(s, '103/09/25', '103/09/25', {"reason_id": "21", "reason_text": "testing", "section": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14"]})) 222 | # for test 223 | # import json 224 | # print(json.dumps(getList(s,year='107',semester='1'),ensure_ascii=False)) 225 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/news.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import random 4 | 5 | ENABLE = 1 6 | NEWS_ID = 31 7 | NEWS_DEBUG = False 8 | 9 | DEFAULT_WEIGHT = 10 10 | 11 | 12 | def random_by_weight(p): 13 | choice_id = [] 14 | for i in range(len(p)): 15 | choice_id += [i for _ in range(DEFAULT_WEIGHT + p[i]["news_weight"])] 16 | 17 | return p[random.choice(choice_id)] 18 | 19 | 20 | def random_news(): 21 | news_list = [ 22 | { 23 | "news_title": "第八屆泰北團-夢想,「泰」不一樣", 24 | "news_image": "http://i.imgur.com/iNbbd4B.jpg", 25 | "news_url": "https://docs.google.com/forms/d/11Awcel_MfPeiEkl7zQ0MldvnAw59gXKLecbIODPOaMs/viewform?edit_requested=true", 26 | "news_content": "", 27 | "news_weight": 3 28 | }, 29 | { 30 | "news_title": "體委幹部體驗營", 31 | "news_image": "http://i.imgur.com/aJyQlJp.jpg", 32 | "news_url": "https://www.facebook.com/Kuas%E9%AB%94%E5%A7%94-440439566106678/?fref=ts", 33 | "news_content": "", 34 | "news_weight": 4 35 | }, 36 | { 37 | "news_title": "遊戲外掛 原理實戰", 38 | "news_image": "http://i.imgur.com/WkI23R2.jpg", 39 | "news_url": "https://www.facebook.com/profile.php?id=735951703168873", 40 | "news_content": "", 41 | "news_weight": 6 42 | }, 43 | { 44 | "news_title": "好日子育樂營", 45 | "news_image": "https://scontent-hkg3-1.xx.fbcdn.net/hphotos-xft1/v/t34.0-0/p206x206/12834566_977348362345349_121675822_n.jpg?oh=e04f6830fdfe5d3a77e05a8b3c32fefc&oe=56E663E6", 46 | "news_url": "https://m.facebook.com/kuasYGR/", 47 | "news_content": "", 48 | "news_weight": 6 49 | } 50 | ] 51 | 52 | if NEWS_DEBUG: 53 | return news_list[0] 54 | else: 55 | return random_by_weight(news_list) 56 | 57 | 58 | def news_status(): 59 | return [ENABLE, NEWS_ID] 60 | 61 | 62 | def news(): 63 | """ 64 | News for kuas. 65 | 66 | return [enable, news_id, news_title, news_template, news_url] 67 | enable: bool 68 | news_id: int 69 | news_title: string 70 | news_tempalte: string 71 | news_url: string 72 | """ 73 | 74 | # Get news from random news 75 | news = random_news() 76 | 77 | news_title = news["news_title"] 78 | news_template = ( 79 | "
" 80 | "
" + news["news_content"] + "
" + 82 | "
" 83 | 84 | ) 85 | news_url = news["news_url"] 86 | 87 | return [ENABLE, NEWS_ID, news_title, news_template, news_url] 88 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/notification.py: -------------------------------------------------------------------------------- 1 | #-*- encoding=utf-8 2 | 3 | from lxml import etree 4 | import requests 5 | 6 | 7 | NOTIFICATION_URL = "http://www.kuas.edu.tw/files/501-1000-1003-%d.php" 8 | 9 | 10 | def get(page=1): 11 | r = requests.get(NOTIFICATION_URL % (page)) 12 | r.encoding = "utf-8" 13 | 14 | root = etree.HTML(r.text) 15 | trs = root.xpath("//tr[starts-with(@class, 'row')]") 16 | 17 | result = [] 18 | for tr in trs: 19 | a = tr.xpath("td//a")[0].values()[1] 20 | tr = list(filter(lambda x: x, map( 21 | lambda x: x.replace("\t", "").replace("\n", ""), tr.itertext()))) 22 | 23 | tr = dict(zip(("id", "title", "department", "date"), tr)) 24 | 25 | result.append({'link': a, 'info': tr}) 26 | 27 | return result 28 | 29 | if __name__ == "__main__": 30 | print(get(2)) 31 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/parse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from lxml import etree 4 | 5 | 6 | sections_time = [] 7 | weekdays_abbr = [] 8 | 9 | 10 | def parse(fncid, content): 11 | if fncid in parse_function: 12 | return parse_function[fncid](content) 13 | else: 14 | return content 15 | 16 | 17 | def course(cont): 18 | """Parse raw kuas ap course data 19 | Return: 20 | parse data: json 21 | have_saturday: bool 22 | have_sunday: bool 23 | except_text: string 24 | """ 25 | 26 | root = etree.HTML(cont) 27 | 28 | try: 29 | center = root.xpath("//center")[0] 30 | center_text = list(center.itertext())[0] 31 | except: 32 | center = "" 33 | center_text = "" 34 | 35 | # Return if no course data 36 | if center_text.startswith(u'學生目前無選課資料!'): 37 | return {} 38 | 39 | tbody = root.xpath("//table")[-1] 40 | 41 | course_table = [] 42 | for sections, r in enumerate(tbody[1:]): 43 | section = "" 44 | start_time = "" 45 | end_time = "" 46 | 47 | for weekends, c in enumerate(r.xpath("td")): 48 | classes = {"title": "", "date": {}, 49 | "location": {}, "instructors": []} 50 | 51 | r = list( 52 | filter( 53 | lambda x: x, 54 | map(lambda x: x.replace("\xa0", ""), c.itertext()) 55 | ) 56 | ) 57 | 58 | if not weekends: 59 | section = r[0] 60 | start_time = "" 61 | end_time = "" 62 | 63 | if len(r) > 1: 64 | start_time, end_time = r[1].split("-") 65 | start_time = "%s:%s" % (start_time[: 2], start_time[2:]) 66 | end_time = "%s:%s" % (end_time[: 2], end_time[2:]) 67 | 68 | continue 69 | 70 | if not r: 71 | continue 72 | 73 | classes["title"] = r[0] 74 | classes["date"]["start_time"] = start_time 75 | classes["date"]["end_time"] = end_time 76 | classes["date"]["weekday"] = " MTWRFSH"[weekends] 77 | classes["date"]["section"] = section 78 | 79 | if len(r) > 1: 80 | classes["instructors"].append(r[1]) 81 | 82 | classes["location"]["building"] = "" 83 | classes["location"]["room"] = r[2] if len(r) > 2 else "" 84 | 85 | course_table.append(classes) 86 | 87 | timecode = [] 88 | for r in tbody[1:]: 89 | timecode.append(list(r.itertext())[1]) 90 | course_table.append({'timecode': timecode}) 91 | 92 | return course_table 93 | 94 | 95 | def score(cont): 96 | root = etree.HTML(cont) 97 | 98 | try: 99 | tbody = root.xpath("//table")[-1] 100 | 101 | center = root.xpath("//center") 102 | center_text = list(center[-1].itertext())[0] 103 | except: 104 | tbody = "" 105 | center = "" 106 | center_text = "" 107 | 108 | if center_text.startswith(u'目前無學生個人成績資料'): 109 | return {} 110 | 111 | score_table = [] 112 | for r_index, r in enumerate(tbody[1:-1]): 113 | r = list(map(lambda x: x.replace(u"\xa0", ""), r.itertext())) 114 | 115 | row = {} 116 | 117 | row["title"] = r[1] 118 | row["units"] = r[2] 119 | row["hours"] = r[3] 120 | row["required"] = r[4] 121 | row["at"] = r[5] 122 | row["middle_score"] = r[6] 123 | row["final_score"] = r[7] 124 | row["remark"] = r[8] 125 | 126 | score_table.append(row) 127 | 128 | total_score = root.xpath("//div")[-1].text.replace(u"    ", " ").split(" ") 129 | detail = { 130 | "conduct": float(total_score[0].split(":")[-1]) if not total_score[0].startswith("操行成績:0") else 0.0, 131 | "average": float(total_score[1].split(":")[-1]) if total_score[1] != "總平均:" else 0.0, 132 | "class_rank": total_score[2].split(":")[-1] if not total_score[2].startswith("班名次/班人數:/") else "", 133 | "class_percentage": float(total_score[3].split(":")[-1][:-1]) if not total_score[3].startswith("班名次百分比:%") else 0.0 134 | } 135 | 136 | return {"scores": score_table, "detail": detail} 137 | 138 | 139 | parse_function = {"ag222": course, "ag008": score} 140 | 141 | 142 | if __name__ == "__main__": 143 | # print(course(open("c.html").read())) 144 | pass 145 | -------------------------------------------------------------------------------- /src/kuas_api/kuas/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import g 4 | import kuas_api.kuas.ap as ap 5 | import kuas_api.kuas.cache as cache 6 | from lxml import etree 7 | 8 | AP_QUERY_USER_EXPIRE = 300 9 | 10 | 11 | def _get_user_info(session): 12 | """Get user info 13 | 14 | return: `lxml.etree._Element` 15 | """ 16 | 17 | content = cache.ap_query( 18 | session, "ag003", {}, g.username, expire=AP_QUERY_USER_EXPIRE) 19 | 20 | root = etree.HTML(content) 21 | 22 | return root 23 | 24 | 25 | def get_user_info(session): 26 | root = _get_user_info(session) 27 | td = root.xpath("//td") 28 | 29 | result = { 30 | "education_system": "", 31 | "department": "", 32 | "class": "", 33 | "student_id": g.username, 34 | "student_name_cht": "", 35 | "student_name_eng": "", 36 | "status": 200, 37 | "message": "" 38 | } 39 | if len(td) > 3 : 40 | result["education_system"] = td[3].text[5:] 41 | result["department"] = td[4].text[5:] 42 | result["class"] = td[8].text[5:] 43 | result["student_id"] = td[9].text[5:] 44 | result["student_name_cht"] = td[10].text[5:] 45 | result["student_name_eng"] = td[11].text[5:] 46 | else : 47 | result["status"] = 204 48 | result["message"] = td[0].text 49 | 50 | return result 51 | 52 | 53 | def get_user_picture(session): 54 | root = _get_user_info(session) 55 | 56 | try: 57 | image = ap.AP_BASE_URL + "/nkust" + \ 58 | root.xpath("//img")[0].values()[0][2:] 59 | except: 60 | image = "" 61 | 62 | return image 63 | -------------------------------------------------------------------------------- /src/kuas_api/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/kuas_api/modules/__init__.py -------------------------------------------------------------------------------- /src/kuas_api/modules/const.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | device_version = { 3 | "android": "2.2.7", 4 | "android_donate": "2.1.8", 5 | "ios": "1.6.0" 6 | } 7 | 8 | 9 | # Token duration in seconds 10 | token_duration = 3600 11 | serect_key = "usapoijupojfa;dsj;lv;ldakjads;lfkjapoiuewqprjf" 12 | 13 | # HTTP Status Code 14 | ok = 200 15 | no_content = 204 16 | -------------------------------------------------------------------------------- /src/kuas_api/modules/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | 5 | def error_handle(status, 6 | developer_message, user_message, 7 | error_code=-1, more_info=""): 8 | """Return error handler json 9 | :param status: HTTP status code 10 | :type status: int 11 | :param developer_message: message for developer 12 | :type developer_message: str 13 | :param user_message: message for user 14 | :type user_message: str 15 | :param error_code: internal error code 16 | :type error_code: int 17 | :param more_info: links for more information 18 | :type more_info: str 19 | 20 | :return: json error handle 21 | :rtype: json 22 | """ 23 | 24 | error_handle = { 25 | "status": status, 26 | "developer_message": developer_message, 27 | "user_message": user_message, 28 | "error_code": error_code, 29 | "more_info": more_info 30 | } 31 | 32 | return json.dumps(error_handle) 33 | -------------------------------------------------------------------------------- /src/kuas_api/modules/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import current_app 3 | 4 | 5 | def jsonify(*args, **kwargs): 6 | """Creates a :class:`~flask.Response` with the JSON representation of 7 | the given arguments with an :mimetype:`application/json` mimetype. The 8 | arguments to this function are the same as to the :class:`dict` 9 | constructor. 10 | 11 | Example usage:: 12 | 13 | from flask import jsonify 14 | 15 | @app.route('/_get_current_user') 16 | def get_current_user(): 17 | return jsonify(username=g.user.username, 18 | email=g.user.email, 19 | id=g.user.id) 20 | 21 | This will send a JSON response like this to the browser:: 22 | 23 | { 24 | "username": "admin", 25 | "email": "admin@localhost", 26 | "id": 42 27 | } 28 | 29 | For security reasons only objects are supported toplevel. For more 30 | information about this, have a look at :ref:`json-security`. 31 | 32 | This function's response will be pretty printed if it was not requested 33 | with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless 34 | the ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to false. 35 | Compressed (not pretty) formatting currently means no indents and no 36 | spaces after separators. 37 | 38 | .. versionadded:: 0.2 39 | """ 40 | 41 | indent = 2 42 | separators = (',', ':') 43 | 44 | # Note that we add '\n' to end of response 45 | # (see https://github.com/mitsuhiko/flask/pull/1262) 46 | rv = current_app.response_class( 47 | (json.dumps( 48 | dict(*args, **kwargs), 49 | indent=indent, 50 | separators=separators, 51 | ensure_ascii=False 52 | ), '\n'), 53 | mimetype='application/json') 54 | return rv 55 | -------------------------------------------------------------------------------- /src/kuas_api/modules/stateless_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | import redis 6 | import requests 7 | from flask import g, abort 8 | from flask_httpauth import HTTPBasicAuth 9 | from itsdangerous import (TimedJSONWebSignatureSerializer 10 | as Serializer, BadSignature, SignatureExpired) 11 | 12 | 13 | import kuas_api.kuas.cache as cache 14 | import kuas_api.modules.const as const 15 | import kuas_api.modules.error as error 16 | 17 | # Create HTTP auth 18 | auth = HTTPBasicAuth() 19 | 20 | # Redis connection 21 | red = redis.StrictRedis.from_url(url=os.environ['REDIS_URL'], db=2) 22 | 23 | # Shit lazy key 24 | DIRTY_SECRET_KEY = red.get("SECRET_KEY") if red.exists( 25 | "SECRET_KEY") else str(os.urandom(32)) 26 | 27 | 28 | def check_cookies(username): 29 | """Check username is exist in redis 30 | :param username: school id 31 | :type username: str 32 | :return: Exist then return True, else return False 33 | :rtype: bool 34 | """ 35 | return red.exists(username) 36 | 37 | 38 | def set_cookies(s, username): 39 | """Reset cookies to requests.Session 40 | :param s: Requests session 41 | :type s: requests.sessions.Session 42 | :param username: school id 43 | :type username: str 44 | :return: None 45 | """ 46 | cookies = json.loads(str(red.get(username), "utf-8"))['cookies'] 47 | 48 | for c in cookies: 49 | s.cookies.set(c['name'], c['value'], domain=c['domain']) 50 | 51 | 52 | def get_requests_session_with_cookies(): 53 | s = requests.Session() 54 | s.verify = False 55 | 56 | if g.username: 57 | set_cookies(s, g.username) 58 | 59 | return s 60 | 61 | 62 | def generate_auth_token(username, cookies, expiration=600): 63 | """Generate auth token and save cookies to redis by username 64 | :param username: usrename (school id) 65 | :type username: str 66 | :param cookies: cookies list from :class:`requests.Session.cookies` 67 | :type cookies: :class:`requests.cookies.RequestsCookieJar` 68 | :return: auth token 69 | :rtype: str 70 | """ 71 | s = Serializer(DIRTY_SECRET_KEY, expires_in=expiration) 72 | 73 | red.set(username, json.dumps(cookies), ex=600) 74 | 75 | return s.dumps({"sid": username}) 76 | 77 | 78 | def verify_auth_token(token): 79 | """Verify auth token 80 | :param token: auth token from user 81 | :type token: str 82 | :return: None or username 83 | :rtype: str or None 84 | """ 85 | s = Serializer(DIRTY_SECRET_KEY) 86 | try: 87 | data = s.loads(token) 88 | except SignatureExpired: 89 | abort(401) # valid token, but expired 90 | except BadSignature: 91 | return None # invalid token 92 | 93 | if not check_cookies(data['sid']): 94 | return None # Cookies not exist in redis 95 | 96 | user = data['sid'] 97 | return user 98 | 99 | 100 | @auth.verify_password 101 | def verify_password(username_or_token, password): 102 | """For verify username or token is valid. 103 | :param username_or_token: username or token 104 | :type username_or_token: str 105 | :param password: password for username, if using token, 106 | password can be ignore 107 | :type password: str 108 | :return: is the username and password, or token is valid 109 | :rtype: bool 110 | """ 111 | # Check auth token 112 | username = verify_auth_token(username_or_token) 113 | 114 | # Set username and token to global 115 | if username: 116 | g.username = username 117 | g.token = username_or_token 118 | else: 119 | # If auth token is bad (valid token but expired, or invalid token) 120 | # Then Try to login to school service 121 | cookies = cache.login(username_or_token, password) 122 | 123 | # If cookies is False, mean login error 124 | # return False for unverify password 125 | if not cookies: 126 | return False 127 | 128 | # If return cookies list, 129 | # generate auth token and save cookies to redis 130 | # and set to g.token pass to /api_version/token 131 | # for return token. 132 | # 133 | # Data set in redis server: 134 | # key: username, value: cookies 135 | g.token = generate_auth_token( 136 | username_or_token, cookies, expiration=const.token_duration) 137 | g.username = username_or_token 138 | 139 | return True 140 | 141 | 142 | @auth.error_handler 143 | def auth_error(): 144 | """Return Authroized Error to users. 145 | 146 | :return: error json 147 | :rtype: json 148 | """ 149 | 150 | user_message = ("Your token has been expired or " 151 | "using wrong username and password to login") 152 | 153 | return error.error_handle( 154 | status=401, 155 | developer_message="Token expired or Unauthorized Access", 156 | user_message=user_message 157 | ) 158 | -------------------------------------------------------------------------------- /src/kuas_api/news_db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/kuas_api/news_db.sqlite -------------------------------------------------------------------------------- /src/kuas_api/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /src/kuas_api/templates/query.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /src/kuas_api/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/kuas_api/views/__init__.py -------------------------------------------------------------------------------- /src/kuas_api/views/latest/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask_apiblueprint import APIBlueprint 4 | from kuas_api.views.v2 import api_v2 5 | 6 | # Create latest blueprint 7 | latest = APIBlueprint( 8 | 'latest', __name__, 9 | subdomain='', 10 | url_prefix='/latest', 11 | inherit_from=api_v2) 12 | -------------------------------------------------------------------------------- /src/kuas_api/views/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | from functools import wraps 6 | 7 | import requests 8 | import kuas_api.kuas.ap as ap 9 | import kuas_api.kuas.user as user 10 | import kuas_api.kuas.cache as cache 11 | 12 | from flask import Flask, render_template, request, session, redirect 13 | from flask_cors import * 14 | 15 | from flask_apiblueprint import APIBlueprint 16 | 17 | 18 | __version__ = "2.0" 19 | 20 | android_version = "1.5.4" 21 | android_donate_version = "2.0.0" 22 | ios_version = "1.4.3" 23 | 24 | 25 | api_v1 = APIBlueprint( 26 | 'api_v1', __name__, 27 | subdomain='', 28 | url_prefix='') 29 | 30 | 31 | def authenticate(func): 32 | @wraps(func) 33 | def call(*args, **kwargs): 34 | if 'c' in session: 35 | return func(*args, **kwargs) 36 | else: 37 | return "false" 38 | 39 | return call 40 | 41 | 42 | def dump_cookies(cookies_list): 43 | """Dumps cookies to list 44 | """ 45 | 46 | cookies = [] 47 | for c in cookies_list: 48 | cookies.append({ 49 | 'name': c.name, 50 | 'domain': c.domain, 51 | 'value': c.value}) 52 | 53 | return cookies 54 | 55 | 56 | def set_cookies(s, cookies): 57 | for c in cookies: 58 | s.cookies.set(c['name'], c['value'], domain=c['domain']) 59 | 60 | 61 | @api_v1.route('/') 62 | def index(): 63 | return "kuas-api version 1." 64 | 65 | 66 | @api_v1.route('/version') 67 | @cross_origin(supports_credentials=True) 68 | def version(): 69 | return android_version 70 | 71 | 72 | @api_v1.route('/android_version') 73 | @cross_origin(supports_credentials=True) 74 | def a_version(): 75 | return android_version 76 | 77 | 78 | @api_v1.route('/android_donate_version') 79 | @cross_origin(supports_credentials=True) 80 | def a_donate_version(): 81 | return android_donate_version 82 | 83 | 84 | @api_v1.route('/ios_version') 85 | @cross_origin(supports_credentials=True) 86 | def i_version(): 87 | return ios_version 88 | 89 | 90 | @api_v1.route('/fixed') 91 | @cross_origin(supports_credentials=True) 92 | def is_fixed(): 93 | return "" 94 | 95 | 96 | @api_v1.route('/backup') 97 | @cross_origin(supports_credentials=True) 98 | def backup(): 99 | return "0" 100 | 101 | 102 | @api_v1.route('/status') 103 | @cross_origin(supports_credentials=True) 104 | def status(): 105 | return json.dumps(cache.server_status()) 106 | 107 | 108 | @api_v1.route('/ap/semester') 109 | @cross_origin(supports_credentials=True) 110 | def ap_semester(): 111 | semester_list = ap.get_semester_list() 112 | default_yms = list(filter(lambda x: x['selected'] == 1, semester_list))[0] 113 | return json.dumps({"semester": semester_list, 114 | "default_yms": default_yms}, ensure_ascii=False) 115 | 116 | 117 | @api_v1.route('/ap/login', methods=['POST']) 118 | @cross_origin(supports_credentials=True) 119 | def login_post(): 120 | if request.method == "POST": 121 | session.permanent = True 122 | 123 | # Start login 124 | username = request.form['username'] 125 | password = request.form['password'] 126 | 127 | s = requests.session() 128 | is_login = cache.login(s, username, password) 129 | 130 | if is_login: 131 | # Serialize cookies with domain 132 | session['c'] = dump_cookies(s.cookies) 133 | session['username'] = username 134 | 135 | return "true" 136 | else: 137 | return "false" 138 | 139 | return render_template("login.html") 140 | 141 | 142 | @api_v1.route('/ap/only/login', methods=['POST']) 143 | @cross_origin(supports_credentials=True) 144 | def ap_login(): 145 | if request.method == "POST": 146 | session.permanent = True 147 | 148 | # Start login 149 | username = request.form['username'] 150 | password = request.form['password'] 151 | 152 | s = requests.session() 153 | is_login = cache.ap.login(s, username, password) 154 | 155 | if is_login: 156 | # Serialize cookies with domain 157 | session['c'] = dump_cookies(s.cookies) 158 | session['username'] = username 159 | 160 | return "true" 161 | else: 162 | return "false" 163 | 164 | return render_template("login.html") 165 | 166 | 167 | @api_v1.route('/ap/is_login', methods=['POST']) 168 | @cross_origin(supports_credentials=True) 169 | def is_login(): 170 | if 'c' not in session: 171 | return "false" 172 | 173 | return "true" 174 | 175 | 176 | @api_v1.route('/ap/logout', methods=['POST']) 177 | @cross_origin(supports_credentials=True) 178 | def logout(): 179 | session.clear() 180 | 181 | return 'logout' 182 | 183 | 184 | @api_v1.route('/ap/query', methods=['GET', 'POST']) 185 | @cross_origin(supports_credentials=True) 186 | @authenticate 187 | def query_post(): 188 | if request.method == "POST": 189 | fncid = request.form['fncid'] 190 | arg01 = request.form['arg01'] if 'arg01' in request.form else None 191 | arg02 = request.form['arg02'] if 'arg02' in request.form else None 192 | arg03 = request.form['arg03'] if 'arg03' in request.form else None 193 | arg04 = request.form['arg04'] if 'arg04' in request.form else None 194 | 195 | # if 'c' not in session: 196 | # return "false" 197 | # Restore cookies 198 | s = requests.session() 199 | set_cookies(s, session['c']) 200 | 201 | query_content = cache.ap_query( 202 | s, fncid, {"arg01": arg01, "arg02": arg02, "arg03": arg03, "arg04": arg04}, session['username']) 203 | 204 | if fncid == "ag222": 205 | return json.dumps(query_content) 206 | elif fncid == "ag008": 207 | return json.dumps(query_content) 208 | else: 209 | return json.dumps(query_content) 210 | 211 | return render_template("query.html") 212 | 213 | 214 | @api_v1.route('/ap/user/info') 215 | @cross_origin(supports_credentials=True) 216 | @authenticate 217 | def ap_user_info(): 218 | # Restore cookies 219 | s = requests.session() 220 | set_cookies(s, session['c']) 221 | 222 | return json.dumps(user.get_user_info(s, session['username'])) 223 | 224 | 225 | @api_v1.route('/ap/user/picture') 226 | @cross_origin(supports_credentials=True) 227 | @authenticate 228 | def ap_user_picture(): 229 | # Restore cookies 230 | s = requests.session() 231 | set_cookies(s, session['c']) 232 | 233 | return user.get_user_picture(s, session['username']) 234 | 235 | 236 | @api_v1.route('/leave', methods=["POST"]) 237 | @cross_origin(supports_credentials=True) 238 | @authenticate 239 | def leave_post(): 240 | if request.method == "POST": 241 | print(request.form) 242 | arg01 = request.form['arg01'] if 'arg01' in request.form else None 243 | arg02 = request.form['arg02'] if 'arg02' in request.form else None 244 | 245 | # Restore cookies 246 | s = requests.session() 247 | set_cookies(s, session['c']) 248 | 249 | if arg01 and arg02: 250 | return json.dumps(cache.leave_query(s, arg01, arg02)) 251 | else: 252 | return json.dumps(cache.leave_query(s)) 253 | 254 | 255 | @api_v1.route('/leave/submit', methods=['POST']) 256 | @cross_origin(supports_credentials=True) 257 | @authenticate 258 | def leave_submit(): 259 | if request.method == 'POST': 260 | start_date = request.form['start_date'].replace("-", "/") 261 | end_date = request.form['end_date'].replace("-", "/") 262 | reason_id = request.form[ 263 | 'reason_id'] if 'reason_id' in request.form else None 264 | reason_text = request.form[ 265 | 'reason_text'] if 'reason_text' in request.form else None 266 | section = json.loads( 267 | request.form['section']) if 'section' in request.form else None 268 | 269 | s = requests.session() 270 | set_cookies(s, session['c']) 271 | 272 | start_date = start_date.split("/") 273 | start_date[0] = str(int(start_date[0]) - 1911) 274 | start_date = "/".join(start_date) 275 | 276 | end_date = end_date.split("/") 277 | end_date[0] = str(int(end_date[0]) - 1911) 278 | end_date = "/".join(end_date) 279 | 280 | # Fixing, don't send it 281 | return json.dumps((False, "請假維修中, 目前無法請假~")) 282 | 283 | # Fixed 284 | # if reason_id and reason_text and section: 285 | # return json.dumps(cache.leave_submit(s, start_date, end_date, reason_id, reason_text, section)) 286 | # else: 287 | # return json.dumps((False, "Error...")) 288 | 289 | 290 | @api_v1.route('/bus/query', methods=["POST"]) 291 | @cross_origin(supports_credentials=True) 292 | @authenticate 293 | def bus_query(): 294 | if request.method == "POST": 295 | date = request.form['date'] 296 | 297 | # Restore cookies 298 | s = requests.session() 299 | set_cookies(s, session['c']) 300 | 301 | return json.dumps(cache.bus_query(s, date)) 302 | 303 | 304 | @api_v1.route("/bus/reserve") 305 | @cross_origin(supports_credentials=True) 306 | @authenticate 307 | def bus_reserve(): 308 | if 'c' in session: 309 | s = requests.session() 310 | set_cookies(s, session['c']) 311 | 312 | return json.dumps(cache.bus_reserve_query(s)) 313 | 314 | 315 | @api_v1.route('/bus/booking', methods=["POST"]) 316 | @cross_origin(supports_credentials=True) 317 | @authenticate 318 | def bus_booking(): 319 | if request.method == "POST": 320 | busId = request.form['busId'] 321 | action = request.form['action'] 322 | 323 | # Restore cookies 324 | s = requests.session() 325 | set_cookies(s, session['c']) 326 | 327 | return json.dumps(cache.bus_booking(s, busId, action)) 328 | 329 | 330 | @api_v1.route('/notification/') 331 | @cross_origin(supports_credentials=True) 332 | def notification(page): 333 | page = int(page) 334 | return json.dumps(cache.notification_query(page)) 335 | 336 | 337 | @api_v1.route('/news') 338 | @cross_origin(supports_credentials=True) 339 | def news(): 340 | return redirect("/v2/news") 341 | 342 | 343 | @api_v1.route('/news/status') 344 | @cross_origin(supports_credentials=True) 345 | def news_status(): 346 | return json.dumps(cache.news_status()) 347 | 348 | 349 | if __name__ == '__main__': 350 | app.run(host="0.0.0.0", port=5001) 351 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/__init__.py: -------------------------------------------------------------------------------- 1 | import kuas_api.modules.error as error 2 | from flask_apiblueprint import APIBlueprint 3 | from kuas_api.modules.json import jsonify 4 | 5 | 6 | # Create v2 blueprint 7 | api_v2 = APIBlueprint( 8 | 'api_v2', __name__, 9 | subdomain='', 10 | url_prefix='/v2') 11 | 12 | 13 | def get_git_revision_short_hash(): 14 | import subprocess 15 | return subprocess.check_output( 16 | ['git', 'rev-parse', '--short', 'HEAD']).decode("utf-8").strip("\n") 17 | 18 | 19 | @api_v2.route('/') 20 | def version_2(): 21 | """Return API version 22 | """ 23 | return jsonify( 24 | name="kuas-api version 2.", 25 | version="2", 26 | server_revision=get_git_revision_short_hash(), 27 | endpoints="https://kuas.grd.idv.tw:14769/v2/" 28 | ) 29 | 30 | 31 | @api_v2.errorhandler(401) 32 | def unauthorized_error(err): 33 | return error.error_handle(status=401, 34 | developer_message="token expired", 35 | user_message="token expired", 36 | error_code=401 37 | ) 38 | 39 | 40 | # Add v2 routes 41 | from kuas_api.views.v2.utils import routes as utils_routes 42 | from kuas_api.views.v2.ap import routes as ap_routes 43 | from kuas_api.views.v2.bus import routes as bus_routes 44 | from kuas_api.views.v2.leave import routes as leave_routes 45 | from kuas_api.views.v2.notifications import routes as notifications_routes 46 | from kuas_api.views.v2.news import routes as news_routes 47 | 48 | # Dirty from #593 49 | routes = ( 50 | utils_routes + 51 | ap_routes + 52 | bus_routes + 53 | leave_routes + 54 | notifications_routes + 55 | news_routes 56 | ) 57 | 58 | for r in routes: 59 | endpoint = r["options"].pop("endpoint", None) 60 | api_v2.add_url_rule( 61 | r["rule"], 62 | endpoint, 63 | r["view_func"], 64 | **r["options"] 65 | ) 66 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/ap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | from flask import request, g 5 | from flask_cors import * 6 | from flask import abort 7 | import kuas_api.kuas.ap as ap 8 | import kuas_api.kuas.user as user 9 | import kuas_api.kuas.cache as cache 10 | 11 | from kuas_api.modules.json import jsonify 12 | from kuas_api.modules.stateless_auth import auth 13 | import kuas_api.modules.stateless_auth as stateless_auth 14 | import kuas_api.modules.const as const 15 | import kuas_api.modules.error as error 16 | from .doc import auto 17 | 18 | # Nestable blueprints problem 19 | # not sure isn't this a best practice now. 20 | # https://github.com/mitsuhiko/flask/issues/593 21 | routes = [] 22 | 23 | 24 | def route(rule, **options): 25 | def decorator(f): 26 | url_rule = { 27 | "rule": rule, 28 | "view_func": f, 29 | "options": options if options else {} 30 | } 31 | 32 | routes.append(url_rule) 33 | return f 34 | 35 | return decorator 36 | 37 | 38 | @route('/ap/users/info') 39 | @auth.login_required 40 | def ap_user_info(): 41 | """Get user's information. 42 | 43 | :reqheader Authorization: Using Basic Auth 44 | :resjson string class: User's class name 45 | :resjson string education_system: User's scheme 46 | :resjson string department: User's department 47 | :resjson string student_id: User's student identifier 48 | :resjson string student_name_cht: User's name in Chinese 49 | :resjson string student_name_eng: User's name in English 50 | :statuscode 200: Query successful 51 | :statuscode 401: Login failed or auth_token has been expired 52 | 53 | **Request** 54 | 55 | .. sourcecode:: http 56 | 57 | GET /latest/ap/users/info HTTP/1.1 58 | Host: kuas.grd.idv.tw:14769 59 | Authorization: Basic xxxxxxxxxxxxx= 60 | 61 | .. sourcecode:: shell 62 | 63 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/latest/ap/users/info 64 | 65 | 66 | **Response** 67 | 68 | .. sourcecode:: http 69 | 70 | HTTP/1.1 200 OK 71 | Content-Type: text/html; charset=utf-8 72 | 73 | { 74 | "class":"四資工三甲", 75 | "education_system":"日間部四技", 76 | "department":"資訊工程系", 77 | "student_id":"1104137***", 78 | "student_name_cht":"顏**", 79 | "student_name_eng":"" 80 | } 81 | """ 82 | # Restore cookies 83 | s = stateless_auth.get_requests_session_with_cookies() 84 | 85 | return json.dumps(user.get_user_info(s), ensure_ascii=False) 86 | 87 | 88 | @route('/ap/users/picture') 89 | @auth.login_required 90 | def ap_user_picture(): 91 | """Get user's picture URL. 92 | 93 | :reqheader Authorization: Using Basic Auth 94 | :statuscode 200: Query successful 95 | :statuscode 401: Login failed or auth_token has been expired 96 | 97 | **Request** 98 | 99 | .. sourcecode:: http 100 | 101 | GET /latest/ap/users/info HTTP/1.1 102 | Host: kuas.grd.idv.tw:14769 103 | Authorization: Basic xxxxxxxxxxxxx= 104 | 105 | .. sourcecode:: shell 106 | 107 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/latest/ap/users/picture 108 | 109 | 110 | **Response** 111 | 112 | .. sourcecode:: http 113 | 114 | HTTP/1.1 200 OK 115 | Content-Type: text/html; charset=utf-8 116 | 117 | http://140.127.113.231/kuas/stdpics/1104137***_20170803213***.jpg 118 | """ 119 | # Restore cookies 120 | s = stateless_auth.get_requests_session_with_cookies() 121 | 122 | return user.get_user_picture(s) 123 | 124 | 125 | @route('/ap/users/coursetables//') 126 | @auth.login_required 127 | def get_coursetables(year, semester): 128 | """Get user's class schedule. 129 | 130 | :reqheader Authorization: Using Basic Auth 131 | :query int year: Specific year to query class schedule. format: yyy (see below) 132 | :query int semester: Given a semester 133 | :statuscode 200: Query successful 134 | :statuscode 401: Login failed or auth_token has been expired 135 | 136 | **Request** 137 | 138 | .. sourcecode:: http 139 | 140 | GET /latest/ap/users/info HTTP/1.1 141 | Host: kuas.grd.idv.tw:14769 142 | Authorization: Basic xxxxxxxxxxxxx= 143 | 144 | .. sourcecode:: shell 145 | 146 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/\\ 147 | latest/ap/users/coursetables/106/1 148 | 149 | 150 | **Response** 151 | 152 | .. sourcecode:: http 153 | 154 | HTTP/1.1 200 OK 155 | Content-Type: application/json 156 | 157 | { 158 | "status":200, 159 | "coursetables":{ 160 | "Tuesday":[ 161 | { 162 | "instructors":[ 163 | "張雲龍" 164 | ], 165 | "date":{ 166 | "weekday":"T", 167 | "start_time":"09:10", 168 | "end_time":"10:00", 169 | "section":"第 2 節" 170 | }, 171 | "title":"生物資訊概論", 172 | "location":{ 173 | "building":"", 174 | "room":"資002" 175 | } 176 | } 177 | ], 178 | "Wednesday":[ 179 | { 180 | "instructors":[ 181 | "洪靖婷" 182 | ], 183 | "date":{ 184 | "weekday":"W", 185 | "start_time":"08:10", 186 | "end_time":"09:00", 187 | "section":"第 1 節" 188 | }, 189 | "title":"應用文與習作", 190 | "location":{ 191 | "building":"", 192 | "room":"育302" 193 | } 194 | } 195 | ], 196 | "Monday":[ 197 | { 198 | "instructors":[ 199 | "張道行" 200 | ], 201 | "date":{ 202 | "weekday":"M", 203 | "start_time":"13:30", 204 | "end_time":"14:20", 205 | "section":"第 5 節" 206 | }, 207 | "title":"計算機結構", 208 | "location":{ 209 | "building":"", 210 | "room":"南101" 211 | } 212 | } 213 | ], 214 | "Thursday":[ 215 | { 216 | "instructors":[ 217 | "蕭淳元" 218 | ], 219 | "date":{ 220 | "weekday":"R", 221 | "start_time":"09:10", 222 | "end_time":"10:00", 223 | "section":"第 2 節" 224 | }, 225 | "title":"資料結構", 226 | "location":{ 227 | "building":"", 228 | "room":"育302" 229 | } 230 | } 231 | ] 232 | }, 233 | "messages":"" 234 | } 235 | """ 236 | # See Gist for more infomation. 237 | # https://gist.github.com/hearsilent/a2570371cc6aa7db97bb 238 | 239 | weekdays = {"M": "Monday", "T": "Tuesday", "W": "Wednesday", 240 | "R": "Thursday", "F": "Friday", "S": "Saturday", 241 | "H": "Sunday" 242 | } 243 | 244 | # Restore cookies 245 | s = stateless_auth.get_requests_session_with_cookies() 246 | 247 | classes = cache.ap_query( 248 | s, "ag222", {"arg01": year, "arg02": semester}, g.username) 249 | 250 | # No Content 251 | if not classes: 252 | return jsonify(status=const.no_content, messages="學生目前無選課資料", coursetables=classes) 253 | 254 | coursetables = {} 255 | for c in classes: 256 | if 'timecode' in c: 257 | coursetables['timecode'] = c['timecode'] 258 | continue 259 | weekday = weekdays[c["date"]["weekday"]] 260 | if not weekday in coursetables: 261 | coursetables[weekday] = [] 262 | 263 | coursetables[weekday].append(c) 264 | 265 | return jsonify(status=const.ok, messages="", coursetables=coursetables) 266 | 267 | 268 | @route('/ap/users/scores//') 269 | @auth.login_required 270 | def get_score(year, semester): 271 | """Get user's scores. 272 | 273 | :reqheader Authorization: Using Basic Auth 274 | :query int year: Specific year to query class schedule. format: yyy (see below) 275 | :query int semester: Set semester to query class schedule. value: 1~4 (see below) 276 | :statuscode 200: Query successful 277 | :statuscode 401: Login failed or auth_token has been expired 278 | 279 | **Request** 280 | 281 | .. sourcecode:: http 282 | 283 | GET /latest/ap/users/info HTTP/1.1 284 | Host: kuas.grd.idv.tw:14769 285 | Authorization: Basic xxxxxxxxxxxxx= 286 | 287 | .. sourcecode:: shell 288 | 289 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/\\ 290 | latest/ap/users/scores/105/2 291 | 292 | 293 | **Response** 294 | 295 | .. sourcecode:: http 296 | 297 | HTTP/1.1 200 OK 298 | Content-Type: application/json 299 | 300 | 301 | { 302 | "status":200, 303 | "messages":"", 304 | "scores":{ 305 | "detail":{ 306 | "conduct":82.0, 307 | "class_rank":"44/56", 308 | "average":70.33, 309 | "class_percentage":78.57 310 | }, 311 | "scores":[ 312 | { 313 | "required":"【選修】", 314 | "hours":"3.0", 315 | "title":"系統程式", 316 | "remark":"", 317 | "middle_score":"*", 318 | "units":"3.0", 319 | "final_score":"60.00", 320 | "at":"【學期】" 321 | }, 322 | { 323 | "required":"【必修】", 324 | "hours":"3.0", 325 | "title":"物理(二)", 326 | "remark":"停修", 327 | "middle_score":"*", 328 | "units":"3.0", 329 | "final_score":"0.00", 330 | "at":"【學期】" 331 | }, 332 | { 333 | "required":"【必修】", 334 | "hours":"2.0", 335 | "title":"英語聽講訓練(二)", 336 | "remark":"", 337 | "middle_score":"85.00", 338 | "units":"1.0", 339 | "final_score":"75.00", 340 | "at":"【學期】" 341 | }, 342 | { 343 | "required":"【必修】", 344 | "hours":"3.0", 345 | "title":"計算機網路", 346 | "remark":"", 347 | "middle_score":"*", 348 | "units":"3.0", 349 | "final_score":"63.00", 350 | "at":"【學期】" 351 | }, 352 | { 353 | "required":"【選修】", 354 | "hours":"3.0", 355 | "title":"視窗程式設計", 356 | "remark":"", 357 | "middle_score":"76.00", 358 | "units":"3.0", 359 | "final_score":"76.00", 360 | "at":"【學期】" 361 | }, 362 | { 363 | "required":"【必修】", 364 | "hours":"3.0", 365 | "title":"微處理機", 366 | "remark":"", 367 | "middle_score":"*", 368 | "units":"3.0", 369 | "final_score":"79.00", 370 | "at":"【學期】" 371 | }, 372 | { 373 | "required":"【必修】", 374 | "hours":"3.0", 375 | "title":"線性代數", 376 | "remark":"", 377 | "middle_score":"*", 378 | "units":"3.0", 379 | "final_score":"59.00", 380 | "at":"【學期】" 381 | }, 382 | { 383 | "required":"【必修】", 384 | "hours":"3.0", 385 | "title":"機率與統計", 386 | "remark":"", 387 | "middle_score":"80.00", 388 | "units":"3.0", 389 | "final_score":"67.00", 390 | "at":"【學期】" 391 | }, 392 | { 393 | "required":"【必修】", 394 | "hours":"2.0", 395 | "title":"延伸通識(科技)-近代科技概論", 396 | "remark":"", 397 | "middle_score":"*", 398 | "units":"2.0", 399 | "final_score":"95.00", 400 | "at":"【學期】" 401 | }, 402 | { 403 | "required":"【必修】", 404 | "hours":"2.0", 405 | "title":"體育-體適能加強班NTC", 406 | "remark":"", 407 | "middle_score":"*", 408 | "units":"0", 409 | "final_score":"0.00", 410 | "at":"【學期】" 411 | } 412 | ] 413 | } 414 | } 415 | """ 416 | # Restore cookies 417 | s = stateless_auth.get_requests_session_with_cookies() 418 | 419 | scores = cache.ap_query( 420 | s, "ag008", {"arg01": year, "arg02": semester, "arg03": g.username}, g.username) 421 | 422 | if not scores: 423 | return jsonify(status=const.no_content, messages="目前無學生個人成績資料", scores={}) 424 | 425 | return jsonify(status=const.ok, messages="", scores=scores) 426 | 427 | 428 | @route('/ap/samples/coursetables/normal') 429 | @route('/ap/samples/coursetables/all') 430 | @route('/ap/samples/coursetables/aftereight') 431 | @route('/ap/samples/coursetables/weekends') 432 | @route('/ap/samples/coursetables/multiinstructors') 433 | @route('/ap/samples/coursetables/wtf') 434 | def get_sample_coursetables(): 435 | sample_data = { 436 | "normal": {'Wednesday': [{'date': {'weekday': 'W', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南101', 'building': ''}, 'instructors': ['張道行'], 'title': '演算法'}, {'date': {'weekday': 'W', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南101', 'building': ''}, 'instructors': ['張道行'], 'title': '演算法'}, {'date': {'weekday': 'W', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南101', 'building': ''}, 'instructors': ['張道行'], 'title': '演算法'}], 'Thursday': [{'date': {'weekday': 'R', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['楊孟翰'], 'title': '資料庫'}, {'date': {'weekday': 'R', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['楊孟翰'], 'title': '資料庫'}, {'date': {'weekday': 'R', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['楊孟翰'], 'title': '資料庫'}, {'date': {'weekday': 'R', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': 'HS210', 'building': ''}, 'instructors': ['詹喆君'], 'title': '延伸通識(人文)-音樂賞析'}, {'date': {'weekday': 'R', 'end_time': '17:20', 'start_time': '16:30', 'section': '第 8 節'}, 'location': {'room': 'HS210', 'building': ''}, 'instructors': ['詹喆君'], 'title': '延伸通識(人文)-音樂賞析'}], 'Tuesday': [{'date': {'weekday': 'T', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '', 'building': ''}, 'instructors': ['陳忠信'], 'title': '體育-羽球'}, {'date': {'weekday': 'T', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'instructors': ['陳忠信'], 'title': '體育-羽球'}, {'date': {'weekday': 'T', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南108', 'building': ''}, 'instructors': ['林威成'], 'title': '離散數學'}, {'date': {'weekday': 'T', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南108', 'building': ''}, 'instructors': ['林威成'], 'title': '離散數學'}, {'date': {'weekday': 'T', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南108', 'building': ''}, 'instructors': ['林威成'], 'title': '離散數學'}], 'Monday': [{'date': {'weekday': 'M', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '育302', 'building': ''}, 'instructors': ['林良志'], 'title': '核心通識(五)-民主與法治'}, {'date': {'weekday': 'M', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '育302', 'building': ''}, 'instructors': ['林良志'], 'title': '核心通識(五)-民主與法治'}, {'date': {'weekday': 'M', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '育302', 'building': ''}, 'instructors': ['鐘文鈺'], 'title': '資料壓縮'}, {'date': {'weekday': 'M', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '育302', 'building': ''}, 'instructors': ['鐘文鈺'], 'title': '資料壓縮'}, {'date': {'weekday': 'M', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '育302', 'building': ''}, 'instructors': ['鐘文鈺'], 'title': '資料壓縮'}], 'Friday': [{'date': {'weekday': 'F', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['王志強'], 'title': '作業系統'}, {'date': {'weekday': 'F', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['王志強'], 'title': '作業系統'}, {'date': {'weekday': 'F', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['王志強'], 'title': '作業系統'}]}, 437 | "weekends": {'Monday': [{'date': {'weekday': 'M', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}], 'Sunday': [{'date': {'weekday': 'H', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'title': '實務專題(一)', 'instructors': ['羅孟彥']}, {'date': {'weekday': 'H', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '', 'building': ''}, 'title': '實務專題(一)', 'instructors': ['羅孟彥']}, {'date': {'weekday': 'H', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '', 'building': ''}, 'title': '實務專題(一)', 'instructors': ['羅孟彥']}, {'date': {'weekday': 'H', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '', 'building': ''}, 'title': '英語能力訓練', 'instructors': ['秦月貞']}, {'date': {'weekday': 'H', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '', 'building': ''}, 'title': '英語能力訓練', 'instructors': ['秦月貞']}], 'Thursday': [{'date': {'weekday': 'R', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}, {'date': {'weekday': 'R', 'end_time': '17:20', 'start_time': '16:30', 'section': '第 8 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}], 'Tuesday': [{'date': {'weekday': 'T', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}], 'Wednesday': [{'date': {'weekday': 'W', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}], 'Friday': [{'date': {'weekday': 'F', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}]}, 438 | "aftereight": {'Monday': [{'date': {'weekday': 'M', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}], 'Thursday': [{'date': {'weekday': 'R', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}, {'date': {'weekday': 'R', 'end_time': '17:20', 'start_time': '16:30', 'section': '第 8 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}], 'Tuesday': [{'date': {'weekday': 'T', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}], 'Wednesday': [{'date': {'weekday': 'W', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:00', 'start_time': '20:20', 'section': '第 13 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}], 'Friday': [{'date': {'weekday': 'F', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '', 'start_time': '', 'section': 'B'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '19:20', 'start_time': '18:30', 'section': '第 11 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}]}, 439 | "all": {'Monday': [{'date': {'weekday': 'M', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}], 'Sunday': [{'date': {'weekday': 'H', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'H', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'H', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['RegisterAutumn'], 'title': '5倍紅寶石'}, {'date': {'weekday': 'H', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': 'HS202', 'building': ''}, 'instructors': ['RegisterAutumn'], 'title': '5倍紅寶石'}], 'Thursday': [{'date': {'weekday': 'R', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}, {'date': {'weekday': 'R', 'end_time': '17:20', 'start_time': '16:30', 'section': '第 8 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}], 'Tuesday': [{'date': {'weekday': 'T', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}], 'Wednesday': [{'date': {'weekday': 'W', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:00', 'start_time': '20:20', 'section': '第 13 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}], 'Friday': [{'date': {'weekday': 'F', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '', 'start_time': '', 'section': 'B'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '19:20', 'start_time': '18:30', 'section': '第 11 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}]}, 440 | "multiinstructors": {'Monday': [{'date': {'weekday': 'M', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}], 'Sunday': [{'date': {'weekday': 'H', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'H', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'H', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['RegisterAutumn'], 'title': '5倍紅寶石'}, {'date': {'weekday': 'H', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': 'HS202', 'building': ''}, 'instructors': ['RegisterAutumn'], 'title': '5倍紅寶石'}, {'date': {'weekday': 'H', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': 'HS202', 'building': ''}, 'instructors': ['RegisterAutumn', '紅寶石', '藍寶石'], 'title': '5倍紅寶石'}], 'Thursday': [{'date': {'weekday': 'R', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}, {'date': {'weekday': 'R', 'end_time': '17:20', 'start_time': '16:30', 'section': '第 8 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}], 'Tuesday': [{'date': {'weekday': 'T', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}], 'Wednesday': [{'date': {'weekday': 'W', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:00', 'start_time': '20:20', 'section': '第 13 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}], 'Friday': [{'date': {'weekday': 'F', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '', 'start_time': '', 'section': 'B'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '19:20', 'start_time': '18:30', 'section': '第 11 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}]}, 441 | "wtf": {'Monday': [{'date': {'weekday': 'M', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '核心通識(五)-民主與法治', 'instructors': ['林良志']}, {'date': {'weekday': 'M', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}, {'date': {'weekday': 'M', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '育302', 'building': ''}, 'title': '資料壓縮', 'instructors': ['鐘文鈺']}], 'Sunday': [{'date': {'weekday': 'T', 'end_time': '09:00', 'start_time': '08:10', 'section': '第 1 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '', 'building': ''}, 'title': '體育-羽球', 'instructors': ['陳忠信']}, {'date': {'weekday': 'T', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南108', 'building': ''}, 'weekday': 'F', 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}, {'date': {'weekday': 'T', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南108', 'building': ''}, 'title': '離散數學', 'instructors': ['林威成']}], 'Thursday': [{'date': {'weekday': 'R', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '資料庫', 'instructors': ['楊孟翰']}, {'date': {'weekday': 'R', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}, {'date': {'weekday': 'R', 'end_time': '17:20', 'start_time': '16:30', 'section': '第 8 節'}, 'location': {'room': 'HS210', 'building': ''}, 'title': '延伸通識(人文)-音樂賞析', 'instructors': ['詹喆君']}], 'Tuesday': [{'date': {'weekday': 'H', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'H', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'H', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['RegisterAutumn'], 'title': '5倍紅寶石'}, {'date': {'weekday': 'H', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'location': {'room': 'HS202', 'building': ''}, 'instructors': ['RegisterAutumn'], 'title': '5倍紅寶石'}, {'date': {'weekday': 'H', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': 'HS202', 'building': ''}, 'instructors': ['RegisterAutumn', '紅寶石', '藍寶石'], 'title': '5倍紅寶石'}], 'Wednesday': [{'date': {'weekday': 'W', 'end_time': '14:20', 'start_time': '13:30', 'section': '第 5 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '15:20', 'start_time': '14:30', 'section': '第 6 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '16:20', 'start_time': '15:30', 'section': '第 7 節'}, 'location': {'room': '南101', 'building': ''}, 'title': '演算法', 'instructors': ['張道行']}, {'date': {'weekday': 'W', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:00', 'start_time': '20:20', 'section': '第 13 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}, {'date': {'weekday': 'W', 'end_time': '21:15', 'start_time': '22:05', 'section': '第 14 節'}, 'location': {'room': '資202', 'building': ''}, 'instructors': ['Awei'], 'title': '實用網路安全技術'}], 'Friday': [{'date': {'weekday': 'F', 'end_time': '10:00', 'start_time': '09:10', 'section': '第 2 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'date': {'weekday': 'F', 'end_time': '11:00', 'start_time': '10:10', 'section': '第 3 節'}, 'location': {'room': '資201', 'building': ''}, 'title': '作業系統', 'instructors': ['王志強']}, {'location': {'room': '資201', 'building': ''}, 'campus': 'Yanchao', 'building': '資', 'room': '201', 'date': {'weekday': 'F', 'end_time': '12:00', 'start_time': '11:10', 'section': '第 4 節'}, 'instructors': ['王志強'], 'title': '作業系統'}, {'date': {'weekday': 'F', 'end_time': '', 'start_time': '', 'section': 'B'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '19:20', 'start_time': '18:30', 'section': '第 11 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}, {'date': {'weekday': 'F', 'end_time': '20:15', 'start_time': '19:25', 'section': '第 12 節'}, 'location': {'room': '資201', 'building': ''}, 'instructors': ['John Thunder'], 'title': '黑客技巧'}]}, 442 | } 443 | 444 | return jsonify(sample_data[request.path[request.path.rfind("/") + 1:]]) 445 | 446 | 447 | @route('/ap/semester') 448 | def ap_semester(): 449 | """Get user's information. 450 | 451 | :reqheader Authorization: Using Basic Auth 452 | :resjson string value: Every semester value. format: (year,semester) 453 | :resjson string text: Every semester description text. 454 | :resjson int selected: If the value is 1, means the semester is default value on KUAS AP Website. 455 | :statuscode 200: Query successful 456 | :statuscode 401: Login failed or auth_token has been expired 457 | 458 | **Request** 459 | 460 | .. sourcecode:: http 461 | 462 | GET /latest/ap/users/info HTTP/1.1 463 | Host: kuas.grd.idv.tw:14769 464 | Authorization: Basic xxxxxxxxxxxxx= 465 | 466 | .. sourcecode:: shell 467 | 468 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/latest/ap/semester 469 | 470 | 471 | **Response** 472 | 473 | .. sourcecode:: http 474 | 475 | HTTP/1.1 200 OK 476 | Content-Type: text/html; charset=utf-8 477 | 478 | { 479 | "default":{ 480 | "value":"106,1", 481 | "text":"106學年度第1學期", 482 | "selected":1 483 | }, 484 | "semester":[ 485 | { 486 | "value":"106,1", 487 | "text":"106學年度第1學期", 488 | "selected":1 489 | }, 490 | { 491 | "value":"105,1", 492 | "text":"105學年度第1學期", 493 | "selected":0 494 | }, 495 | { 496 | "value":"105,2", 497 | "text":"105學年度第2學期", 498 | "selected":0 499 | }, 500 | { 501 | "value":"105,4", 502 | "text":"105學年度暑修", 503 | "selected":0 504 | }, 505 | { 506 | "value":"104,1", 507 | "text":"104學年度第1學期", 508 | "selected":0 509 | }, 510 | { 511 | "value":"104,2", 512 | "text":"104學年度第2學期", 513 | "selected":0 514 | }, 515 | { 516 | "value":"104,3", 517 | "text":"104學年度寒修", 518 | "selected":0 519 | }, 520 | { 521 | "value":"104,4", 522 | "text":"104學年度暑修", 523 | "selected":0 524 | }, 525 | { 526 | "value":"103,1", 527 | "text":"103學年度第1學期", 528 | "selected":0 529 | }, 530 | { 531 | "value":"103,2", 532 | "text":"103學年度第2學期", 533 | "selected":0 534 | }, 535 | { 536 | "value":"103,4", 537 | "text":"103學年度暑修", 538 | "selected":0 539 | }, 540 | { 541 | "value":"103,5", 542 | "text":"103學年度先修學期", 543 | "selected":0 544 | }, 545 | { 546 | "value":"102,1", 547 | "text":"102學年度第1學期", 548 | "selected":0 549 | }, 550 | { 551 | "value":"102,2", 552 | "text":"102學年度第2學期", 553 | "selected":0 554 | }, 555 | { 556 | "value":"102,4", 557 | "text":"102學年度暑修", 558 | "selected":0 559 | }, 560 | { 561 | "value":"101,1", 562 | "text":"101學年度第1學期", 563 | "selected":0 564 | }, 565 | { 566 | "value":"101,2", 567 | "text":"101學年度第2學期", 568 | "selected":0 569 | }, 570 | { 571 | "value":"101,3", 572 | "text":"101學年度寒修", 573 | "selected":0 574 | }, 575 | { 576 | "value":"101,4", 577 | "text":"101學年度暑修", 578 | "selected":0 579 | }, 580 | { 581 | "value":"100,1", 582 | "text":"100學年度第1學期", 583 | "selected":0 584 | }, 585 | { 586 | "value":"100,2", 587 | "text":"100學年度第2學期", 588 | "selected":0 589 | }, 590 | { 591 | "value":"100,3", 592 | "text":"100學年度寒修", 593 | "selected":0 594 | }, 595 | { 596 | "value":"100,4", 597 | "text":"100學年度暑修", 598 | "selected":0 599 | }, 600 | { 601 | "value":"99,1", 602 | "text":"99學年度第1學期", 603 | "selected":0 604 | }, 605 | { 606 | "value":"99,2", 607 | "text":"99學年度第2學期", 608 | "selected":0 609 | }, 610 | { 611 | "value":"99,3", 612 | "text":"99學年度寒修", 613 | "selected":0 614 | }, 615 | { 616 | "value":"99,4", 617 | "text":"99學年度暑修", 618 | "selected":0 619 | }, 620 | { 621 | "value":"98,1", 622 | "text":"98學年度第1學期", 623 | "selected":0 624 | }, 625 | { 626 | "value":"98,2", 627 | "text":"98學年度第2學期", 628 | "selected":0 629 | }, 630 | { 631 | "value":"98,3", 632 | "text":"98學年度寒修", 633 | "selected":0 634 | }, 635 | { 636 | "value":"98,4", 637 | "text":"98學年度暑修", 638 | "selected":0 639 | }, 640 | { 641 | "value":"97,1", 642 | "text":"97學年度第1學期", 643 | "selected":0 644 | }, 645 | { 646 | "value":"97,2", 647 | "text":"97學年度第2學期", 648 | "selected":0 649 | }, 650 | { 651 | "value":"97,3", 652 | "text":"97學年度寒修", 653 | "selected":0 654 | }, 655 | { 656 | "value":"97,4", 657 | "text":"97學年度暑修", 658 | "selected":0 659 | }, 660 | { 661 | "value":"96,1", 662 | "text":"96學年度第1學期", 663 | "selected":0 664 | }, 665 | { 666 | "value":"96,2", 667 | "text":"96學年度第2學期", 668 | "selected":0 669 | }, 670 | { 671 | "value":"96,3", 672 | "text":"96學年度寒修", 673 | "selected":0 674 | }, 675 | { 676 | "value":"96,4", 677 | "text":"96學年度暑修", 678 | "selected":0 679 | }, 680 | { 681 | "value":"95,1", 682 | "text":"95學年度第1學期", 683 | "selected":0 684 | }, 685 | { 686 | "value":"95,2", 687 | "text":"95學年度第2學期", 688 | "selected":0 689 | }, 690 | { 691 | "value":"94,1", 692 | "text":"94學年度第1學期", 693 | "selected":0 694 | }, 695 | { 696 | "value":"94,2", 697 | "text":"94學年度第2學期", 698 | "selected":0 699 | }, 700 | { 701 | "value":"93,1", 702 | "text":"93學年度第1學期", 703 | "selected":0 704 | }, 705 | { 706 | "value":"93,2", 707 | "text":"93學年度第2學期", 708 | "selected":0 709 | }, 710 | { 711 | "value":"92,2", 712 | "text":"92學年度第2學期", 713 | "selected":0 714 | } 715 | ] 716 | } 717 | """ 718 | semester_list = cache.get_semester_list() 719 | if semester_list == False: 720 | return abort(502) 721 | 722 | default_yms = list( 723 | filter(lambda x: x['selected'] == 1, semester_list))[0] 724 | 725 | # Check default args 726 | if request.args.get("default") == "1": 727 | return jsonify(default=default_yms) 728 | 729 | # Check limit args 730 | limit = request.args.get("limit") 731 | if limit: 732 | try: 733 | semester_list = semester_list[: int(limit)] 734 | except ValueError: 735 | return error.error_handle( 736 | status=400, 737 | developer_message="Error value for limit.", 738 | user_message="You type a wrong value for limit.") 739 | 740 | return jsonify( 741 | semester=semester_list, 742 | default=default_yms 743 | ) 744 | 745 | 746 | @route('/ap/queries/semester') 747 | @auth.login_required 748 | def query_post(): 749 | fncid = request.form['fncid'] 750 | arg01 = request.form['arg01'] if 'arg01' in request.form else None 751 | arg02 = request.form['arg02'] if 'arg02' in request.form else None 752 | arg03 = request.form['arg03'] if 'arg03' in request.form else None 753 | arg04 = request.form['arg04'] if 'arg04' in request.form else None 754 | 755 | # Restore cookies 756 | s = stateless_auth.get_requests_session_with_cookies() 757 | 758 | query_content = cache.ap_query( 759 | s, fncid, {"arg01": arg01, "arg02": arg02, 760 | "arg03": arg03, "arg04": arg04}, g.username) 761 | 762 | if fncid == "ag222": 763 | return json.dumps(query_content) 764 | elif fncid == "ag008": 765 | return json.dumps(query_content) 766 | else: 767 | return json.dumps(query_content) 768 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/bus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import json 4 | from flask import request, g 5 | from flask_cors import * 6 | import kuas_api.kuas.cache as cache 7 | 8 | from kuas_api.modules.stateless_auth import auth 9 | import kuas_api.modules.stateless_auth as stateless_auth 10 | import kuas_api.modules.error as error 11 | from kuas_api.modules.json import jsonify 12 | from .doc import auto 13 | 14 | 15 | # Nestable blueprints problem 16 | # not sure isn't this a best practice now. 17 | # https://github.com/mitsuhiko/flask/issues/593 18 | #from kuas_api.views.v2 import api_v2 19 | routes = [] 20 | 21 | 22 | def route(rule, **options): 23 | def decorator(f): 24 | url_rule = { 25 | "rule": rule, 26 | "view_func": f, 27 | "options": options if options else {} 28 | } 29 | 30 | routes.append(url_rule) 31 | return f 32 | 33 | return decorator 34 | 35 | 36 | @route('/bus/timetables') 37 | @auto.doc(groups=["public"]) 38 | #@cross_origin(supports_credentials=True) 39 | @auth.login_required 40 | def timetables(): 41 | """Get KUAS school bus time table. 42 | 43 | :reqheader Authorization: Using Basic Auth 44 | :query string date: Specific date to query timetable. format: yyyy-mm-dd 45 | :query string from: The start station you want to query. (not impl yet) 46 | :statuscode 200: no error 47 | 48 | 49 | **Request** 50 | 51 | without date (default the date on server) 52 | 53 | .. sourcecode:: http 54 | 55 | GET /latest/bus/timetables HTTP/1.1 56 | Host: kuas.grd.idv.tw:14769 57 | Authorization: Basic xxxxxxxxxxxxx= 58 | 59 | .. sourcecode:: shell 60 | 61 | curl -u username:password -X GET https://kuas.grd.idv.tw:14769/v2/bus/timetables 62 | 63 | with date 64 | 65 | .. sourcecode:: http 66 | 67 | GET /latest/bus/timetables?date=2015-9-1 HTTP/1.1 68 | Host: kuas.grd.idv.tw:14769 69 | Authorization: Basic xxxxxxxxxxxxx= 70 | 71 | .. sourcecode:: shell 72 | 73 | curl -u username:password -X GET https://kuas.grd.idv.tw:14769/v2/bus/timetables?date=2017-08-09 74 | 75 | 76 | **Response** 77 | 78 | .. sourcecode:: http 79 | 80 | HTTP/1.0 200 OK 81 | Content-Type: application/json 82 | 83 | { 84 | "timetable":[ 85 | { 86 | "endStation":"燕巢", 87 | "EndEnrollDateTime":"2015-08-31 17:20", 88 | "isReserve":-1, 89 | "Time":"08:20", 90 | "busId":"27034", 91 | "limitCount":"999", 92 | "reserveCount":"27", 93 | "runDateTime":"2015-09-01 08:20" 94 | }, 95 | { 96 | "endStation":"燕巢", 97 | "EndEnrollDateTime":"2015-09-01 08:00", 98 | "isReserve":-1, 99 | "Time":"13:00", 100 | "busId":"27062", 101 | "limitCount":"999", 102 | "reserveCount":"1", 103 | "runDateTime":"2015-09-01 13:00" 104 | }, 105 | { 106 | "endStation":"建工", 107 | "EndEnrollDateTime":"2015-09-01 07:15", 108 | "isReserve":-1, 109 | "Time":"12:15", 110 | "busId":"27090", 111 | "limitCount":"999", 112 | "reserveCount":"5", 113 | "runDateTime":"2015-09-01 12:15" 114 | }, 115 | { 116 | "endStation":"建工", 117 | "EndEnrollDateTime":"2015-09-01 11:45", 118 | "isReserve":-1, 119 | "Time":"16:45", 120 | "busId":"27118", 121 | "limitCount":"999", 122 | "reserveCount":"24", 123 | "runDateTime":"2015-09-01 16:45" 124 | } 125 | ], 126 | "date":"2015-9-1" 127 | } 128 | 129 | """ 130 | 131 | date = time.strftime("%Y-%m-%d", time.gmtime()) 132 | if request.args.get("date"): 133 | date = request.args.get("date") 134 | 135 | # Restore cookies 136 | s = stateless_auth.get_requests_session_with_cookies() 137 | 138 | return jsonify(date=date, timetable=cache.bus_query(s, date)) 139 | 140 | 141 | @route("/bus/reservations", methods=["GET"]) 142 | @route("/bus/reservations/", methods=["PUT"]) 143 | @route("/bus/reservations/", methods=["DELETE"]) 144 | @auth.login_required 145 | def bus_reservations(bus_id=None, cancel_key=None): 146 | 147 | # Restore cookies 148 | s = stateless_auth.get_requests_session_with_cookies() 149 | 150 | # Debugging 151 | user_agent = request.user_agent.string 152 | user_id = g.username 153 | 154 | if request.method == "GET": 155 | return jsonify(reservation=cache.bus_reserve_query(s)) 156 | elif request.method == "PUT": 157 | result = cache.bus_booking(s, bus_id, "") 158 | try: 159 | print("PUT,%s,%s,%s" % (user_agent, user_id, result)) 160 | except: 161 | print("PUT ERROR, %s, %s" % (user_agent, user_id)) 162 | 163 | return jsonify(result) 164 | elif request.method == "DELETE": 165 | result = cache.bus_booking(s, cancel_key, "un") 166 | 167 | print("DELETE,%s,%s,%s" % (user_agent, user_id, result)) 168 | 169 | return jsonify(result) 170 | 171 | 172 | 173 | return request.method 174 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/doc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import Blueprint 4 | from flask_autodoc.autodoc import Autodoc 5 | 6 | doc = Blueprint("doc", __name__, url_prefix="/v2/docs") 7 | auto = Autodoc() 8 | 9 | 10 | @doc.route('/') 11 | @doc.route('/public') 12 | def public_doc(): 13 | return auto.html(title="KUAS API Documentation") 14 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/leave.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import requests 4 | 5 | from flask import request, g 6 | from flask_cors import * 7 | 8 | import kuas_api.kuas.cache as cache 9 | 10 | from kuas_api.modules.stateless_auth import auth 11 | import kuas_api.modules.stateless_auth as stateless_auth 12 | import kuas_api.modules.const as const 13 | from kuas_api.modules.json import jsonify 14 | 15 | 16 | from .doc import auto 17 | 18 | # Nestable blueprints problem 19 | # not sure isn't this a best practice now. 20 | # https://github.com/mitsuhiko/flask/issues/593 21 | routes = [] 22 | 23 | 24 | def route(rule, **options): 25 | def decorator(f): 26 | url_rule = { 27 | "rule": rule, 28 | "view_func": f, 29 | "options": options if options else {} 30 | } 31 | 32 | routes.append(url_rule) 33 | return f 34 | 35 | return decorator 36 | 37 | 38 | @route('/leaves//') 39 | @auth.login_required 40 | def get_leave(year, semester): 41 | """Get user's leaves record. 42 | 43 | :reqheader Authorization: Using Basic Auth 44 | :query int year: Specific year to query class schedule. format: yyy (see below) 45 | :query int semester: Given a semester 46 | :statuscode 200: Query successful 47 | :statuscode 401: Login failed or auth_token has been expired 48 | 49 | **Request** 50 | 51 | .. sourcecode:: http 52 | 53 | GET /latest/leaves/105/2 HTTP/1.1 54 | Host: kuas.grd.idv.tw:14769 55 | Authorization: Basic xxxxxxxxxxxxx= 56 | 57 | .. sourcecode:: shell 58 | 59 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/latest/leaves/105/2 60 | 61 | 62 | **Response** 63 | 64 | .. sourcecode:: http 65 | 66 | HTTP/1.1 200 OK 67 | Content-Type: application/json 68 | 69 | """ 70 | timecode = [ 71 | "A", 72 | "1", 73 | "2", 74 | "3", 75 | "4", 76 | "B", 77 | "5", 78 | "6", 79 | "7", 80 | "8", 81 | "C", 82 | "11", 83 | "12", 84 | "13", 85 | "14" 86 | ] 87 | # Restore cookies 88 | s = stateless_auth.get_requests_session_with_cookies() 89 | 90 | leaves = cache.leave_query(s, year, semester) 91 | 92 | if not leaves: 93 | return jsonify(status=const.no_content, messages="本學期無缺曠課記錄", leaves=[]) 94 | else: 95 | #leaves.append() 96 | return jsonify(status=const.ok, messages="", leaves=leaves,timecode=timecode) 97 | 98 | 99 | @route('/leave/submit', methods=['POST']) 100 | @auto.doc() 101 | @cross_origin(supports_credentials=True) 102 | @auth.login_required 103 | def leave_submit(): 104 | """Take a user's leave. 105 | 106 | :reqheader Authorization: Using Basic Auth 107 | :fparam start_date: The first leave date 108 | :fparam end_date: The last leave date 109 | :fparam reason_id: The reason identifier 110 | :fparam reason_text: The reason of taking a leave 111 | :statuscode 200: Query successful 112 | :statuscode 401: Login failed or auth_token has been expired 113 | 114 | **Request** 115 | 116 | .. sourcecode:: http 117 | 118 | POST /latest/leave/submit HTTP/1.1 119 | Host: kuas.grd.idv.tw:14769 120 | Authorization: Basic xxxxxxxxxxxxx= 121 | 122 | .. sourcecode:: shell 123 | 124 | curl -X POST -d "start_date=2017-05-30&end_date=2017-05-31\\ 125 | &reason_text=I want to take a leave" \\ 126 | https://kuas.grd.idv.tw:14769/latest/leave/submit -u username:password 127 | 128 | **Response** 129 | 130 | .. sourcecode:: http 131 | 132 | HTTP/1.1 200 OK 133 | Content-Type: application/json 134 | 135 | """ 136 | if request.method == 'POST': 137 | start_date = request.form['start_date'].replace("-", "/") 138 | end_date = request.form['end_date'].replace("-", "/") 139 | reason_id = request.form[ 140 | 'reason_id'] if 'reason_id' in request.form else None 141 | reason_text = request.form[ 142 | 'reason_text'] if 'reason_text' in request.form else None 143 | section = json.loads( 144 | request.form['section']) if 'section' in request.form else None 145 | 146 | s = requests.session() 147 | set_cookies(s, session['c']) 148 | 149 | start_date = start_date.split("/") 150 | start_date[0] = str(int(start_date[0]) - 1911) 151 | start_date = "/".join(start_date) 152 | 153 | end_date = end_date.split("/") 154 | end_date[0] = str(int(end_date[0]) - 1911) 155 | end_date = "/".join(end_date) 156 | 157 | # Fixing, don't send it 158 | return json.dumps((False, "請假維修中, 目前無法請假~"), ensure_ascii=False) 159 | 160 | # Fixed 161 | # if reason_id and reason_text and section: 162 | # return json.dumps(cache.leave_submit(s, start_date, end_date, reason_id, reason_text, section)) 163 | # else: 164 | # return json.dumps((False, "Error...")) 165 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/news.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import random 4 | import json 5 | 6 | from flask import redirect 7 | 8 | import kuas_api.kuas.cache as cache 9 | 10 | 11 | from kuas_api import admin, app 12 | from kuas_api import news_db as db 13 | from flask_admin.contrib import sqla 14 | 15 | 16 | DEFAULT_WEIGHT = 1 17 | ENABLE = 1 18 | NEWS_ID = 0 19 | 20 | # Nestable blueprints problem 21 | # not sure isn't this a best practice now. 22 | # https://github.com/mitsuhiko/flask/issues/593 23 | #from kuas_api.views.v2 import api_v2 24 | routes = [] 25 | 26 | 27 | def route(rule, **options): 28 | def decorator(f): 29 | url_rule = { 30 | "rule": rule, 31 | "view_func": f, 32 | "options": options if options else {} 33 | } 34 | 35 | routes.append(url_rule) 36 | return f 37 | 38 | return decorator 39 | 40 | 41 | class News(db.Model): 42 | id = db.Column(db.Integer, primary_key=True) 43 | title = db.Column(db.String(100)) 44 | content = db.Column(db.String(100)) 45 | link = db.Column(db.Text()) 46 | image = db.Column(db.Text()) 47 | weight = db.Column(db.Integer()) 48 | 49 | # Required for administrative interface. For python 3 please use __str__ 50 | # instead. 51 | def __str__(self): 52 | return self.username 53 | 54 | 55 | class NewsInfo(db.Model): 56 | id = db.Column(db.Integer, primary_key=True) 57 | 58 | key = db.Column(db.String(64), nullable=False) 59 | value = db.Column(db.String(64)) 60 | 61 | news_id = db.Column(db.Integer(), db.ForeignKey(News.id)) 62 | news = db.relationship(News, backref='info') 63 | 64 | def __str__(self): 65 | return '%s - %s' % (self.key, self.value) 66 | 67 | 68 | class NewsAdmin(sqla.ModelView): 69 | inline_models = (NewsInfo,) 70 | 71 | 72 | admin.add_view(NewsAdmin(News, db.session)) 73 | 74 | 75 | def build_news_db(): 76 | db.drop_all() 77 | db.create_all() 78 | 79 | # Create sample Users 80 | news_list = [ 81 | { 82 | "news_title": "第八屆泰北團-夢想,「泰」不一樣", 83 | "news_image": "http://i.imgur.com/iNbbd4B.jpg", 84 | "news_url": "https://docs.google.com/forms/d/11Awcel_MfPeiEkl7zQ0MldvnAw59gXKLecbIODPOaMs/viewform?edit_requested=true", 85 | "news_content": "", 86 | "news_weight": 3 87 | }, 88 | { 89 | "news_title": "體委幹部體驗營", 90 | "news_image": "http://i.imgur.com/aJyQlJp.jpg", 91 | "news_url": "https://www.facebook.com/Kuas%E9%AB%94%E5%A7%94-440439566106678/?fref=ts", 92 | "news_content": "", 93 | "news_weight": 4 94 | }, 95 | { 96 | "news_title": "遊戲外掛 原理實戰", 97 | "news_image": "http://i.imgur.com/WkI23R2.jpg", 98 | "news_url": "https://www.facebook.com/profile.php?id=735951703168873", 99 | "news_content": "", 100 | "news_weight": 6 101 | }, 102 | { 103 | "news_title": "好日子育樂營", 104 | "news_image": "https://scontent-hkg3-1.xx.fbcdn.net/hphotos-xft1/v/t34.0-0/p206x206/12834566_977348362345349_121675822_n.jpg?oh=e04f6830fdfe5d3a77e05a8b3c32fefc&oe=56E663E6", 105 | "news_url": "https://m.facebook.com/kuasYGR/", 106 | "news_content": "", 107 | "news_weight": 6 108 | } 109 | ] 110 | 111 | for i in range(len(news_list)): 112 | user = News() 113 | user.title = news_list[i]['news_title'] 114 | user.image = news_list[i]['news_image'] 115 | user.link = news_list[i]['news_url'] 116 | user.weight = news_list[i]['news_weight'] 117 | user.content = news_list[i]['news_content'] 118 | db.session.add(user) 119 | 120 | db.session.commit() 121 | 122 | return 123 | 124 | 125 | def check_db(): 126 | import os 127 | 128 | app_dir = os.path.realpath(os.path.driname(__file__)) 129 | database_path = os.path.join(app_dir, app.config["DATABASE_FILE"]) 130 | 131 | if not os.path.exists(database_path): 132 | build_news_db() 133 | 134 | 135 | def random_by_weight(p): 136 | choice_id = [] 137 | for i in range(len(p)): 138 | choice_id += [i for _ in range(DEFAULT_WEIGHT + p[i]["news_weight"])] 139 | 140 | return p[random.choice(choice_id)] 141 | 142 | 143 | def random_news(): 144 | news_list = [] 145 | 146 | for i in News.query.all(): 147 | news_list.append({ 148 | "news_title": i.title, 149 | "news_weight": i.weight, 150 | "news_image": i.image, 151 | "news_url": i.link, 152 | "news_content": "" 153 | }) 154 | 155 | return random_by_weight(news_list) 156 | 157 | 158 | @route('/news/all') 159 | def news_all(): 160 | """Get all news. 161 | 162 | :resjson string news_image: The image URL 163 | :resjson string news_url: The link on image 164 | :resjson string news_title: The news title 165 | :statuscode 200: Query successful 166 | 167 | **Request** 168 | 169 | .. sourcecode:: http 170 | 171 | GET /latest/news/all HTTP/1.1 172 | Host: kuas.grd.idv.tw:14769 173 | Content-Type: text/html; charset=utf-8 174 | 175 | .. sourcecode:: shell 176 | 177 | curl -X GET https://kuas.grd.idv.tw:14769/latest/news/all 178 | 179 | 180 | **Response** 181 | 182 | .. sourcecode:: http 183 | 184 | HTTP/1.1 200 OK 185 | Content-Type: application/json 186 | 187 | [ 188 | { 189 | "news_weight":8, 190 | "news_image":"http://i.imgur.com/blHCDYh.jpg", 191 | "news_content":"", 192 | "news_url":"https://www.facebook.com/I.LIKE.CCI/", 193 | "news_title":"TURN 2017 海峽兩岸三地文創平面設計作品交流展" 194 | }, 195 | { 196 | "news_weight":8, 197 | "news_image":"http://i.imgur.com/tXt8YLh.jpg", 198 | "news_content":"", 199 | "news_url":"http://youth.blisswisdom.org/camp/main/", 200 | "news_title":"2017 大專青年生命成長營" 201 | } 202 | ] 203 | """ 204 | ret = [] 205 | for i in News.query.all(): 206 | ret.append({ 207 | "news_title": i.title, 208 | "news_weight": i.weight, 209 | "news_image": i.image, 210 | "news_url": i.link, 211 | "news_content": i.content 212 | }) 213 | 214 | return json.dumps(ret, ensure_ascii=False) 215 | 216 | 217 | @route('/news') 218 | def news(): 219 | """Get random one news HTML data. 220 | 221 | :statuscode 200: Query successful 222 | 223 | **Request** 224 | 225 | .. sourcecode:: http 226 | 227 | GET /latest/news HTTP/1.1 228 | Host: kuas.grd.idv.tw:14769 229 | Content-Type: text/html; charset=utf-8 230 | 231 | .. sourcecode:: shell 232 | 233 | curl -X GET https://kuas.grd.idv.tw:14769/latest/news 234 | 235 | 236 | **Response** 237 | 238 | .. sourcecode:: http 239 | 240 | HTTP/1.1 200 OK 241 | Content-Type: application/json 242 | 243 | [1, 0, "TURN 2017 \u6d77\u5cfd\u5169\u5cb8\u4e09\u5730\u6587\u5275\u5e73\u9762\u8a2d\u8a08\u4f5c\u54c1\u4ea4\u6d41\u5c55","
", "https://www.facebook.com/I.LIKE.CCI/"] 244 | """ 245 | 246 | # Get news from random news 247 | news = random_news() 248 | 249 | news_title = news["news_title"] 250 | news_template = ( 251 | "
" 252 | "
" + news["news_content"] + "
" + 254 | "
" 255 | 256 | ) 257 | 258 | news_url = news["news_url"] 259 | 260 | return json.dumps([ENABLE, NEWS_ID, news_title, news_template, news_url]) 261 | 262 | 263 | if __name__ == "__main__": 264 | build_news_db() 265 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/notifications.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import kuas_api.kuas.cache as cache 4 | 5 | from kuas_api.modules.json import jsonify 6 | 7 | 8 | # Nestable blueprints problem 9 | # not sure isn't this a best practice now. 10 | # https://github.com/mitsuhiko/flask/issues/593 11 | #from kuas_api.views.v2 import api_v2 12 | routes = [] 13 | 14 | 15 | def route(rule, **options): 16 | def decorator(f): 17 | url_rule = { 18 | "rule": rule, 19 | "view_func": f, 20 | "options": options if options else {} 21 | } 22 | 23 | routes.append(url_rule) 24 | return f 25 | 26 | return decorator 27 | 28 | 29 | @route('/notifications/') 30 | def notification(page): 31 | """Get KUAS notification 32 | 33 | :param int page: specific page for notifications 34 | 35 | 36 | **Request** 37 | 38 | .. sourcecode:: http 39 | 40 | GET /v2/notifications/1 HTTP/1.1 41 | Host: https://kuas.grd.idv.tw:14769/v2/notifications/1 42 | 43 | .. sourcecode:: shell 44 | 45 | curl -X GET https://kuas.grd.idv.tw:14769/v2/notifications/1 46 | 47 | **Response** 48 | 49 | .. sourcecode:: http 50 | 51 | HTTP/1.0 200 OK 52 | Content-Type: application/json 53 | 54 | 55 | { 56 | "page":1, 57 | "notification":[ 58 | { 59 | "link":"http://student.kuas.edu.tw/files/13-1002-45032-1.php", 60 | "info":{ 61 | "title":"『鄭豐喜國外深造獎助學金』104年度申請辦法公告", 62 | "date":"2015-09-04 ", 63 | "id":"1", 64 | "department":"諮商輔導中心" 65 | } 66 | }, 67 | { 68 | "link":"http://gender.kuas.edu.tw/files/13-1005-45026-1.php", 69 | "info":{ 70 | "title":"轉知社團法人台灣愛之希望協會辦理-104年同志公民運動系列活動,歡迎踴躍參加。", 71 | "date":"2015-09-04 ", 72 | "id":"2", 73 | "department":"性別平等專區" 74 | } 75 | }, 76 | {}, 77 | {} 78 | ] 79 | } 80 | """ 81 | 82 | return jsonify( 83 | page=page, 84 | notification=cache.notification_query(page) 85 | ) 86 | -------------------------------------------------------------------------------- /src/kuas_api/views/v2/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import g, jsonify 3 | import json 4 | 5 | import kuas_api.kuas.cache as cache 6 | import kuas_api.modules.error as error 7 | import kuas_api.modules.const as const 8 | from kuas_api.modules.stateless_auth import auth 9 | from .doc import auto 10 | 11 | # Nestable blueprints problem 12 | # not sure isn't this a best practice now. 13 | # https://github.com/mitsuhiko/flask/issues/593 14 | #from kuas_api.views.v2 import api_v2 15 | routes = [] 16 | 17 | 18 | def route(rule, **options): 19 | def decorator(f): 20 | url_rule = { 21 | "rule": rule, 22 | "view_func": f, 23 | "options": options if options else {} 24 | } 25 | 26 | routes.append(url_rule) 27 | return f 28 | 29 | return decorator 30 | 31 | 32 | @route("/token") 33 | @auto.doc(groups=["public"]) 34 | @auth.login_required 35 | def get_auth_token(): 36 | """Login to KUAS, and return token for KUAS API. 37 | 38 | 39 | :reqheader Authorization: Using Basic Auth 40 | :resjson int duration: The duration of this token to expired. 41 | :resjson string token_type: Token type of this token. 42 | :resjson strin gauth_token: Auth token. 43 | :statuscode 200: success login 44 | :statuscode 401: login fail or auth_token expired 45 | 46 | 47 | **Request**: 48 | 49 | .. sourcecode:: http 50 | 51 | GET /latest/token HTTP/1.1 52 | Host: kuas.grd.idv.tw:14769 53 | Authorization: Basic xxxxxxxxxxxxx= 54 | Accept: */* 55 | 56 | .. sourcecode:: shell 57 | 58 | curl -X GET -u username:password https://kuas.grd.idv.tw:14769/v2/token 59 | 60 | 61 | **Response**: 62 | 63 | .. sourcecode:: http 64 | 65 | HTTP/1.1 200 OK 66 | Content-Type: application/json 67 | 68 | { 69 | "duration": 3600, 70 | "token_type": "Basic", 71 | "auth_token": "adfakdflakds.fladkjflakjdf.adslkfakdadf" 72 | } 73 | """ 74 | is_login = json.loads(str(cache.red.get(g.username), "utf-8"))['is_login'] 75 | token = g.token 76 | return jsonify( 77 | auth_token=token.decode('ascii'), 78 | token_type="Basic", 79 | duration=const.token_duration, 80 | is_login= is_login 81 | ) 82 | 83 | 84 | @route('/versions/') 85 | @auto.doc(groups=["public"]) 86 | def device_version(device_type): 87 | """Get latest version for app on (`device_type`) in webstore. 88 | 89 | :param device_type: device we support 90 | :resjson version: Object of version (see below) 91 | 92 | The versions `version` is a json object list below. 93 | 94 | :json string device: query device. 95 | :json string version: latest version for device. 96 | 97 | 98 | **Request** 99 | 100 | .. sourcecode:: http 101 | 102 | GET /latest/versions/android HTTP/1.1 103 | Host: kuas.grd.idv.tw:14769 104 | 105 | .. sourcecode:: shell 106 | 107 | curl -X GET https://kuas.grd.idv.tw:14769/v2/versions/android 108 | 109 | 110 | **Response** 111 | 112 | .. sourcecode:: http 113 | 114 | HTTP/1.1 200 OK 115 | Content-Type: application/json 116 | 117 | { 118 | "version": { 119 | "device": "android", 120 | "version": "1.5.4" 121 | } 122 | } 123 | 124 | 125 | 126 | """ 127 | 128 | if device_type in const.device_version: 129 | result = { 130 | "version": { 131 | "device": device_type, 132 | "version": const.device_version[device_type] 133 | } 134 | } 135 | 136 | return jsonify(result) 137 | 138 | return error.error_handle(status=404, 139 | developer_message="Device not found.", 140 | user_message="Device not found.") 141 | 142 | 143 | @route('/servers/status') 144 | @auto.doc(groups=["public"]) 145 | def servers_status(): 146 | """Get KUAS API status for service 147 | 148 | :resjson list status: Status list (see below) 149 | 150 | Servers status list 151 | 152 | :json service: service name. 153 | :json status: HTTP status code. 154 | 155 | **Request** 156 | 157 | .. sourcecode:: http 158 | 159 | GET /v2/servers/status HTTP/1.1 160 | Host: kuas.grd.idv.tw:14769 161 | 162 | .. sourcecode:: shell 163 | 164 | curl -X GET https://kuas.grd.idv.tw:14769/v2/servers/status 165 | 166 | 167 | **Response** 168 | 169 | .. sourcecode:: http 170 | 171 | HTTP/1.1 200 OK 172 | Content-Type: application/json 173 | 174 | { 175 | "status": [ 176 | { 177 | "service": "ap", 178 | "status": 200 179 | }, 180 | { 181 | "service": "bus", 182 | "status": 200 183 | }, 184 | { 185 | "service": "leave", 186 | "status": 200 187 | } 188 | ] 189 | } 190 | 191 | """ 192 | 193 | try: 194 | original_status = cache.server_status() 195 | except Exception as err: 196 | return error.error_handle(status=404, 197 | developer_message=str(err), 198 | user_message="Something wrong.") 199 | 200 | status = { 201 | "status": [ 202 | {"service": "ap", "status": original_status[0]}, 203 | {"service": "bus", "status": original_status[1]}, 204 | {"service": "leave", "status": original_status[2]} 205 | ] 206 | } 207 | 208 | return jsonify(status) 209 | -------------------------------------------------------------------------------- /src/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/__init__.py -------------------------------------------------------------------------------- /src/test/legacy.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import time 4 | import warnings 5 | import datetime 6 | import base64 7 | import unittest 8 | import kuas_api 9 | 10 | USERNAME = "" 11 | PASSWORD = "" 12 | 13 | 14 | def ignore_warnings(func): 15 | def do_test(self, *args, **kwargs): 16 | with warnings.catch_warnings(): 17 | func(self, *args, **kwargs) 18 | return do_test 19 | 20 | 21 | class APITestCase(unittest.TestCase): 22 | 23 | def setUp(self): 24 | self.app = kuas_api.app.test_client() 25 | self.username = USERNAME 26 | self.password = PASSWORD 27 | 28 | def open_with_auth(self, url, method, username, password): 29 | basic = "Basic %s" % str( 30 | base64.b64encode( 31 | bytes(username + ":" + password, "utf-8")), "utf-8") 32 | 33 | return self.app.open(url, 34 | method=method, 35 | headers={"Authorization": basic} 36 | ) 37 | 38 | def test_api(self): 39 | rv = self.app.get("/latest/") 40 | json_object = json.loads(str(rv.data, "utf-8")) 41 | 42 | # Check version 43 | assert json_object["version"] == "2" 44 | 45 | @ignore_warnings 46 | def test_bus_get_timetables(self): 47 | rv = self.open_with_auth( 48 | "/latest/bus/timetables?date=2015-09-4", 49 | "GET", 50 | self.username, 51 | self.password) 52 | 53 | json_object = json.loads(str(rv.data, "utf-8")) 54 | 55 | # Check date 56 | # TODO: date to yyyy-mm-dd 57 | # v2/bus.py 58 | assert "date" in json_object 59 | assert json_object["date"] == "2015-09-4" 60 | 61 | # Check timetable 62 | assert "timetable" in json_object 63 | assert len(json_object["timetable"]) == 4 64 | 65 | # Check bus information 66 | bus_27037 = {"runDateTime": "2015-09-04 08:20", "limitCount": "999", 67 | "reserveCount": "7", "Time": "08:20", 68 | "EndEnrollDateTime": "2015-09-03 17:20", 69 | "endStation": "燕巢", "isReserve": 0, 70 | "busId": "27037", "cancelKey": 0 71 | } 72 | 73 | assert json_object["timetable"][0] == bus_27037 74 | 75 | def test_bus_get_put_del_reservations(self): 76 | tomorrow = datetime.datetime.strftime( 77 | datetime.date.today() + datetime.timedelta(days=1), "%Y-%m-%d") 78 | 79 | rv = self.open_with_auth( 80 | "/latest/bus/timetables?date=%s" % (tomorrow), 81 | "GET", 82 | self.username, 83 | self.password) 84 | 85 | json_object = json.loads(str(rv.data, "utf-8")) 86 | if not json_object["timetable"]: 87 | print(">>> Warning: Pass testing PUT DELETE for bus resrevations.") 88 | return False 89 | 90 | # What I want to reseve 91 | bus_id = json_object["timetable"][-1]["busId"] 92 | 93 | # GET reservations 94 | rv = self.open_with_auth( 95 | "/latest/bus/reservations", 96 | "GET", 97 | self.username, 98 | self.password) 99 | 100 | # GET reservations 101 | json_object = json.loads(str(rv.data, "utf-8")) 102 | assert "reservation" in json_object 103 | 104 | # PUT reservations 105 | rv = self.open_with_auth( 106 | "/latest/bus/reservations/%s" % (bus_id), 107 | "PUT", 108 | self.username, 109 | self.password 110 | ) 111 | 112 | json_object = json.loads(str(rv.data, "utf-8")) 113 | 114 | # Check put reservations 115 | assert json_object["message"] == "預約成功" 116 | 117 | # Check two put reservations 118 | rv = self.open_with_auth( 119 | "/latest/bus/reservations/%s" % (bus_id), 120 | "PUT", 121 | self.username, 122 | self.password 123 | ) 124 | 125 | json_object = json.loads(str(rv.data, "utf-8")) 126 | 127 | # Check has been reserve 128 | assert json_object["message"] == "該班次已預約,不可重覆預約" 129 | 130 | # Check bus go time 131 | # go_stamp = re.compile( 132 | # "\((.*?)\)").search(json_object['data']['startTime']).group(1) 133 | # go_date = time.strftime( 134 | # "%Y-%m-%d %H:%M", time.localtime(int(go_stamp) / 1000)) 135 | 136 | # Check two put reservations 137 | rv = self.open_with_auth( 138 | "/latest/bus/reservations/%s" % (bus_id), 139 | "PUT", 140 | self.username, 141 | self.password 142 | ) 143 | 144 | # Get cancel key by timetables 145 | rv = self.open_with_auth( 146 | "/latest/bus/timetables?date=%s" % (tomorrow), 147 | "GET", 148 | self.username, 149 | self.password) 150 | 151 | json_object = json.loads(str(rv.data, "utf-8")) 152 | 153 | # Get cancel key 154 | cancel_key = json_object["timetable"][-1]["cancelKey"] 155 | 156 | # DELETE reservations 157 | rv = self.open_with_auth( 158 | "/latest/bus/reservations/%s" % (cancel_key), 159 | "DELETE", 160 | self.username, 161 | self.password 162 | ) 163 | 164 | json_object = json.loads(str(rv.data, "utf-8")) 165 | 166 | # Check bus reserve has been delete 167 | assert json_object["message"] == "已取消預約紀錄" 168 | 169 | # ERROR: You can't DELETE an non-exist date. 170 | # Really need to fix for avoid HTTP 500 171 | # rv = self.open_with_auth( 172 | # "/latest/bus/reservations/%s" % (go_date), 173 | # "DELETE", 174 | # self.username, 175 | # self.password 176 | # ) 177 | # print(rv.data) 178 | 179 | # json_object = json.loads(str(rv.data, "utf-8")) 180 | 181 | # print(json_object) 182 | 183 | def test_notification(self): 184 | rv = self.app.get("/latest/notifications/1") 185 | json_object = json.loads(str(rv.data, "utf-8")) 186 | 187 | # Check return content-type 188 | assert rv.mimetype.startswith("application/json") 189 | 190 | # Check return page 191 | assert json_object["page"] == 1 192 | 193 | # Check return notification 194 | assert len(json_object["notification"]) 195 | 196 | # Check return notification content 197 | for c in json_object["notification"]: 198 | # Check link 199 | assert "link" in c 200 | assert c["link"].startswith("http://") 201 | 202 | # Check info 203 | assert "info" in c 204 | assert type(c["info"]) == dict 205 | for key in ["id", "department", "date", "title"]: 206 | assert key in c["info"] 207 | 208 | 209 | if __name__ == "__main__": 210 | unittest.main() 211 | -------------------------------------------------------------------------------- /src/test/test_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/__init__.py -------------------------------------------------------------------------------- /src/test/test_api/test_ap.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/test_ap.py -------------------------------------------------------------------------------- /src/test/test_api/test_bus.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/test_bus.py -------------------------------------------------------------------------------- /src/test/test_api/test_leave.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/test_leave.py -------------------------------------------------------------------------------- /src/test/test_api/test_news.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/test_news.py -------------------------------------------------------------------------------- /src/test/test_api/test_notifications.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/test_notifications.py -------------------------------------------------------------------------------- /src/test/test_api/test_utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_api/test_utils.py -------------------------------------------------------------------------------- /src/test/test_kuas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_kuas/__init__.py -------------------------------------------------------------------------------- /src/test/test_kuas/test_ap.py: -------------------------------------------------------------------------------- 1 | import config 2 | import unittest 3 | 4 | 5 | class FooTest(unittest.TestCase): 6 | def test_foo(self): 7 | self.assertTrue(1 == 1) 8 | self.assertTrue(config.UNITTEST_USERNAME) 9 | -------------------------------------------------------------------------------- /src/test/test_kuas/test_bus.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_kuas/test_bus.py -------------------------------------------------------------------------------- /src/test/test_kuas/test_leave.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_kuas/test_leave.py -------------------------------------------------------------------------------- /src/test/test_kuas/test_news.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_kuas/test_news.py -------------------------------------------------------------------------------- /src/test/test_kuas/test_parse.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_kuas/test_parse.py -------------------------------------------------------------------------------- /src/test/test_kuas/test_user.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_kuas/test_user.py -------------------------------------------------------------------------------- /src/test/test_modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_modules/__init__.py -------------------------------------------------------------------------------- /src/test/test_modules/test_json.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_modules/test_json.py -------------------------------------------------------------------------------- /src/test/test_modules/test_stateless_auth.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NKUST-ITC/AP-API/9e1fa4d7de1d844b453cd6b86faefba8bf051c3c/src/test/test_modules/test_stateless_auth.py -------------------------------------------------------------------------------- /src/web-server.py: -------------------------------------------------------------------------------- 1 | from kuas_api import app 2 | from flask_compress import Compress 3 | 4 | if __name__ == "__main__": 5 | Compress(app) 6 | app.run(host="0.0.0.0", port=5001, debug=True, ssl_context='adhoc') 7 | --------------------------------------------------------------------------------