diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..440c357 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: .idea/workspace.xml .idea/tasks.xml .idea/dictionaries .idea/vcs.xml .idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml .idea/sqlDataSources.xml .idea/dynamic.xml .idea/uiDesigner.xml + +# Gradle: .idea/gradle.xml .idea/libraries + +# Mongo Explorer plugin: .idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ /out/ + +# mpeltonen/sbt-idea plugin .idea_modules/ + +# JIRA plugin atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20a4b23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Markus Bilz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..74a993d --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +init: + pip install -r requirements.txt + diff --git a/dualis/main.py b/dualis/main.py new file mode 100644 index 0000000..6e62ad2 --- /dev/null +++ b/dualis/main.py @@ -0,0 +1,102 @@ +import requests +from bs4 import BeautifulSoup +from flask import Flask, jsonify, request +from werkzeug.exceptions import abort + +app = Flask(__name__) + +base_url = "https://dualis.dhbw.de" + + +@app.route("/dualis/api/v1.0/grades/", methods=['GET']) +def get_grades(): + """ + api endpoint to query grades from dualis.dhbw.de. Function expects credentials in GET request + like {"user":"karel.zeman@dhbw-karlsruhe.de","password":"journeyToTheCenterOftheEarth"} + :return: grades of all semesters from all modules as json + """ + if not request.json or not 'password' in request.json or not 'user' in request.json: + abort(401) + + # retrieve password and username from body + request_json = request.get_json() + user = request_json.get('user') + password = request_json.get('password') + + # create a session + url = base_url + "/scripts/mgrqcgi?APPNAME=CampusNet&PRGNAME=EXTERNALPAGES&ARGUMENTS=-N000000000000001,-N000324,-Awelcome" + cookie_request = requests.get(url) + + data = {"usrname": user, "pass": password, + "APPNAME": "CampusNet", + "PRGNAME": "LOGINCHECK", + "ARGUMENTS": "clino,usrname,pass,menuno,menu_type, browser,platform", + "clino": "000000000000001", + "menuno": "000324", + "menu_type": "classic", + "browser": "", + "platform": "" + } + # return dualis response code, if response code is not 200 + login_response = requests.post(url, data=data, headers=None, verify=True, cookies=cookie_request.cookies) + arguments = login_response.headers['REFRESH'] + if not login_response.ok: + abort(login_response.status_code) + + # redirecting to course results... + url_content = base_url + "/scripts/mgrqcgi?APPNAME=CampusNet&PRGNAME=STARTPAGE_DISPATCH&ARGUMENTS=" + arguments[79:] + url_content = url_content.replace("STARTPAGE_DISPATCH", "COURSERESULTS") + semester_ids_response = requests.get(url_content, cookies=login_response.cookies) + if not semester_ids_response.ok: + abort(semester_ids_response.status_code) + + # get ids of all semester, replaces -N ... + soup = BeautifulSoup(semester_ids_response.content, 'html.parser') + options = soup.find_all('option') + semester_ids = [option['value'] for option in options] + + units = [] + for semester_id in semester_ids: + semester_response = requests.get(url_content[:-15] + semester_id, cookies=login_response.cookies) + semester_soup = BeautifulSoup(semester_response.content, 'html.parser') + + # get unit details from javascript + unit_urls = [] + for script in semester_soup.find_all('script'): + unshortend_url = script.next.strip() + url = unshortend_url[301:414] + if url is not "": + unit_urls.append(url) + + # querying unit details + for url in unit_urls: + detail_response = requests.get(base_url + url, cookies=login_response.cookies) + detail_soup = BeautifulSoup(detail_response.content, "html.parser") + h1 = detail_soup.find("h1").text.strip() + table = detail_soup.find("table", {"class": "tb"}) + td = [td.text.strip() for td in table.find_all("td")] + unit = {'name': h1.replace("\n", " ").replace("\r", ""), 'exams': []} + for idx in range(13, len(td) - 5, 6): + exam = {'name': td[idx], 'date': td[idx + 1], 'grade': td[idx + 2], 'externally accepted': td[idx + 3]} + unit['exams'].append(exam) + units.append(unit) + + # find logout url in html source code and logout + logout_url = base_url + soup.find('a', {'id': 'logoutButton'})['href'] + logout(logout_url, cookie_request.cookies) + # return dict containing units and exams as valid json + return jsonify(units), 200 + + +def logout(url, cookies): + """ + Function to perform logout in dualis.dhbw.de + :param url: url to perform logout + :param cookies: cookie with session information + :return: boolean whether logging out was successful + """ + return requests.get(url=url, cookies=cookies).ok + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..17de390 --- /dev/null +++ b/readme.md @@ -0,0 +1,65 @@ +# dualis api +This is an **unofficial API** for [dualis](https://www.dualis.dhbw.de) by DHBW (Cooperative State Univerity) built on +top of Python Flask, Requests and Beautiful Soup. This is a student project and is not affiliated with DHBW. + +## purpose +Checking for new marks in dualis can be very tedious. I thought, it would be great to automate it and spend your time on things +and spend my time on things that count. That`s why I decided to create an API interface to query all your grades at once, +which can then connect to all your apps and bots. + +## usage +### sample request +Using the API is really simple as there is just one endpoint, that let's you query all grades from all semesters. Just +pass in your credentials in the body of the GET request and you are good to go. Be aware, it might take a few seconds +until you receive a response, as the API has to make plenty of requests until all data is gathered (for my account it +took around 20 sec.) +``` +$ curl -i -H "Content-Type: application/json" -X GET -d '{"user":"karel.zeman@dhbw-karlsruhe.de","password":"journeyToTheCenterOftheEarth"}' http://localhost:5000/dualis/api/v1.0/grades/ +``` +### sample output +``` +[ +... + { + "name": "Fundamentals of IT (SU 2017)", + "exams": [ + { + "name": "Klausur (100%)", + "date": "31.05.2017", + "grade": "1,0", + "externally accepted": "" + }, + { + "name": "Grundlegende Konzepte der IT (6)", + "date": "", + "grade": "100,0", + "externally accepted": "" + }, + { + "name": "Kommunikations- und Betriebssysteme (9)", + "date": "", + "grade": "100,0", + "externally accepted": "" + } + ] + }, +... +] +``` +## installation +Installing is rather straight forward, but here is how you'd wanna do it. +### linux +``` +$ wget https://github.com/KarelZe/dualis/archive/dualis.zip +$ unzip master.zip +$ mv dualis-master dualis +$ cd dualis +$ make +``` + +## todos +See the [issues tab](https://github.com/KarelZe/yown/issues) for details. + +## contact + +Feel free to send me a mail at [github@markusbilz.com](mailto:github@markusbilz.com). diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5acf2a9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +werkzeug +requests +beautifulsoup4 +flask \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c3c32af --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages + + +with open('README.md') as f: + readme = f.read() + +with open('LICENSE') as f: + license = f.read() + +setup( + name='dualis', + version='0.0.1', + description='This is a set of python scripts to scrape grades from dualis.dhbw.de', + long_description=readme, + author='Markus Bilz', + author_email='github@markusbilz.com', + url='https://github.com/karelze/dualis', + license=license, + packages=find_packages(exclude=('tests', 'docs')) +) +