diff --git a/README.md b/README.md index 547684cd20fadcea7b5f2df3d4a494dd821864e0..17b7cb1626eb85d882c2741d6a30ab5758ef1579 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,10 @@ Notwendig: * git (zum Anzeigen der aktuellen Version) Optional (wird für einzelne Features benötigt): -* python-lxml (Campus Import) +* python-lxml (Campus- und RO-Import) +* python-pytz (RO-Import) * python-ldap (Login mit Fachschaftsaccount) -* python-icalendar (SoGo-Kalenderimport für Sitzungsankündigungen) +* python-icalendar (RO-Import, Kalenderimport für Sitzungsankündigungen) * python-mysql-connector (wenn MySQL als Datenbank verwendet werden soll) * python-coverage (Für Coverage Tests benötigt) diff --git a/config.py.example b/config.py.example index bbbed17acd8d288f28ef84c2ac9d772d13ed376f..767ec61fbf69cec53dd3f272e53d0470234a72e7 100644 --- a/config.py.example +++ b/config.py.example @@ -38,3 +38,5 @@ MAIL_FROM = 'Video AG-Website <videoag-it@lists.fsmpi.rwth-aachen.de>' MAIL_SUFFIX = 'fsmpi.rwth-aachen.de' MAIL_DEFAULT = 'Video AG <videoag@fsmpi.rwth-aachen.de>' MAIL_ADMINS = 'videoag-it@lists.fsmpi.rwth-aachen.de' +STREAMING_SERVER = 'rtmp://video-web-0.fsmpi.rwth-aachen.de/src/' +BACKUP_STREAMING_SERVER = 'rtmp://video-web-1.fsmpi.rwth-aachen.de/src/' diff --git a/edit.py b/edit.py index a76f9f25087ea7827c10d2ab0c357c89e11a941e..c04b7917db3a6d6790c9c95dee51a961a904b26e 100644 --- a/edit.py +++ b/edit.py @@ -120,6 +120,7 @@ editable_tables = { 'editable_fields': { 'name': {'type': 'shortstring'}, 'description': {'type': 'text'}, + 'deleted': {'type': 'boolean'} }, 'creationtime_fields': ['created_by', 'time_created', 'time_updated'] } } diff --git a/importer.py b/importer.py index c7d2e63d1eff395f4d3e4a3e9061637ebabaae5d..90e893b100043ce1930ae4840871c48d25ce2f4b 100644 --- a/importer.py +++ b/importer.py @@ -1,5 +1,8 @@ from server import * +import urllib.request +import urllib.parse + @app.route('/internal/import/<int:id>', methods=['GET', 'POST']) @mod_required def list_import_sources(id): @@ -25,104 +28,161 @@ def list_import_sources(id): return render_template('import_campus.html', course=courses, import_campus=import_campus, events=[]) +def fetch_co_course_events(i): + from lxml import html + from lxml import etree + events = [] + try: + remote_html = urllib.request.urlopen(i['url']).read() + except: + flash("Ungültige URL: '"+i['url']+"'") + tablexpath = "//td[text()='Termine und Ort']/following::table[1]" + basetable = html.fromstring(remote_html).xpath(tablexpath)[0] + parsebase = html.tostring(basetable); + + #parse recurring events + toparse = [i['url']] + for j in basetable.xpath("//table[@cellpadding='5']//tr[@class='hierarchy4' and td[@name='togglePeriodApp']]"): + url = str(j.xpath("td[@name='togglePeriodApp']/a/@href")[0]) + toparse.append(url) + events_raw = [] + for j in toparse: + if j.startswith('event'): + url = 'https://www.campus.rwth-aachen.de/rwth/all/'+j + else: + url = j + text = urllib.request.urlopen(url).read() + dom = html.fromstring(text).xpath(tablexpath)[0] + #we get the "heading" row, from it extract the room and time. best way to get it is to match on the picture -.- + baserow = dom.xpath("//table[@cellpadding='5']//tr[@class='hierarchy4' and td[@name='togglePeriodApp']/*/img[@src='../../server/img/minus.gif']]") + if not baserow: + continue + baserow = baserow[0] + rowdata = {'dates': []} + + # "kein raum vergeben" is a special case, else use campus id + if baserow.xpath("td[6]/text()")[0] == 'Kein Raum vergeben': + rowdata['place'] = '' + elif baserow.xpath("td[6]/a"): + rowdata['place'] = baserow.xpath("td[6]/a")[0].text_content() + else: + rowdata['place'] = baserow.xpath("td[6]/text()")[0].split(' ',1)[0] + + rowdata['start'] = baserow.xpath("td[3]/text()")[0] + rowdata['end'] = baserow.xpath("td[5]/text()")[0] + rowdata['dates'] = baserow.getparent().xpath("tr[@class='hierarchy5']//td[@colspan='3']/text()") + events_raw.append(rowdata) + + # parse single appointments + if basetable.xpath("//table[@cellpadding='3']/tr/td[text()='Einmalige Termine:']"): + singletable = basetable.xpath("//table[@cellpadding='3']/tr/td[text()='Einmalige Termine:']")[0].getparent().getparent() + for row in singletable.xpath("tr/td[2]"): + rowdata = {} + if row.xpath("text()[2]")[0] == 'Kein Raum vergeben': + rowdata['place'] = '' + elif row.xpath("a"): + rowdata['place'] = row.xpath("a")[0].text_content() + else: + rowdata['place'] = row.xpath("text()[2]")[0].split(' ',1)[0] + + rowdata['dates'] = [row.xpath("text()[1]")[0][4:14]] + rowdata['start'] = row.xpath("text()[1]")[0][17:22] + rowdata['end'] = row.xpath("text()[1]")[0][27:32] + events_raw.append(rowdata) + + #now we have to filter our data and do some lookups + for j in events_raw: + for k in j['dates']: + e = {} + fmt= "%d.%m.%Y %H:%M" + e['time'] = datetime.strptime("%s %s"%(k,j['start']) ,fmt) + e['duration'] = int((datetime.strptime("%s %s"%(k,j['end']) ,fmt) - e['time']).seconds/60) + j['place'] = str(j['place']) + if j['place'] != '': + dbplace = query("SELECT name FROM places WHERE (campus_room = ?) OR (campus_name = ?) OR ((NOT campus_name) AND name = ?)",j['place'],j['place'],j['place']) + if dbplace: + e['place'] = dbplace[0]['name'] + else: + e['place'] = 'Unbekannter Ort ('+j['place']+')' + else: + e['place'] = '' + e['title'] = i['type'] + events.append(e) + # it is parsed. + return events + +def fetch_ro_event_ical(ids): + data = {'pMode': 'T', 'pInclPruef': 'N', 'pInclPruefGepl': 'N', 'pOutputFormat': '99', 'pCharset': 'UTF8', 'pMaskAction': 'DOWNLOAD'} + data = list(data.items()) + for id in ids: + data.append(('pTerminNr', id)) + data = urllib.parse.urlencode(data).encode('utf-8') + r = urllib.request.Request('https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbKalender.wbExport', + data=data, method='POST') + with urllib.request.urlopen(r) as f: + return f.read().decode('utf-8') + +def fetch_ro_course_ical(id): + from lxml import html + url = 'https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbTermin_List.wbLehrveranstaltung?pStpSpNr='+'%i'%(int(id)) + req = urllib.request.urlopen(url) + dom = html.fromstring(req.read()) + event_ids = [x.value for x in dom.xpath('//input[@name="pTerminNr"]')] + return fetch_ro_event_ical(event_ids) + +def fetch_ro_course_events(item): + import icalendar + import pytz + localtz = pytz.timezone('Europe/Berlin') + # First fix crappy javascript fragment-Paths + url = urllib.parse.urlparse(item['url'].replace('#/', '')) + args = urllib.parse.parse_qs(url.query) + if 'pStpSpNr' in args: # Legacy URLs + id = args['pStpSpNr'][0] + elif url.path.split('/')[-2] == 'courses': # New URLs + id = url.path.split('/')[-1] + else: + flash("Ungültige URL: '"+i['url']+"'") + cal = icalendar.Calendar().from_ical(fetch_ro_course_ical(id)) + events = [] + for comp in cal.subcomponents: + if comp.name != 'VEVENT': + continue + if comp.get('STATUS') != 'CONFIRMED': + continue + e = {} + place = str(comp.get('LOCATION', '')) + if place: + campus_room = place.split('(')[-1].split(')')[0] + dbplace = query('SELECT name FROM places WHERE campus_room = ?', campus_room) + if dbplace: + e['place'] = dbplace[0]['name'] + else: + e['place'] = 'Unbekannter Ort ('+place+')' + else: + e['place'] = '' + e['time'] = comp['DTSTART'].dt.astimezone(localtz).replace(tzinfo=None) + e['duration'] = int((comp['DTEND'].dt - comp['DTSTART'].dt).seconds/60) + e['title'] = item['type'] + events.append(e) + return events + @app.route('/internal/import/<int:id>/now', methods=['GET', 'POST']) @mod_required def import_from(id): - courses = query('SELECT * FROM courses WHERE id = ?', id)[0] lectures = query('SELECT * FROM lectures WHERE course_id = ?', courses['id']) - - import_campus = query('SELECT * FROM import_campus WHERE course_id = ?',id) events = [] try: - from lxml import html - from lxml import etree - import urllib.request # if u have to port this to anything new, god be with you. for i in import_campus: - try: - remote_html = urllib.request.urlopen(i['url']).read() - except: - flash("Ungültige URL: '"+i['url']+"'") - tablexpath = "//td[text()='Termine und Ort']/following::table[1]" - basetable = html.fromstring(remote_html).xpath(tablexpath)[0] - parsebase = html.tostring(basetable); - - #parse recurring events - toparse = [i['url']] - for j in basetable.xpath("//table[@cellpadding='5']//tr[@class='hierarchy4' and td[@name='togglePeriodApp']]"): - url = str(j.xpath("td[@name='togglePeriodApp']/a/@href")[0]) - toparse.append(url) - events_raw = [] - for j in toparse: - if j.startswith('event'): - url = 'https://www.campus.rwth-aachen.de/rwth/all/'+j - else: - url = j - text = urllib.request.urlopen(url).read() - dom = html.fromstring(text).xpath(tablexpath)[0] - #we get the "heading" row, from it extract the room and time. best way to get it is to match on the picture -.- - baserow = dom.xpath("//table[@cellpadding='5']//tr[@class='hierarchy4' and td[@name='togglePeriodApp']/*/img[@src='../../server/img/minus.gif']]") - if not baserow: - continue - baserow = baserow[0] - rowdata = {'dates': []} - - # "kein raum vergeben" is a special case, else use campus id - if baserow.xpath("td[6]/text()")[0] == 'Kein Raum vergeben': - rowdata['place'] = '' - elif baserow.xpath("td[6]/a"): - rowdata['place'] = baserow.xpath("td[6]/a")[0].text_content() - else: - rowdata['place'] = baserow.xpath("td[6]/text()")[0].split(' ',1)[0] - - rowdata['start'] = baserow.xpath("td[3]/text()")[0] - rowdata['end'] = baserow.xpath("td[5]/text()")[0] - rowdata['dates'] = baserow.getparent().xpath("tr[@class='hierarchy5']//td[@colspan='3']/text()") - events_raw.append(rowdata) - - # parse single appointments - if basetable.xpath("//table[@cellpadding='3']/tr/td[text()='Einmalige Termine:']"): - singletable = basetable.xpath("//table[@cellpadding='3']/tr/td[text()='Einmalige Termine:']")[0].getparent().getparent() - for row in singletable.xpath("tr/td[2]"): - rowdata = {} - if row.xpath("text()[2]")[0] == 'Kein Raum vergeben': - rowdata['place'] = '' - elif row.xpath("a"): - rowdata['place'] = row.xpath("a")[0].text_content() - else: - rowdata['place'] = row.xpath("text()[2]")[0].split(' ',1)[0] - - rowdata['dates'] = [row.xpath("text()[1]")[0][4:14]] - rowdata['start'] = row.xpath("text()[1]")[0][17:22] - rowdata['end'] = row.xpath("text()[1]")[0][27:32] - events_raw.append(rowdata) - - #now we have to filter our data and do some lookups - for j in events_raw: - for k in j['dates']: - e = {} - fmt= "%d.%m.%Y %H:%M" - e['time'] = datetime.strptime("%s %s"%(k,j['start']) ,fmt) - e['duration'] = int((datetime.strptime("%s %s"%(k,j['end']) ,fmt) - e['time']).seconds/60) - j['place'] = str(j['place']) - if j['place'] != '': - dbplace = query("SELECT name FROM places WHERE (campus_room = ?) OR (campus_name = ?) OR ((NOT campus_name) AND name = ?)",j['place'],j['place'],j['place']) - if dbplace: - e['place'] = dbplace[0]['name'] - else: - e['place'] = 'Unbekannter Ort ('+j['place']+')' - else: - e['place'] = '' - e['title'] = i['type'] - events.append(e) - # it is parsed. - - - + if 'www.campus.rwth-aachen.de' in i['url']: + events += fetch_co_course_events(i) + else: + events += fetch_ro_course_events(i) except ImportError: - flash('python-lxml not found, campus import will not work.') + flash('python-lxml or python-pytz not found, campus and ro import will not work!') # events to add newevents = [] diff --git a/livestreams.py b/livestreams.py index fd1a6ea58a5e825d2cbf82c262ddd7f0a8f34f23..ed2b76fe6c509b873280f96b4482eedc61d04c37 100644 --- a/livestreams.py +++ b/livestreams.py @@ -98,7 +98,7 @@ def gentoken(): def streamrekey(id): modify('UPDATE live_sources SET `key` = ? WHERE id = ? AND NOT deleted', gentoken(), id) source = query('SELECT * FROM live_sources WHERE NOT deleted AND id = ?', id)[0] - flash('Der Streamkey von <strong>'+source['name']+'</strong> wurde neu generiert: <span><input readonly type="text" style="width: 15em" value="'+source['key']+'"></span>') + flash('Der Streamkey von <strong>'+source['name']+'</strong> wurde neu generiert: <span><input readonly type="text" style="width: 15em" value="'+source['key']+'"></span><br>Trage diesen Streamkey zusammen mit einem der folgenden Streamingserver in die Streamingsoftware ein:<ul><li>'+config['STREAMING_SERVER']+'</li><li>'+config['BACKUP_STREAMING_SERVER']+'</li></ul>Insgesamt sollte die Streaming-URL z.B. so aussehen: <a href="'+config['STREAMING_SERVER']+source['key']+'">'+config['STREAMING_SERVER']+source['key']+'</a>') return redirect(url_for('streaming')) @app.route('/internal/streaming/drop/<int:id>') diff --git a/templates/course.html b/templates/course.html index d074dce091c081d7eadad0181398963f148d75fc..2bd5d92bda3bba7be108690a0d1f082d43148142 100644 --- a/templates/course.html +++ b/templates/course.html @@ -87,7 +87,7 @@ <h1 class="panel-title">Videos {% if ismod() %} <a class="btn btn-default" style="margin-right: 5px;" href="{{ url_for('create', table='lectures', time=datetime.now(), title='Noch kein Titel', visible='0', course_id=course.id, ref=url_for('course', id=course.id)) }}">Neuer Termin</a> - <a class="btn btn-default" style="margin-right: 5px;" href="{{url_for('list_import_sources', id=course['id'])}}">Campus Import</a> + <a class="btn btn-default" style="margin-right: 5px;" href="{{url_for('list_import_sources', id=course['id'])}}">Import</a> {% endif %} <ul class="list-inline pull-right"> <li> diff --git a/templates/import_campus.html b/templates/import_campus.html index 11e0e87e5ede2c3211c7b084b1baf45121ff4e25..8c5a98d0d9bef1b25d0541e7248c5399a20037d6 100644 --- a/templates/import_campus.html +++ b/templates/import_campus.html @@ -4,11 +4,11 @@ <div class="panel-group"> <div class="panel panel-default"> <div class="panel-heading"> - <h1 class="panel-title">Campus Import für <strong>{{course.title}}</strong> <span><a href="{{url_for('course', handle=course.handle)}}" class="btn btn-default" >Zur Veranstaltungsseite</a><span> </h1> + <h1 class="panel-title">Campus-/RWTHonline-Import für <strong>{{course.title}}</strong> <span><a href="{{url_for('course', handle=course.handle)}}" class="btn btn-default" >Zur Veranstaltungsseite</a><span> </h1> </div> <div class="panel-body"> <div> - <p>Es folgen viele Pärchen an Campus-URL und Veranstaltungstyp Pärchen. Die Campus URL bekommt man aus dem Campus-System (<a href="https://www.campus.rwth-aachen.de/rwth/all/groups.asp" target="_blank">hier</a>). Der Veranstaltungstyp ist z.B. "Vorlesung" oder "Übung" oder "Praktikum". + <p>Es folgen ein oder mehrere Veranstaltungs-URLs mit dem jeweiligen Veranstaltungstyp. Die Veranstaltungs-URL bekommt man aus dem Campus-System (<a href="https://www.campus.rwth-aachen.de/rwth/all/groups.asp" target="_blank">hier</a>) bzw. bei RWTHonline über die Veranstaltungssuche (<a href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche" target="_blank">hier</a> oder über Durchklicken im neuen Interface). Der Veranstaltungstyp ist z.B. "Vorlesung" oder "Übung" oder "Praktikum". </p> <form method="post" action="{{url_for('list_import_sources', id=course['id'])}}"> <ul class="list-group row" style="margin-left: 0px; margin-right: 0px;"> @@ -73,7 +73,7 @@ <span class="col-xs-3"> </span> <span class="pull-right"> - <button class="btn btn-default newlecture" onclick="moderator.api.gethttp('{{ url_for('create', table='lectures', course_id=course.id, time=i.time, title=i.title, place=i.place) }}')">anlegen</a> + <button class="btn btn-default newlecture" onclick="moderator.api.gethttp('{{ url_for('create', table='lectures', course_id=course.id, time=i.time, title=i.title, place=i.place, duration=i.duration) }}')">anlegen</a> </span> </li> {% endfor %} diff --git a/tests/test_misc.py b/tests/test_misc.py index 5effa3045c0f086c9fd3a9639f8a46a59ab12ee7..a32c0d2e274b73245c0d1dab871db4af965d6de6 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -268,6 +268,9 @@ class VideoTestCase(unittest.TestCase): r = self.app.post('/internal/import/257', data={'campus.new.url': 'https://www.campus.rwth-aachen.de/rwth/all/event.asp?gguid=0x4664DBD60E5A02479B53089BF0EB0681&tguid=0x0B473CF286B45B4984CD02565C07D6F8', 'campus.new.type': 'Vorlesung'}) assert r.status_code == 200 + r = self.app.post('/internal/import/257', data={'campus.new.url': 'https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbLv.wbShowLVDetail?pStpSpNr=269474&pSpracheNr=1', 'campus.new.type': 'Übung'}) + assert r.status_code == 200 + r = self.app.get('/internal/import/257/now') assert r.status_code == 200