Commit 657c711a authored by Gabriel Dengler's avatar Gabriel Dengler
Browse files

🚀 First working version for new campo portal

parent 0d028ec1
......@@ -7,6 +7,7 @@ from bs4 import BeautifulSoup
from log import print_info, print_warn
from urllib.parse import urlparse
from colorama import init as colorama_init, Fore, Style
from lxml import etree
from lxml.html import fromstring
from urllib.parse import unquote, urlencode
......@@ -15,15 +16,12 @@ headers = {
+ '(KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36'
}
url_login_form = 'https://www.campus.uni-erlangen.de/qisserver/rds?' \
'state=user&type=1'
url_exams_page = 'https://www.campo.fau.de/qisserver/pages/sul/examAssessment/personExamsReadonly.xhtml?' \
'_flowId=examsOverviewForPerson-flow&_flowExecutionKey=e1s1'
url_exams_page = 'https://www.campus.uni-erlangen.de/qisserver/rds?' \
'state=template&template=pruefungen'
AUTH_STATE_URL = 'https://www.campus.uni-erlangen.de/Shibboleth.sso/Login'
AUTH_STATE_URL = 'https://www.campo.fau.de/Shibboleth.sso/Login'
OAUTH_URL = 'https://www.sso.uni-erlangen.de/simplesaml/module.php/core/loginuserpass.php?'
SAML_URL = 'https://www.campus.uni-erlangen.de/Shibboleth.sso/SAML2/POST'
SAML_URL = 'https://www.campo.fau.de/Shibboleth.sso/SAML2/POST'
class Connection:
......@@ -79,17 +77,13 @@ class Connection:
while True:
try:
success = self.do_sso()
success = self.__do_sso()
if not success:
return False
r = self.__get(url_exams_page)
self.url_exam_results = find_exam_results_url(r.content)
if self.url_exam_results:
return True
self.__expand_rows()
print_warn('exam results url not found')
return True
except Exception as e:
print_warn(e)
......@@ -97,7 +91,7 @@ class Connection:
time.sleep(5)
def do_sso(self):
def __do_sso(self):
print(Style.DIM + 'generate auth state session...', end=' ', flush=True)
self.s = requests.Session()
auth_state_rq = self.s.get(AUTH_STATE_URL)
......@@ -125,88 +119,85 @@ class Connection:
self.s.post(SAML_URL, data=payload)
return True
def __get_grades_overview_page(self):
return self.__get(self.url_exam_results).content
def __get_course_page(self, base_url, node_id, payload):
return self.__get(base_url,
data={'nodeID': node_id, **payload}).content
def __expand_rows(self):
# Get authenticity token
raw_site = self.__get(url_exams_page)
dom = fromstring(raw_site.text)
self.authenticity_token = dom.xpath('//input[@name=\'authenticity_token\']/@value')[0]
def __get_grades_for_course(self, course_html):
rows = BeautifulSoup(course_html, 'html5lib') \
.find('table', attrs={'id': 'notenspiegel'}) \
.find('tbody').find_all('tr')
# Execute request to actuall expand all rows
data = {
'activePageElementId': 'examsReadonly:overviewAsTreeReadonly:tree:collapseAll2',
'refreshButtonClickedId': '',
'navigationPosition': '',
'authenticity_token': self.authenticity_token,
'autoScroll': '',
'examsReadonly:overviewAsTreeReadonly:collapsiblePanelCollapsedState': 'false',
'examsReadonly:degreeProgramProgressForReportAsTree:collapsiblePanelCollapsedState': 'true',
'examsReadonly_SUBMIT': '1',
'javax.faces.ViewState': 'e1s1',
'javax.faces.behavior.event': 'action',
'javax.faces.partial.event': 'click',
'javax.faces.source': 'examsReadonly:overviewAsTreeReadonly:tree:expandAll2',
'javax.faces.partial.ajax': 'true',
'javax.faces.partial.execute': 'examsReadonly:overviewAsTreeReadonly:tree:expandAll2',
'javax.faces.partial.render': 'examsReadonly:overviewAsTreeReadonly:tree:expandAll2 ' \
'examsReadonly:overviewAsTreeReadonly:tree:ExamOverviewForPersonTreeReadonly ' \
'examsReadonly:overviewAsTreeReadonly:tree:collapseAll2 ' \
'examsReadonly:messages-infobox',
'examsReadonly': 'examsReadonly'
}
def p(list): return [' '.join(s.get_text().split()) for s in list]
self.__post(url_exams_page, data)
headers = [p(r) for r in [r.find_all('th')
for r in rows] if len(r) > 0]
grades = [p(r) for r in [r.find_all('td') for r in rows] if len(r) > 0]
header = next((h for h in headers if h[0].startswith('#')))
grades = [dict(zip(header, row)) for row in grades]
def __read_grades(self):
# Open grades page
raw_site = self.__get(url_exams_page)
dom = fromstring(raw_site.text)
# Find out header files
module_names = [ele.text.strip() for ele in dom.xpath(
'//div[@id=\'examsReadonly:overviewAsTreeReadonly:tree:ExamOverviewForPersonTreeReadonly\']' \
'//table[@class=\'treeTableWithIcons\']/tr[1]/th')]
module_names.pop(0) # Remove layer tag which confuses anyway
# Read elements one by one
modules = []
for grade in grades:
if not grade.get('Semester'):
grade['exams'] = []
modules.append(grade)
else:
if len(modules) == 0:
modules.append({ 'exams': [] })
modules[-1]['exams'].append(grade)
for row in dom.xpath(
'//div[@id=\'examsReadonly:overviewAsTreeReadonly:tree:ExamOverviewForPersonTreeReadonly\']' \
'//table[@class=\'treeTableWithIcons\']/tr[position() > 1]'):
# Reading the information from the columns
columns = []
for ele in row.xpath('td'):
ele_span = ele.xpath('span')
if len(ele_span) > 0 and ele_span[0].text is not None:
columns.append(ele_span[0].text.strip())
else:
columns.append('')
this_module = {}
# Going backwards to avoid problems with indentations
for i in range(min(len(module_names), len(columns))):
name = module_names[len(module_names) - 1 - i]
content = columns[len(columns) - 1 - i]
this_module[name] = content
modules.append(this_module)
return modules
def get_grades(self):
while True:
try:
# Case 1: No overview page, overview page is results page
overview_page = self.__get_grades_overview_page()
if 'selectStg' not in str(overview_page):
return self.__get_grades_for_course(overview_page)
# Case 2: Multiple pages
base_url = BeautifulSoup(overview_page, 'html5lib') \
.find('form', attrs={'id': 'selectStg'}) \
.get('action')
courses = [ele.get('value') for ele in \
BeautifulSoup(overview_page, 'html5lib') \
.find('form', attrs={'id': 'selectStg'}) \
.findAll('input', attrs={'name': 'nodeID'})]
payload = {ele.get('name'): ele.get('value') for ele in \
BeautifulSoup(overview_page, 'html5lib') \
.find('form', attrs={'id': 'selectStg'}) \
.findAll('input', attrs={'type': 'hidden'})}
modules = []
for course in courses:
course_html = self.__get_course_page(base_url, course, payload)
modules += self.__get_grades_for_course(course_html)
return modules
print('test1')
return self.__read_grades()
except Exception as e:
print_warn(e)
time.sleep(5)
self.__connect()
def find_exam_results_url(page):
try:
return BeautifulSoup(page, 'html5lib') \
.find('a', attrs={'id': 'notenspiegelStudent'}) \
.get('href')
except Exception as e:
print_warn(e)
return None
if __name__ == '__main__':
with Connection() as c:
grades = c.get_grades()
for grade in grades:
print(f'{grade.get("Prüfungstext")}:'.rjust(
60), f'{grade.get("Note")}')
......@@ -145,86 +145,38 @@ def send_startup_message(chat_id):
def send_goodbye_message(chat_id):
send_message(chat_id, "I am not here anymore.")
def is_relevant_module(module):
try:
id = int(module['#'])
return id not in to_ignore and module['exams']
except:
return False
def read_module_properties(module):
def is_relevant_module(module):
try:
exam = module['Prüfungstext']
date = module['Prüfungsdatum']
grade = module['Note']
status = module['Status']
ects = module['ECTS']
if not grade:
this_ects = 0
grade_ects = 0
for child_exam in module['exams']:
try:
raw_ects = ''.join(filter(lambda x: x == '.' or x.isdigit(), child_exam['ECTS'].replace(',', '.')))
raw_grade = ''.join(filter(lambda x: x == '.' or x.isdigit(), child_exam['Note'].replace(',', '.')))
if float(raw_grade) > 0:
this_ects += float(raw_ects)
grade_ects += float(raw_ects) * float(raw_grade)
except ValueError:
pass
if this_ects != 0:
grade = grade_ects/this_ects
grade = '{:.1f}'.format(grade).replace('.', ',')
else:
grade = '-/-'
if not ects:
ects = 0
for child_exam in module['exams']:
try:
raw_ects = ''.join(filter(lambda x: x == '.' or x.isdigit(), child_exam['ECTS'].replace(',', '.')))
ects += float(raw_ects)
except ValueError:
pass
ects = '{:.1f}'.format(ects).replace('.', ',')
if not date:
date = set()
for child_exam in module['exams']:
if child_exam['Prüfungsdatum']: date.add(child_exam['Prüfungsdatum'])
date = '/'.join(date)
if not status:
status = set()
for child_exam in module['exams']:
if child_exam['Status']: status.add(child_exam['Status'])
status = '/'.join(status)
return exam, date, grade, status, ects
return module['Status'] != '' and module['Status'] != 'PV' and module['Status'] != 'BE'
except:
return None
return True
def construct_update_message(change, username):
try:
# Check if we should print the message anyways
if not is_relevant_module(change):
return None
exam, date, grade, status, ects = read_module_properties(change)
return '\n'.join([
'There is an update to the exams of user '+username+':',
'Exam: ' + exam,
'On: ' + date,
'Grade: ' + grade,
'Status: ' + status,
'ECTS: ' + ects,
'Raw change: ' + str(change)
])
# Print with fixed order
msg = 'There is an update to the exams of user {}:\n'.format(username)
for name in ['Titel', 'Nummer', 'Bewertung', 'Status', 'ECTS-Punkte', 'Vermerk', 'Versuch']:
content = change[name]
msg += '{}: {}\n'.format(name, content if content != '' else '/')
msg = msg.rstrip()
return msg
except:
return None
# Fallback: print the contents in any order
try:
msg = 'There is an update to the exams of user {}:\n'.format(username)
for name, content in sorted(change.items()):
msg += '{}: {}\n'.format(name, content if content != '' else '/')
msg = msg.rstrip()
return msg
except:
return None
# Telegram Commands
......@@ -293,7 +245,8 @@ def tgc_on_grades(update, context):
send_message(chat_id, msg)
try:
module_exam, module_date, module_grade, module_status, module_ects = read_module_properties(module)
module_ects = module['ECTS-Punkte']
module_grade = module['Bewertung']
raw_ects = ''.join(filter(lambda x: x == '.' or x.isdigit(), module_ects.replace(',', '.')))
raw_grade = ''.join(filter(lambda x: x == '.' or x.isdigit(), module_grade.replace(',', '.')))
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment