From 25c2405f3530b6f5a2a95c0f92eb306f89301db3 Mon Sep 17 00:00:00 2001 From: Wolfgang Wiedermann Date: Tue, 30 May 2023 10:47:48 +0200 Subject: [PATCH] Codebeispiele: Ausgangszustand --- h1wsutils.py | 101 +++++++++++++++++++ incomings_to_stu.py | 219 ++++++++++++++++++++++++++++++++++++++++++ outgoings_from_stu.py | 106 ++++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 h1wsutils.py create mode 100644 incomings_to_stu.py create mode 100644 outgoings_from_stu.py diff --git a/h1wsutils.py b/h1wsutils.py new file mode 100644 index 0000000..bc16e9a --- /dev/null +++ b/h1wsutils.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# @author Dr. Wolfgang Wiedermann +# +# Webservice-Handling für HISinOne +# +# Abhängigkeiten: +# * zeep (pip install zeep oder python -m pip install zeep) +# +# Doku der HIS zu Webservices im Bereich Bewerbung: +# * https://wiki.his.de/mediawiki/index.php/APP-Webservices-HISinOne +# + +class H1WebServiceUtils: + # + # Attribute + # + user = None + password = None + host = "localhost" + wsdl_folder = "wsdl" + service_names = [] + proxies = {} + + # + # Konstruktor + # + def __init__(self, user, password, host, wsdl_folder, services): + self.user = user + self.password = password + self.host = host + self.wsdl_folder = wsdl_folder + self.service_names = services + + # + # Einzelne WSDL laden und relevante Teile ersetzen + # + def __download_wsdl(self, wsdl): + global HOST + import urllib.request + import re + + txt = "" + new_file = "{0}/{1}.wsdl".format(self.wsdl_folder, wsdl) + urllib.request.urlretrieve("https://{0}.kdv-fh-bayern.de/qisserver/services2/{1}?wsdl".format(self.host, wsdl), new_file) + + with open(new_file) as f: + txt = f.read() + txt = re.sub(r"http://[^/]+:8080/", ("https://{0}.kdv-fh-bayern.de/".format(self.host)), txt) + txt = re.sub(r"https://[^/]+:8443/", ("https://{0}.kdv-fh-bayern.de/".format(self.host)), txt) + txt = re.sub(r"http://localhost/", ("https://{0}.kdv-fh-bayern.de/".format(self.host)), txt) + with open(new_file, "w+") as f: + f.write(txt) + + # + # WSDL-Beschreibung von HISinOne laden + # + def download_wsdls(self): + import os + # Falls nötig, Ordner für WSDL-Dateien anlegen + if not os.path.isdir(self.wsdl_folder): + os.makedirs(self.wsdl_folder) + # Falls nötig, WSDL-Dateien laden + for service in self.service_names: + if not os.path.isfile("{0}/{1}.wsdl".format(self.wsdl_folder, service)): + self.__download_wsdl(service) + + # + # Proxy für einzelnen Service laden + # + def get_proxy(self, service): + from zeep import Client + from zeep.wsse.username import UsernameToken + + if service not in self.proxies.keys(): + svc = Client("./{0}/{1}.wsdl".format(self.wsdl_folder, service), wsse=UsernameToken(self.user, self.password)) + self.proxies[service] = svc + return self.proxies[service] + + + # + # Typ aus WSDL-Definition laden + # + def get_type(self, service, typename): + return self.proxies[service].get_type("ns0:{0}".format(typename)) + + + # + # Passwort ermitteln (entweder erfragen oder aus Passwort-Save) + # + def get_password(appname, username): + import getpass + import keyring + + # Passwort sinnvoll behandeln! + if keyring.get_password(appname, username) is None: + print("Bitte geben Sie das Passwort für den Benutzer {0} an.".format(username)) + p = getpass.getpass() + keyring.set_password(appname, username, p) + + return keyring.get_password(appname, username) \ No newline at end of file diff --git a/incomings_to_stu.py b/incomings_to_stu.py new file mode 100644 index 0000000..ce2638f --- /dev/null +++ b/incomings_to_stu.py @@ -0,0 +1,219 @@ +from datetime import datetime +from h1wsutils import H1WebServiceUtils + +# Some configuration Data statically added (for examples only!) +USER = "mo_webservice_user" +PASSWD = "geheim" +HOST = "hisinone-7350-s" +WSDL_FOLDER = "wsdl" + + +SERVICES = ( + "CourseOfStudyService", + "PersonService", + "PersonAddressService", + "StudentService201812", + "StudentService", + "KeyvalueService" +) + + +h1util = H1WebServiceUtils(USER, PASSWD, HOST, WSDL_FOLDER, SERVICES) +h1util.download_wsdls() + +# Preparing proxies +cos_svc = h1util.get_proxy("CourseOfStudyService") +person_svc = h1util.get_proxy("PersonService") +person_address_svc = h1util.get_proxy("PersonAddressService") +student2_svc = h1util.get_proxy("StudentService201812") +student_svc = h1util.get_proxy("StudentService") +value_svc = h1util.get_proxy("KeyvalueService") + +#help(person_svc.service.updatePerson) +Person = person_svc.get_type("ns0:PersonExisting") +PersonInfo = person_svc.get_type("ns0:PersoninfoDto") +StudyBeforeDTO = student_svc.get_type("ns0:StudyBeforeDto") +EntranceQualificationDto = student_svc.get_type("ns0:EntranceQualificationDto") +Examplan70 = student_svc.get_type("ns0:Examplan70") +ExamrelationDto = student_svc.get_type("ns0:ExamrelationDto") +Examimport70 = student_svc.get_type("ns0:Examimport70") +Person70 = student_svc.get_type("ns0:Person70") + +COUNTRIES = value_svc.service.getAllExtended(valueClass="CountryValue", lang="de") +#print(COUNTRIES) +TERMTYPES = value_svc.service.getAllExtended(valueClass="TermTypeValue", lang="de") +print(TERMTYPES) + +TERM_WS = [t for t in TERMTYPES if t["uniquename"] == "WiSe"][0] +TERM_SS = [t for t in TERMTYPES if t["uniquename"] == "SoSe"][0] + +def get_country_id_by_iso2(isocode): + now = datetime.date(datetime.now()) + result = [ + c["id"] + for c in COUNTRIES + if c["iso3166_1_alpha_2"] == isocode + and (c["validFrom"] < now and now < c["validTo"]) + ] + if len(result) == 1: + return result[0] + else: + raise RuntimeError(f"The given isocode {isocode} could not be resolved exactly") + +# Start of example: importing incoming students to hisinone stu + +sample_student = { + "course_of_study_uniquename": "97|271|-|-|H|-|-|P|-|9|", + "surname": "Mustermann", + "firstname": "Max", + "private_email": "max.mustermann@kdv.bayern", + "gender": "M", # one of D, M, U, W (see KeyvalueService with valueClass=GenderValue) + "home_post_address": { + "street": "Rainerstraße 28", + "postcode": "5020", + "city": "Salzburg", + "country": "A", # At that Point its country.uniquename from hisinone + #-> using the Mapping in COUNTRIES that can be resolved from iso too (its your decision what to use) + "addresstag": "home" # not mandatory, but could be set + }, + "birthdate": "2000-02-01", + "birthcity": "Dornbirn", + "country": "AT", # Here we use the ISO 3166.1 Alpha 2 Code + "nationality_country": "AT", # Here we use the ISO 3166.1 Alpha 2 Code + "home_university_first_term_year": 2021, + "home_university_country_iso": "CZ", + "entrance_qualification_country_iso": "CZ", + "entrance_qualification_grade": 1.3, + "entrance_qualification_date": "12.12.2020", + "entrance_qualification_year": "2020" +} + +# 1. Fetching the course information from HISinOne +tmp_courses = cos_svc.service.findCourseOfStudy201812(uniquename=sample_student["course_of_study_uniquename"]) +if(len(tmp_courses) != 1): + raise Exception("Invalid course_of_study given") + +course_of_study = tmp_courses[0] + +#print(course_of_study) + +# 2. Creating a preliminary student object in HISinOne +student_id = student2_svc.service.createCandidateStudent202012( + surname=sample_student["surname"], + firstname=sample_student["firstname"], + gender=sample_student["gender"], + courseOfStudyIds=[ + course_of_study["courseofstudyId"] + ], + generateRegnumber=True, + studentstatus="H", # To be checked + studystatus="N", # to be confirmed by german moveon usergroup, could be E as well but maybe will be constant for all exchangestudents + addresstag="home", # see KeyvalueService with valueClass = 'AddresstagValue' + + # Attention, addresses outside of germany at the are producing an error at the moment + # this is a bug in hisinone which will be solved in next version of HISinOne: + # + # KDV internal tickt: 4538, + # Ticket at HIS https://hiszilla.his.de/hiszilla/show_bug.cgi?id=291198 + # + # Workaround is not to use this attribute + #postaddress=sample_student["home_post_address"] + # and do 2.2. instead +) + +print(student_id) + +# 2.1. fetching missing IDs +student_detail = student2_svc.service.readStudentByStudentId(studentId=student_id) + +person_id = student_detail["personId"] +registrationnumber = student_detail["registrationnumber"] +print(f"person_id={person_id}, registrationnumber={registrationnumber}") + +# 2.2. Adding post address within a separate call: (Workaround from https://hiszilla.his.de/hiszilla/show_bug.cgi?id=291198) +address_id = person_address_svc.service.createPostaddress( + personId=person_id, + notificationCategory="STU", # Use STU for primary and STUALL for all additional ones in case of incomings + postaddress=sample_student["home_post_address"] +) +print(f"AddressID: {address_id}") + +# 2.3. Adding Information about date and place of birth, nationality and so on +# PersonService.readPerson201912 -> https://hisinone-7350-s.kdv-fh-bayern.de/qisserver/services2/PersonService?wsdl#op.id28 +# PersonService.savePerson -> https://hisinone-7350-s.kdv-fh-bayern.de/qisserver/services2/PersonService?wsdl#op.id33 +person = person_svc.service.readPerson(id=person_id) +print(person) + +person.allfirstnames=person.firstname +#person.academicdegree=... if needed +person.dateofbirth=sample_student["birthdate"] +person.gender=sample_student["gender"] +person.birthcity=sample_student["birthcity"] +person.country=sample_student["country"] + +if person.personinfo is None: + pinfo = PersonInfo(familyStatusId=1, hasDoneService=False, nationalityId=get_country_id_by_iso2(sample_student["nationality_country"])) + #pinfo.nationality=staat + person.personinfo = pinfo +else: + #person.personinfo.nationality=staat + person.personinfo.familyStatusId=1 + person.personinfo.hasDoneService=False + person.personinfo.nationalityId=get_country_id_by_iso2(sample_student["nationality_country"]) + +person_svc.service.updatePerson(person) + +# 2.4. Adding private email address +eaddress_id = person_address_svc.service.createEmail( + personId=person_id, + notificationCategory="STUALL", + email={ + "emailValue": sample_student["private_email"], + "eaddresstype": "email", + "addresstag": "privat" + } +) + +print(eaddress_id) + +# 2.5. Add information about the university (just country) the exchange student ist coming from +# for more options see definition at https://hisinone-7350-s.kdv-fh-bayern.de/qisserver/services2/StudentService?wsdl#op.id20 +study_before = StudyBeforeDTO( + firstTermYear=datetime.now().year, + firstTermTermTypeValueId=TERM_WS["id"], # Set it by real term type + ownuniversityTermYear=sample_student["home_university_first_term_year"], + countryId=get_country_id_by_iso2(sample_student["home_university_country_iso"]) +) +student_svc.service.saveStudyBeforeForPerson(studyBefore=study_before, personId=person_id) + +# 2.6. adding some entrance qualification information +# (in Germany its called "Hochschulzugangsberechtigung" or "HZB") +ENTRANCE_QUALIFICATION_TYPES = value_svc.service.getAllExtended(valueClass="EntranceQualificationTypeValue", lang="de") +#print(ENTRANCE_QUALIFICATION_TYPES) +eq_foreign_country = [ + eq + for eq in ENTRANCE_QUALIFICATION_TYPES + if eq["astat"] == '79' # use 79 if there is no information about this in MoveOn, if there is some, use correct information +][0] + +exam_import = Examimport70( + countryId=get_country_id_by_iso2(sample_student["entrance_qualification_country_iso"]), + foreignGrade=sample_student["entrance_qualification_grade"] +) + +examrelation = ExamrelationDto(workstatusId=1) +examplan = Examplan70( + dateOfWork=sample_student["entrance_qualification_date"], + year=sample_student["entrance_qualification_year"], + personId=person_id, + unitId=1, # Can be used als constant for entrance_qualifications + defaultExamrelation=examrelation, + examimport=exam_import +) +entrance_qualification = EntranceQualificationDto( + entranceQualificationTypeId=eq_foreign_country["id"], + examplan=examplan +) + +# Webservicerole needs additional right cm.app.entrancequalification.EDIT_ENTRANCEQUALIFICATION to perform this operation +student_svc.service.saveHZB(eq=entrance_qualification, ausinland=1) diff --git a/outgoings_from_stu.py b/outgoings_from_stu.py new file mode 100644 index 0000000..16db85c --- /dev/null +++ b/outgoings_from_stu.py @@ -0,0 +1,106 @@ +import re + +from datetime import datetime +from h1wsutils import H1WebServiceUtils + +# Some configuration Data statically added (for examples only!) +USER = "mo_webservice_user" +PASSWD = "geheim" +HOST = "hisinone-7350-s" +WSDL_FOLDER = "wsdl" + + +SERVICES = ( + "StayAbroadService", + "PersonService", + "PersonAddressService", + "StudentService201812", + "StudentService", + "KeyvalueService" +) + + +h1util = H1WebServiceUtils(USER, PASSWD, HOST, WSDL_FOLDER, SERVICES) +h1util.download_wsdls() + +# Preparing proxies +abroad_svc = h1util.get_proxy("StayAbroadService") +person_svc = h1util.get_proxy("PersonService") +person_address_svc = h1util.get_proxy("PersonAddressService") +student2_svc = h1util.get_proxy("StudentService201812") +student_svc = h1util.get_proxy("StudentService") +value_svc = h1util.get_proxy("KeyvalueService") + +# Input data for example, has to be replaced with values from real system +# in move on environment +#USERNAME_FROM_SHIBBOLETH = "albrecht4@beispielhochschule.de" +#FOREIGN_COUNTRY_ISO = "AU" # iso3166_1_alpha_2 + +# Alternative ones for testing +USERNAME_FROM_SHIBBOLETH = "bauer2@beispielhochschule.de" +FOREIGN_COUNTRY_ISO = "DK" # iso3166_1_alpha_2 + +# Some simple sample for validating and handling the fully qualified username from Shibboleth +UNIVERSITY_SUFFIX_SHIBBOLETH_REGEX = "^(?P[a-zA-Z0-9]+)@beispielhochschule.de$" + +matches = re.match(UNIVERSITY_SUFFIX_SHIBBOLETH_REGEX, USERNAME_FROM_SHIBBOLETH) +if not matches: + print("Invalid User, not allowed to be used in this context") + exit(code=1) + +username = matches["username"] + +# Finding person by username +person_ids = person_svc.service.findPerson( + username=username, + studyTermYear=datetime.now().year # To focus the problem that usernames can be reused for new persons after some time of inactivity +) +if len(person_ids) != 1: + print(f"ERROR: user assignment invalid or not existent, please check it in HISinOne ({len(person_ids)})") + exit(code=2) + +person_id = person_ids[0] + +# Fetching information about the user +student_info = student2_svc.service.readStudentByPersonId( + person_id, + withDegreePrograms=True, + withAddresses=True +) +print(student_info) + + +# +# +# Then continue to work in MoveOn +# +# + +# After student is back from exchange semester, write back statistics data to HISinOne + +# Those calls you can use to see what values are available within HISinOne +#STAY_ABROAD_TYPES = value_svc.service.getAll(valueClass="StayAbroadTypeValue", lang="de") +#print(STAY_ABROAD_TYPES) + +#MOBILITY_PROGRAMS = value_svc.service.getAll(valueClass="MobilityProgramValue", lang="de") +#print(MOBILITY_PROGRAMS) + +# In our sample case, the person did studies on a foreign university +STAY_ABROAD_TYPE = "01" +RESEARCH_FACILITY_TYPE="UNI" +# and he did this via ERASMUS which means an EU-Program +MOBILITY_PROGRAM = "01" + +# Note: +# calling webservice user needs the right cs.psv.stayabroad.EDIT_STAYABROAD within hisinone! +result = abroad_svc.service.createStayAbroad( + personId=person_id, + country=FOREIGN_COUNTRY_ISO, # iso3166_1_alpha_2 of the country the student has been to + numberOfMonth=7, + startdate="2022-01-15", + enddate="2022-07-15", + stayAbroadTypeValue=STAY_ABROAD_TYPE, + mobilityProgramValue=MOBILITY_PROGRAM, + researchFacilityName="Sampleuniversity of Denmark", + researchFacilityTypeValue=RESEARCH_FACILITY_TYPE +) \ No newline at end of file