├── screenshots ├── tiss-enhancement1.png ├── tiss-enhancement2.png └── tuwel-group-member-list.png ├── tuwel-relative-profile-links.user.js ├── README.md ├── tuwel-group-member-list.user.js ├── ufind-extract-lva-daten.user.js ├── vowi-link-tiss-search.user.js ├── tuwien-autologin.user.js ├── peer-tube-download-button.user.js ├── tiss-enhancement.user.js ├── tiss-extract-vowi-templates.user.js └── tiss-ects-chart.user.js /screenshots/tiss-enhancement1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsinf/userscripts/HEAD/screenshots/tiss-enhancement1.png -------------------------------------------------------------------------------- /screenshots/tiss-enhancement2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsinf/userscripts/HEAD/screenshots/tiss-enhancement2.png -------------------------------------------------------------------------------- /screenshots/tuwel-group-member-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsinf/userscripts/HEAD/screenshots/tuwel-group-member-list.png -------------------------------------------------------------------------------- /tuwel-relative-profile-links.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TUWEL: Make profile links relative to course 3 | // @namespace https://fsinf.at/ 4 | // @match https://tuwel.tuwien.ac.at/mod/forum/discuss.php 5 | // @grant none 6 | // @version 1.1 7 | // ==/UserScript== 8 | 9 | var courseId = document.querySelector(".forumsearch form > input").value 10 | var profileLinks = document.querySelectorAll("address a").forEach(function(profileLink) { 11 | profileLink.href += "&course=" + courseId; 12 | }); 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Userscripts 2 | 3 | This repository contains small JavaScripts that enhance TU Wien websites. 4 | 5 | To use one of these scripts you need a userscript manager, we recommend [Violentmonkey](https://violentmonkey.github.io/). 6 | 7 | ## Contributing 8 | 9 | Pull requests and issues are welcome! 10 | 11 | ## Autologin for TISS, TUWEL & OpenCast 12 | 13 | [**Install**](https://fsinf.at/userscripts/tuwien-autologin.user.js) 14 | 15 | ## VoWi & Mattermost links in TISS 16 | 17 | ![Additional links in TISS favorites](screenshots/tiss-enhancement1.png) 18 | 19 | ![Additional links on TISS course page](screenshots/tiss-enhancement2.png) 20 | 21 | [**Install**](https://fsinf.at/userscripts/tiss-enhancement.user.js) 22 | 23 | ## Re-enable download button in TUpeerTube 24 | 25 | ![image](https://user-images.githubusercontent.com/45362676/121822276-8c8ecd00-cc9e-11eb-994b-46674a426b60.png) 26 | 27 | [**Install**](https://fsinf.at/userscripts/peer-tube-download-button.user.js) 28 | -------------------------------------------------------------------------------- /tuwel-group-member-list.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TUWEL: Show group members 3 | // @namespace https://fsinf.at/ 4 | // @downloadURL https://fsinf.at/userscripts/tuwel-group-member-list.user.js 5 | // @updateURL https://fsinf.at/userscripts/tuwel-group-member-list.user.js 6 | // @version 1 7 | // @grant none 8 | // @include https://tuwel.tuwien.ac.at/mod/grouptool/view.php* 9 | // ==/UserScript== 10 | 11 | var textBreadcrumb = document.getElementsByClassName('breadcrumb')[0].innerHTML; 12 | var courseId = textBreadcrumb.match('id\=([0-9]+)')[1] 13 | 14 | var groupContainers = document.body.getElementsByClassName('showmembers'); 15 | 16 | for (var i = 0; i < groupContainers.length; i++) { 17 | var groupContainer = groupContainers[i]; 18 | var showMemberLink = groupContainer.firstElementChild 19 | var groupData = showMemberLink.getAttribute('data-absregs'); 20 | var groupObj = JSON.parse(groupData); 21 | 22 | var listNode = document.createElement('ul'); 23 | for (var y = 0; y < groupObj.length; y++) { 24 | var listItemNode = document.createElement('li'); 25 | var memberObj = groupObj[y]; 26 | const a = document.createElement('a'); 27 | a.href = 'https://tuwel.tuwien.ac.at/user/view.php?course=' + courseId + '&id=' + memberObj.id; 28 | a.target = '_blank'; 29 | a.textContent = memberObj.fullname; 30 | listItemNode.appendChild(a); 31 | listNode.appendChild(listItemNode); 32 | } 33 | 34 | groupContainer.appendChild(listNode); 35 | } 36 | -------------------------------------------------------------------------------- /ufind-extract-lva-daten.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Extract LVA-Daten template for vowi.fsinf.at 3 | // @namespace https://vowi.fsinf.at/ 4 | // @match https://ufind.univie.ac.at/de/course.html 5 | // @match https://ufind.univie.ac.at/en/course.html 6 | // @description Does not work with Greasemonkey because of https://github.com/greasemonkey/greasemonkey/issues/2700 7 | // @grant none 8 | // @version 1.3 9 | // @downloadURL https://fsinf.at/userscripts/ufind-extract-lva-daten.user.js 10 | // @updateURL https://fsinf.at/userscripts/ufind-extract-lva-daten.user.js 11 | // ==/UserScript== 12 | 13 | function vowiLink(ns, id) { 14 | return 'https://vowi.fsinf.at/wiki/Spezial:CourseById?ns=' + ns + '&id=' + id; 15 | } 16 | 17 | document.addEventListener('ufind:finished', function (e) { 18 | var id = document.getElementsByClassName('number')[0].textContent + '/' + document.getElementsByClassName('when')[0].textContent; 19 | var a = document.createElement("a"); 20 | a.href = vowiLink('Uni_Wien', id); 21 | a.innerHTML = 'zum VoWi'; 22 | document.getElementsByClassName('details')[0].insertAdjacentElement('afterend', a); 23 | 24 | var ects = parseFloat(document.getElementsByClassName('ects')[0].textContent); 25 | var lecturers = []; 26 | $('.lecturers a').each(function(){ 27 | lecturers.push('[[ufind.person:'+this.href.split('=')[1] +'|'+this.textContent+']]'); 28 | }); 29 | var block = document.createElement("pre"); 30 | block.textContent = `{{LVA-Daten 31 | | ects = `+ects+`; 32 | | vortragende = `+lecturers.join(', ')+` 33 | | abteilung = 34 | | homepage = 35 | | id = `+id+` 36 | | wann = 37 | | sprache = 38 | | zuordnungen = 39 | 43 | }}`; 44 | a.insertAdjacentElement('afterend', block); 45 | }, false); 46 | -------------------------------------------------------------------------------- /vowi-link-tiss-search.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TISS Search in VoWi 3 | // @namespace https://fsinf.at/ 4 | // @match https://vowi.fsinf.at/wiki/* 5 | // @match https://tiss.tuwien.ac.at/course/courseList.xhtml* 6 | // @description Does not work with Greasemonkey because of https://github.com/greasemonkey/greasemonkey/issues/2700 7 | // @version 1.4 8 | // @downloadURL https://fsinf.at/userscripts/vowi-link-tiss-search.user.js 9 | // @updateURL https://fsinf.at/userscripts/vowi-link-tiss-search.user.js 10 | // ==/UserScript== 11 | 12 | if (location.host == 'vowi.fsinf.at') { 13 | if (document.getElementById('lva-daten') != null) { 14 | var content = document.getElementById('mw-content-text'); 15 | var div = document.createElement('div'); 16 | content.insertBefore(div, content.firstChild); 17 | var a = document.createElement('a'); 18 | div.insertBefore(a, div.firstChild); 19 | a.setAttribute('target', '_blank'); 20 | a.innerHTML = 'TISS Suche'; 21 | var heading = document.getElementById('firstHeading').innerHTML; 22 | var title = encodeURIComponent(heading.substring(heading.indexOf(':') + 1, heading.indexOf('(') - 4)); 23 | var type = encodeURIComponent(heading.substr(heading.indexOf('(') - 3, 2)); 24 | a.href = 'https://tiss.tuwien.ac.at/course/courseList.xhtml?title=' + title + '&type=' + type; 25 | } 26 | } else if (location.host == 'tiss.tuwien.ac.at') { 27 | var params = new URL(location).searchParams; 28 | if (params.get('title')) { 29 | jsf.ajax.addOnEvent(function (data) { 30 | if (data.status == 'success') { 31 | document.getElementById('courseList:courseTitleInp').value = params.get('title'); 32 | document.getElementById('courseList:courseType').value = params.get('type'); 33 | var select = document.getElementById('courseList:semFrom') 34 | select.value = select.children[select.children.length - 1].value; 35 | document.getElementById('courseList:cSearchBtn').click(); 36 | } 37 | }) 38 | document.getElementById('courseList:quickSearchPanel').children[0].lastElementChild.click() 39 | } else { 40 | var titleInput = document.getElementById('courseList:courseTitleInp'); 41 | if (titleInput) { 42 | document.getElementById('courseList:courseLecturer').focus() 43 | window.find(titleInput.value); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tuwien-autologin.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Autologin for TU Wien SSO, TISS, TUWEL and OpenCast 3 | // @namespace https://vowi.fsinf.at/ 4 | // @include https://tiss.tuwien.ac.at/* 5 | // @include https://tuwel.tuwien.ac.at/* 6 | // @include https://oc-presentation.ltcc.tuwien.ac.at/* 7 | // @match https://idp.zid.tuwien.ac.at/simplesaml/module.php/core/loginuserpass.php 8 | // @match https://toss.fsinf.at/ 9 | // @grant none 10 | // @version 1.7 11 | // @downloadURL https://fsinf.at/userscripts/tuwien-autologin.user.js 12 | // @updateURL https://fsinf.at/userscripts/tuwien-autologin.user.js 13 | // ==/UserScript== 14 | 15 | function tuwelRefreshSession() { 16 | setTimeout(function() { 17 | fetch("https://tuwel.tuwien.ac.at/my/", {method: "HEAD"}); 18 | tuwelRefreshSession(); 19 | }, 15*60*1000); 20 | } 21 | 22 | async function openCastAutoLogin(){ 23 | let response = await fetch('/info/me.json'); 24 | if (response.ok){ 25 | let info = await response.json(); 26 | if (info.user.username == 'anonymous'){ 27 | localStorage.returnURL = location.toString(); 28 | window.location = 'https://tuwel.tuwien.ac.at/mod/lti/launch.php?id=385097'; 29 | } 30 | } 31 | } 32 | 33 | switch(location.host){ 34 | case 'idp.zid.tuwien.ac.at': 35 | if (document.querySelector('input[name="password"]').value) 36 | document.querySelector('input[name="password"]').form.submit() 37 | break; 38 | 39 | case 'tiss.tuwien.ac.at': 40 | if (document.getElementsByClassName("loading").length > 0) { 41 | // Don't run the script on sites which only contain the loading animation. 42 | return; 43 | } 44 | 45 | login = document.querySelector(".toolLogin"); 46 | if (login != null) { 47 | login.click(); 48 | } 49 | break; 50 | 51 | case 'tuwel.tuwien.ac.at': 52 | if (location.pathname == "/theme/university_boost/login/index.php") { 53 | document.querySelector("a[title='TU Wien Login']").click(); 54 | } else { 55 | tuwelRefreshSession(); 56 | } 57 | break; 58 | 59 | case 'oc-presentation.ltcc.tuwien.ac.at': 60 | if (location.search == '?epFrom=d264f820-6d51-4cb1-a4f2-bb74e2094149&e=1&p=1' && localStorage.returnURL){ 61 | let returnURL = localStorage.returnURL; 62 | localStorage.removeItem('returnURL'); 63 | window.location = returnURL; 64 | } else { 65 | openCastAutoLogin(); 66 | } 67 | break; 68 | 69 | case 'toss.fsinf.at': 70 | hasTES = true; 71 | break; 72 | } 73 | -------------------------------------------------------------------------------- /peer-tube-download-button.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Re-enable TUpeerTube download button 3 | // @namespace https://vowi.fsinf.at/ 4 | // @version 1.0 5 | // @description Re-enables the download button for TUpeerTube videos, for videos where they were disabled. 6 | // @author Fabian Scherer 7 | // @match https://tube1.it.tuwien.ac.at/videos/watch/* 8 | // @icon https://tube1.it.tuwien.ac.at/client/assets/images/favicon.png 9 | // @grant GM_openInTab 10 | // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js 11 | // @downloadURL https://fsinf.at/userscripts/peer-tube-download-button.user.js 12 | // @updateURL https://fsinf.at/userscripts/peer-tube-download-button.user.js 13 | // ==/UserScript== 14 | 15 | 16 | 17 | 18 | 19 | //==================================================== 20 | // MIT Licensed 21 | // Author: jwilson8767 22 | 23 | /** 24 | * Waits for an element satisfying selector to exist, then resolves promise with the element. 25 | * Useful for resolving race conditions. 26 | * 27 | * @param selector 28 | * @returns {Promise} 29 | */ 30 | function elementReady(selector) { 31 | return new Promise((resolve, reject) => { 32 | const el = document.querySelector(selector); 33 | if (el) {resolve(el);} 34 | new MutationObserver((mutationRecords, observer) => { 35 | // Query for elements matching the specified selector 36 | Array.from(document.querySelectorAll(selector)).forEach((element) => { 37 | resolve(element); 38 | //Once we have resolved we don't need the observer anymore. 39 | observer.disconnect(); 40 | }); 41 | }) 42 | .observe(document.documentElement, { 43 | childList: true, 44 | subtree: true 45 | }); 46 | }); 47 | } 48 | //===================================================== 49 | 50 | function fs_download_url(id){return "https://tube1.it.tuwien.ac.at/download/videos/" + id + "-720.mp4"} 51 | 52 | function fs_do_the_download(){ 53 | var id = window.location.href.split("/").pop() 54 | var download_url = fs_download_url(id) 55 | GM_openInTab(download_url) 56 | } 57 | 58 | (function() { 59 | 'use strict'; 60 | 61 | $(function() { 62 | elementReady(".video-actions").then(() => { 63 | $(".video-actions").append('
*download*
'); 64 | $("#fs_download").click(fs_do_the_download); 65 | }) 66 | }); 67 | })(); 68 | 69 | -------------------------------------------------------------------------------- /tiss-enhancement.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name VoWi and Mattermost links in TISS 3 | // @description Add links to VoWi pages and Mattermost channels to TISS courses. 4 | // @namespace https://fsinf.at/ 5 | // @match https://tiss.tuwien.ac.at/course/educationDetails.xhtml* 6 | // @match https://tiss.tuwien.ac.at/course/courseDetails.xhtml* 7 | // @match https://tiss.tuwien.ac.at/education/favorites.xhtml* 8 | // @grant none 9 | // @version 1.13 10 | // @downloadURL https://fsinf.at/userscripts/tiss-enhancement.user.js 11 | // @updateURL https://fsinf.at/userscripts/tiss-enhancement.user.js 12 | // ==/UserScript== 13 | 14 | // Inspired by https://greasyfork.org/de/scripts/9914-tiss-enhancer/ 15 | 16 | if (document.getElementsByClassName("loading").length > 0) { 17 | // Don't run the script on sites which only contain the loading animation. 18 | return; 19 | } 20 | 21 | function vowi_link(tissID) { 22 | return "https://vowi.fsinf.at/wiki/Spezial:CourseById?ns=TU_Wien&id=" + tissID; 23 | } 24 | 25 | function mm_link(lvaTitle) { 26 | var channame = lvaTitle.toLowerCase().replace('ä','ae').replace('ö','oe').replace('ü','ue'); 27 | channame = channame.replace(/[^a-zA-Z0-9_]/g,'-'); 28 | channame = channame.replace(/-+/g,'-'); 29 | channame = channame.substring(0,63); 30 | channame = channame.replace(/-$/, ''); 31 | 32 | return "https://mattermost.fsinf.at/w-inf-tuwien/channels/" + encodeURIComponent(channame); 33 | } 34 | 35 | var page = window.location.href.match(/tiss.tuwien.ac.at\/([\w\/]+)\.xhtml/i)[1]; 36 | var locale = document.cookie.match(/TISS_LANG=([\w-]+)/); 37 | locale = locale ? locale[1] : "de"; 38 | 39 | 40 | // course overview: add VoWi link 41 | if (page == "course/educationDetails" || page == "course/courseDetails") { 42 | var header = document.getElementById("subHeader").innerText; 43 | 44 | var heading = document.getElementById("contentInner").getElementsByTagName("h1")[0].innerText; 45 | var lvaTitle = /^\s*[A-Z0-9\.]{7} (.*)$/gm.exec(heading)[1]; 46 | var tissID = /^\s*([A-Z0-9.]{7})\s+(.*)$/gm.exec(heading)[1].replace(".", ""); 47 | 48 | var ul = document.getElementById("contentInner").getElementsByClassName("bulletList")[0]; 49 | var li = document.createElement("li"); 50 | li.innerHTML = '' + (locale == "de" ? "Zum" : "To") + ' VoWi'; 51 | ul.appendChild(li); 52 | 53 | li = document.createElement("li"); 54 | li.innerHTML = '' + (locale == "de" ? "Zum" : "To") + ' Mattermost-Channel'; 55 | ul.appendChild(li); 56 | } 57 | 58 | // favorites page: add VoWi link icon 59 | if (page == "education/favorites") { 60 | 61 | var dlTemplate = document.createElement("img"); 62 | dlTemplate.src = ` 63 | data:image/png;base64, 64 | iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI 65 | WXMAAC4jAAAuIwF4pT92AAABXUlEQVQ4y+2Tv0tCURzFP+/1VEoypCHCSAeJECGMpppaJHFpF6Kx 66 | SWgQ+gcCh4aamiUborEIJCKMIIrAUpFCLLUfDipiIijos0HUTIJKh4bOdO/l8Ll8z7lXwHlQowcS 67 | 6ZH+HkhqLFwmLSpRIJgqYhxWcZosECi1x+cyaVGKMuvhfAdIaISddVvRKCT2jiM4bGYq1QpX4Tgr 68 | u3eEynVz1m1lUFFGuervmEX6uKlRwhdKU6mFmbOMMTtlxKMSmd6O/DSjKjtPRZZ9ceY3L8gUy1gm 69 | 9SzphN+H/VyqEnvJAX3MGIa6a02W62GLotgdSDeiASCayn+//laPAouj/TjtE4xr1SRTKbaiVRDa 70 | rzdI9YOCXCMrfwIJqPHa9DhsZgBe01nWPLdtEAE13gVD03N0HsS+n2yB/NcJBiSRUCLP4dk9wccM 71 | Gzc5cnIL0vQk85xcPgAQiL21P8j/3/+l3gFrvHeTUEQuDAAAAABJRU5ErkJggg== 72 | `; 73 | dlTemplate.alt = "Distance Learning"; 74 | dlTemplate.style = "vertical-align: bottom"; 75 | 76 | Array.from(document.querySelectorAll("tr.ui-widget-content")).forEach(function(row, index) { 77 | var titleCol = row.getElementsByClassName("favoritesTitleCol")[0]; 78 | var lvaTitle = titleCol.getElementsByTagName("a")[0].text.trim(); 79 | var tissID = titleCol.querySelector("span[title='LVA Nr.'],span[title='Course Nr.']").textContent.replace(".", ""); 80 | 81 | var a = document.createElement("a"); 82 | a.href = mm_link(lvaTitle); 83 | a.target = "_blank"; 84 | 85 | var img = document.createElement("img"); 86 | img.src = "https://mattermost.fsinf.at/static/images/favicon/favicon-32x32.png"; 87 | img.title = "Mattermost"; 88 | img.alt = "Mattermost"; 89 | img.width = 16; 90 | img.height = 16; 91 | 92 | a.appendChild(img); 93 | 94 | var favoritesLinksPanel = row.getElementsByClassName("favoritesLinksPanel")[0]; 95 | favoritesLinksPanel.insertBefore(a, favoritesLinksPanel.childNodes[0]); 96 | 97 | a = document.createElement("a"); 98 | a.href = vowi_link(tissID); 99 | a.target = "_blank"; 100 | 101 | img = document.createElement("img"); 102 | img.src = "https://vowi.fsinf.at/favicon.ico"; 103 | img.title = "VoWi"; 104 | img.alt = "VoWi"; 105 | img.width = 16; 106 | img.height = 16; 107 | 108 | a.appendChild(img); 109 | 110 | favoritesLinksPanel = row.getElementsByClassName("favoritesLinksPanel")[0]; 111 | favoritesLinksPanel.insertBefore(a, favoritesLinksPanel.childNodes[0]); 112 | 113 | var dl = row.querySelector("img[alt='Distance Learning']"); 114 | if (dl !== null) { 115 | dl.replaceWith(dlTemplate.cloneNode(false)); 116 | } 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /tiss-extract-vowi-templates.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TISS: Extract VoWi templates 3 | // @namespace https://fsinf.at/ 4 | // @match https://tiss.tuwien.ac.at/course/educationDetails.xhtml 5 | // @match https://tiss.tuwien.ac.at/course/courseDetails.xhtml 6 | // @match https://tiss.tuwien.ac.at/curriculum/public/curriculum.xhtml 7 | // @grant none 8 | // @version 2.2.4 9 | // @downloadURL https://fsinf.at/userscripts/tiss-extract-vowi-templates.user.js 10 | // @updateURL https://fsinf.at/userscripts/tiss-extract-vowi-templates.user.js 11 | // ==/UserScript== 12 | 13 | if (document.getElementsByClassName("loading").length > 0) { 14 | // Don't run the script on sites which only contain the loading animation. 15 | return; 16 | } 17 | 18 | function extractCurriculumData(){ 19 | let data = {}; 20 | let matches = /(\d{3} \d{3})?(.+)/.exec($("#contentInner h1").text()); 21 | if (matches[1]) 22 | data.id = 'E' + matches[1].replace(' ', ''); 23 | let titleWords = matches[2].trim().split(' '); 24 | if (titleWords[0].endsWith('studium')){ 25 | data.typ = titleWords.shift().replace('sstudium', '').replace('studium', ''); 26 | data.name = titleWords.join(' '); 27 | } else { 28 | data.typ = 'Katalog' 29 | data.name = matches[2].trim(); 30 | } 31 | 32 | data.tiss = new URL(location).searchParams.get('key'); 33 | var legalLink = $('a[id$=legalTextLink]'); 34 | if (legalLink.length > 0) 35 | data.plan = legalLink[0].href; 36 | return data; 37 | } 38 | 39 | if (location.pathname.startsWith('/curriculum/public/')){ 40 | $('#subHeader').append($('').click(function(){ 41 | let data = extractCurriculumData(); 42 | $(this).replaceWith($('
').html(`
 43 | {{Katalog-Daten
 44 | |typ=${data.typ}
 45 | |name=${data.name}${data.plan ? '\n|plan=' + data.plan : ''}
 46 | |tiss=${data.tiss}
 47 | }}`))
 48 |   }));
 49 | }
 50 | 
 51 | STUDIENKENNZAHL_BLACKLIST = [
 52 |   "E066011", // Erasmus
 53 |   "E066950", // Informatikdidaktik
 54 |   "E033531", // Data Engineering & Statistics
 55 |   "E066933", // Information & Knowledge Management
 56 |   "860GW",   // Gebundene Wahlfächer - Technische Mathematik
 57 |   "884UF",   // Informatik und Informatikmanagement
 58 |   "175FW",   // Freie Wahlfächer - Wirtschaftsinformatik
 59 |   "880FW",   // Freie Wahlfächer - Informatik
 60 | ];
 61 | 
 62 | function getLvaTitel() {
 63 |   let title = $("#contentInner h1").text();
 64 |   let matches = /^\s*([A-Z0-9.]{7})\s+(.*)$/gm.exec(title);
 65 |   return matches[2];
 66 | }
 67 | 
 68 | function getTissID() {
 69 |   let title = $("#contentInner h1").text();
 70 |   let matches = /^\s*([A-Z0-9.]{7})\s+(.*)$/gm.exec(title);
 71 |   return matches[1];
 72 | }
 73 | 
 74 | function getECTS() {
 75 |   let matches = /ECTS:\s([\d.]+)/.exec($("h2:contains('Merkmale') + ul > li").text());
 76 |   return matches[1].replace(".", ",").replace(",0", "");
 77 | }
 78 | 
 79 | function getLvaTyp() {
 80 |   let matches = /Typ:\s([A-Z]+)/.exec($("h2:contains('Merkmale') + ul > li").text());
 81 |   return matches[1];
 82 | }
 83 | 
 84 | function getVortragende() {
 85 |   let vortragende = [];
 86 |   $.each($("h2:contains('Vortragende') + ul a"), (key, link) => {
 87 |     let vortragender = link.innerText.split(", ");
 88 |     vortragender.reverse();
 89 |     vortragender = vortragender.join(" ");
 90 |     let personId = /(\d+)/.exec(link.href)[1];
 91 |     vortragende.push({
 92 |       wikiLink: `[[tiss.person:${personId}|${vortragender}]]`,
 93 |       name: vortragender,
 94 |       id: personId,
 95 |     });
 96 |   });
 97 |   return vortragende;
 98 | }
 99 | 
100 | async function getLeiter(vortragende) {
101 |   let leiter = [];
102 |   let fallbackLeiter;
103 |   let promises = vortragende.map((vortragender) => {
104 |     return $.ajax({
105 |       url: `https://tiss.tuwien.ac.at/adressbuch/adressbuch/person/${vortragender["id"]}`,
106 |       dataType: "html",
107 |     });
108 |   });
109 |   let adressbuchEntries = await Promise.all(promises);
110 |   $.each(adressbuchEntries, (key, value) => {
111 |     let titel = $(".vorangestellte-titel", value).text();
112 |     let nachname = $(".nachname", value).text();
113 |     if (titel.match(/Prof/) || titel.match(/PD/)) {
114 |       leiter.push(nachname);
115 |     } else if (key == 0) {
116 |       fallbackLeiter = nachname;
117 |     }
118 |   });
119 |   if (leiter.length == 0) {
120 |     leiter.push(fallbackLeiter)
121 |   }
122 |   return leiter;
123 | }
124 | 
125 | function getAbteilung() {
126 |   return $("h2:contains('Institut') + ul > li").text().replace(/E\d+/, "").replace(/Institut für\s*/, "").trim();
127 | }
128 | 
129 | function getSprache() {
130 |   let sprache = $("h2:contains('Sprache')")[0].nextSibling.textContent;
131 |   if (sprache.match(/Bei Bedarf/)) {
132 |     return null;
133 |   } else if (sprache == "Deutsch") {
134 |     return "de";
135 |   } else if (sprache == "Englisch") {
136 |     return "en";
137 |   }
138 | }
139 | 
140 | function getSemester() {
141 |   let semester = "";
142 | 
143 |   let semesters = $("#semesterForm option");
144 |   if (semesters.length > 0) {
145 |     // educationDetails
146 |     let currentSemester = semesters[0].innerText;
147 |     if (semesters.length > 1) {
148 |       // course has been offered in more than one Semester already
149 |       let lastSemester = $("#semesterForm option")[1].innerText;
150 |       if (currentSemester[currentSemester.length - 1] == lastSemester[lastSemester.length - 1]) {
151 |         semester = currentSemester[currentSemester.length - 1] + "S";
152 |       } else {
153 |         semester = "beide";
154 |       }
155 |     } else {
156 |       // first time the course is held
157 |       semester = currentSemester[currentSemester.length - 1] + "S";
158 |     }
159 |   } else {
160 |     // courseDetails
161 |     semester = "";
162 |   }
163 | 
164 |   return semester;
165 | }
166 | 
167 | function getHomepage() {
168 |   return $("h2:contains('Weitere Informationen') + ul > li:contains('Homepage') a").attr("href");
169 | }
170 | 
171 | function getWindowIdRequestTokenUrl(url) {
172 |   // Thanks to Gittenburg for the hints.
173 |   let windowId = dswh.utils.getWindowIdFromWindowName();
174 |   let requestToken = dswh.utils.generateNewRequestToken();
175 |   url = dswh.utils.setUrlParam(url, "dsrid", requestToken);
176 |   url = dswh.utils.setUrlParam(url, "dswid", windowId);
177 |   dswh.utils.storeCookie("dsrwid-" + requestToken, windowId, 3);
178 |   return url;
179 | }
180 | 
181 | function getModulNameFromTissID(data, tissID) {
182 |   return $(`.courseKey:contains('${tissID}')`, data).parents("tr").prevAll("tr:has(.nodeTable-level-2)").first().find(".bold").text().replace(/(Wahl|Pflicht)?[Mm]odul /, "");
183 | }
184 | 
185 | async function getModule(tissID) {
186 |   let promises = [];
187 | 
188 |   $.each($("h2:contains('Curricula') + .ui-datatable tr.ui-widget-content"), (key, tr) => {
189 |     let columns = $.find("td", tr);
190 |     if (columns.length <= 3) {
191 |       return;
192 |     }
193 |     let studienplanUrl = $(columns[0]).find("a").attr("href");
194 |     let matches = /\s*([A-Z0-9 ]{3,7})\s+(.*)/.exec(columns[0].textContent);
195 |     let studienkennzahl = matches[1].replace(" ", "");
196 |     if (studienkennzahl[0] == "0") {
197 |       studienkennzahl = "E" + studienkennzahl;
198 |     }
199 |     if ($.inArray(studienkennzahl, STUDIENKENNZAHL_BLACKLIST) != -1 || /^\d{3}$/.exec(studienkennzahl)) {
200 |       return;
201 |     }
202 |     let studium = matches[2].trim();
203 |     let semester = columns[1].textContent.trim();
204 | 
205 |     promises.push(
206 |       $.ajax({
207 |         url: getWindowIdRequestTokenUrl(studienplanUrl),
208 |         dataType: "html",
209 |       }).then((data) => {
210 |         return {
211 |           studienkennzahl: studienkennzahl,
212 |           wahl: semester == "",
213 |           name: getModulNameFromTissID(data, tissID),
214 |         };
215 |       })
216 |     );
217 |   });
218 | 
219 |   return Promise.all(promises);
220 | }
221 | 
222 | function filterModule(module) {
223 |   let trs = module.find((modul) => modul["studienkennzahl"] == "TRS") != null;
224 |   if (!trs) {
225 |     return module;
226 |   } else {
227 |     return module.filter((modul) => !(modul["name"].includes("Transferable Skills") || modul["name"].includes("Fachübergreifende Qualifikation")));
228 |   }
229 | }
230 | 
231 | async function showLvaDaten() {
232 |   $("#vowi-lva-daten").html(`
233 |     
234 |     
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |
243 | `); 244 | 245 | let lvaTitel = getLvaTitel(); 246 | let tissID = getTissID(); 247 | let ects = getECTS(); 248 | let lvaTyp = getLvaTyp(); 249 | let vortragende = getVortragende(); 250 | let leiter = await getLeiter(vortragende); 251 | let abteilung = getAbteilung(); 252 | let sprache = getSprache(); 253 | let semester = getSemester(); 254 | let homepage = getHomepage(); 255 | let module = filterModule(await getModule(tissID)); 256 | 257 | // Build final template 258 | let lvaDaten = ` 259 | {{LVA-Daten 260 | | ects = ${ects} 261 | | vortragende = ${vortragende.map((vortragender) => vortragender["wikiLink"]).join(", ")} 262 | | abteilung = ${abteilung}${homepage != undefined ? "\n| homepage = " + homepage : ""} 263 | | id = ${tissID.replace(".", "")} 264 | | wann = ${semester}${sprache != null ? "\n| sprache = " + sprache : ""} 265 | | zuordnungen = 266 | ${module.map((modul) => `{{Zuordnung|${modul["studienkennzahl"]}|${modul["name"]}${modul["wahl"] ? "|wahl=1" : ""}}}`).join("\n ")} 267 | }} 268 | `; 269 | $("#vowi-lva-daten").html(` 270 | 274 |
${lvaDaten}
275 | `); 276 | } 277 | 278 | $("#contentInner > form").append(` 279 |

VoWi LVA-Daten

280 |
281 | `); 282 | $("#vowi-lva-daten-btn").on("click", (event) => { 283 | event.preventDefault(); 284 | showLvaDaten(); 285 | }); 286 | -------------------------------------------------------------------------------- /tiss-ects-chart.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TISS ECTS Charts 3 | // @namespace https://fsinf.at/ 4 | // @require https://d3js.org/d3.v6.js 5 | // @require https://cdn.jsdelivr.net/npm/chart.js@2.8.0 6 | // @description add charts to TISS certificates 7 | // @include https://tiss.tuwien.ac.at/graduation/certificates.xhtml* 8 | // @version 1.3 9 | // @downloadURL https://fsinf.at/userscripts/tiss-ects-chart.user.js 10 | // @updateURL https://fsinf.at/userscripts/tiss-ects-chart.user.js 11 | // ==/UserScript== 12 | 13 | async function main() { 14 | // ============================================ 15 | // Custom way to filter out a specific semester 16 | var filterOutSemester = false; 17 | var semesterToFilterOut = "2020SS"; 18 | // ============================================ 19 | 20 | console.log("starting TISS ECTS Charts script"); 21 | const table = await waitForTable(); 22 | const data = fillData(table, filterOutSemester, semesterToFilterOut); // 23 | const studycodes = getStudyCodes(table); 24 | const ects_data = extractEctsData(data); 25 | style(); 26 | 27 | const querySelector = document.querySelector("#certificateList\\:studentInfoPanel"); 28 | 29 | let selectcontainerdiv = document.createElement("div"); 30 | 31 | let selectcontainer = document.createElement("select"); 32 | 33 | selectcontainerdiv = querySelector.appendChild(selectcontainerdiv); 34 | 35 | selectcontainer = selectcontainerdiv.appendChild(selectcontainer); 36 | 37 | selectcontainer.id = "studycodeselect"; 38 | 39 | selectcontainer.addEventListener("change", onSelectedStudyCode); 40 | 41 | studycodes.forEach(studycode => { 42 | const optioncontainer = document.createElement("option"); 43 | const option = selectcontainer.appendChild(optioncontainer); 44 | option.value = studycode; 45 | option.text = studycode; 46 | if(studycode == getCurrentStudy()){ 47 | option.selected = true; 48 | } 49 | }); 50 | 51 | querySelector.appendChild(canvas_container); 52 | const ects_line_chart = makeEctsLineChart(ects_data); 53 | const grade_line_chart = makeGradeLineChart(ects_data); 54 | const ects_cumsum_chart = makeEctsCumsumChart(ects_data); 55 | 56 | console.log("finished TISS ECTS Charts script"); 57 | } 58 | 59 | function onSelectedStudyCode(event){ 60 | localStorage.setItem("studypreference", event.target.value); 61 | window.location.reload(); 62 | } 63 | 64 | function waitForTable() { 65 | return new Promise(resolve => { 66 | const interval = setInterval(() => { 67 | const table = document.querySelector("form #certificateList\\:certificatesPanel table"); 68 | if (table) { 69 | console.log("found table"); 70 | clearInterval(interval); 71 | resolve(table); 72 | } 73 | }, 100); 74 | }); 75 | } 76 | 77 | function mapGrade(grade) { 78 | var ans; 79 | switch(grade) { 80 | case "nicht genügend": 81 | case "unsatisfactory": 82 | case "insufficient": 83 | case "ohne Erfolg teilgenommen": 84 | case "unsuccessfully completed": 85 | ans = 5; 86 | break; 87 | case "genügend": 88 | case "sufficient": 89 | ans = 4; 90 | break; 91 | case "befriedigend": 92 | case "satisfactory": 93 | ans = 3; 94 | break; 95 | case "gut": 96 | case "good": 97 | ans = 2; 98 | break; 99 | case "sehr gut": 100 | case "excellent": 101 | ans = 1; 102 | break; 103 | default: 104 | ans = 0; 105 | } 106 | return ans; 107 | } 108 | 109 | function fillData(table, customFilter, semesterToFilter) { 110 | if (!table) { 111 | throw new Error("Table is undefined - cannot fill data"); 112 | } 113 | var data = []; 114 | var currentstudycode = getCurrentStudy(); 115 | for ( var i = 1; i < table.rows.length; i++ ) { 116 | if (i==1 && currentstudycode == null) { 117 | localStorage.setItem("studypreference", table.rows[i].cells[6].innerText); 118 | currentstudycode = table.rows[i].cells[6].innerText; 119 | } 120 | if (table.rows[i].cells[4].innerText == "" 121 | || table.rows[i].cells[6].innerText != currentstudycode) continue; 122 | data.push({ 123 | 'hours': table.rows[i].cells[3].innerText, 124 | 'ects': parseFloat(table.rows[i].cells[4].innerText), 125 | 'date': table.rows[i].cells[5].innerText, 126 | 'study': table.rows[i].cells[6].innerText, 127 | 'grade': mapGrade(table.rows[i].cells[7].innerText), 128 | 'term': findTerm(table.rows[i].cells[5].innerText) 129 | }); 130 | } 131 | if (customFilter) { 132 | console.log("filtering out: " + semesterToFilter); 133 | data = customDataFilter(data, semesterToFilter); 134 | } 135 | console.log("filled data table") 136 | return data; 137 | } 138 | 139 | function getCurrentStudy(){ 140 | return localStorage.getItem("studypreference") 141 | } 142 | 143 | function getStudyCodes(table){ 144 | var studycodes = new Set(); 145 | for ( var i = 1; i < table.rows.length; i++ ) { 146 | studycodes.add(table.rows[i].cells[6].innerText); 147 | } 148 | return studycodes; 149 | } 150 | 151 | function findTerm(date) { 152 | let splitted = date.split('.'); 153 | let year = parseInt(splitted[2], 10); 154 | let month = parseInt(splitted[1], 10); 155 | let term = (month >= 5 && month <= 11) ? "SS" : "WS"; 156 | year = (month <= 4) ? year-1 : year; 157 | //return term.concat(year); 158 | return (year.toString()).concat(term); 159 | } 160 | 161 | function extractEctsData(data) { //returns ects_data 162 | var data_passed = data.filter(function(d) { return d.grade != 5; }) 163 | var data_weighted = data.filter(function(d) { return d.grade != 0 && d.grade != 5; }) 164 | 165 | var ects_tried = d3.rollup(data, v => d3.sum(v, d => d.ects), d => d.term) 166 | var ects_passed = d3.rollup(data_passed, v => d3.sum(v, d => d.ects), d => d.term) 167 | ects_tried = [...ects_tried.entries()].sort() 168 | 169 | var ects_weighted = d3.rollup(data_weighted, v => d3.sum(v, d => (d.ects * d.grade)), d => d.term) 170 | var ects_graded = d3.rollup(data_weighted, v => d3.sum(v, d => (d.ects)), d => d.term) 171 | 172 | var ects_data = [] 173 | var pass_sum = 0; 174 | var tried_sum = 0; 175 | var weighted_sum = 0; 176 | var graded_sum = 0; 177 | for ( var i = 0; i < ects_tried.length; i++ ) { 178 | let term = ects_tried[i][0]; 179 | let tried = ects_tried[i][1]; 180 | let pass = ects_passed.get(term) || 0; 181 | let weighted = ects_weighted.get(term) || 0; 182 | let graded = ects_graded.get(term) || 0; 183 | 184 | pass_sum += pass; 185 | tried_sum += tried; 186 | weighted_sum += weighted; 187 | graded_sum += graded; 188 | 189 | let grade_avg = (graded !== 0) ? (weighted / graded).toFixed(2) : null; 190 | let grade_mavg = (graded_sum !== 0) ? (weighted_sum / graded_sum).toFixed(2) : null; 191 | 192 | 193 | ects_data.push({ 194 | term, 195 | tried, 196 | passed: pass, 197 | avg: (pass_sum / (i+1)).toFixed(2), 198 | passed_sum: pass_sum, 199 | tried_sum: tried_sum, 200 | grade_avg, 201 | grade_mavg 202 | }); 203 | } 204 | console.log("extracted ects data"); 205 | return ects_data; 206 | } 207 | 208 | function style() { 209 | const style = document.createElement("style"); 210 | style.textContent = '@media (max-width: 800px) {.cool-chart {width: 100% !important;}}'; 211 | document.body.appendChild(style); 212 | console.log("styled"); 213 | } 214 | 215 | const canvas_container = document.createElement("div"); 216 | 217 | function newChartContext(){ 218 | var canvas = document.createElement("canvas"); 219 | var canvas_wrapper = document.createElement('div'); 220 | canvas_wrapper.className = 'cool-chart'; 221 | canvas_wrapper.style.width = '33%'; 222 | canvas_wrapper.style.minHeight = '200px'; 223 | canvas_wrapper.style.display = 'inline-block'; 224 | canvas_wrapper.appendChild(canvas); 225 | canvas_container.appendChild(canvas_wrapper); 226 | console.log("created new chart context"); 227 | return canvas.getContext("2d"); 228 | } 229 | 230 | function makeEctsLineChart(ects_data) { //returns ects_line_chart 231 | var ects_line_chart = new Chart(newChartContext(), { 232 | type: 'line', 233 | 234 | data: { 235 | labels: ects_data.map(function(d) { return d.term }), 236 | datasets: [{ 237 | label: 'Passed', 238 | fill: false, 239 | cubicInterpolationMode: 'monotone', 240 | borderColor: 'rgb(0, 204, 102)', 241 | data: ects_data.map(function(d) { return d.passed }) 242 | }, { 243 | label: 'Tried', 244 | fill: false, 245 | cubicInterpolationMode: 'monotone', 246 | borderColor: 'rgb(255, 99, 132)', 247 | data: ects_data.map(function(d) { return d.tried }) 248 | }, { 249 | label: 'AVG', 250 | fill: false, 251 | borderColor: 'rgb(0, 102, 255)', 252 | data: ects_data.map(function(d) { return d.avg }) 253 | }] 254 | }, 255 | 256 | options: { 257 | title: { 258 | display: true, 259 | text: 'ECTS over time' 260 | }, 261 | maintainAspectRatio: false, 262 | scales: { 263 | yAxes: [{ 264 | display: true, 265 | ticks: { 266 | suggestedMin: 0, 267 | suggestedMax: 30 268 | } 269 | }] 270 | } 271 | } 272 | }); 273 | console.log("made ects line chart"); 274 | return ects_line_chart; 275 | } 276 | 277 | function makeGradeLineChart(ects_data) { //returns grade_line_chart 278 | var grade_line_chart = new Chart(newChartContext(), { 279 | type: 'line', 280 | 281 | data: { 282 | labels: ects_data.map(function(d) { return d.term }), 283 | datasets: [{ 284 | label: 'Weighted GPA by ECTS / Term', 285 | fill: false, 286 | cubicInterpolationMode: 'monotone', 287 | borderColor: 'rgb(255, 102, 255)', 288 | data: ects_data.map(function(d) { return d.grade_avg }) 289 | }, { 290 | label: 'Weighted GPA by ECTS / Total', 291 | fill: false, 292 | borderColor: 'rgb(0,0,0)', 293 | data: ects_data.map(function(d) { return d.grade_mavg }) 294 | }] 295 | }, 296 | 297 | options: { 298 | title: { 299 | display: true, 300 | text: 'GPA over time (passed courses)' 301 | }, 302 | maintainAspectRatio: false, 303 | scales: { 304 | yAxes: [{ 305 | display: true, 306 | ticks: { 307 | suggestedMin: 1, 308 | suggestedMax: 5, 309 | reverse: true 310 | } 311 | }] 312 | } 313 | } 314 | }); 315 | console.log("made grade line chart"); 316 | return grade_line_chart; 317 | } 318 | 319 | function makeEctsCumsumChart(ects_data) { //returns ects_cumsum_chart 320 | var ects_cumsum_chart = new Chart(newChartContext(), { 321 | type: 'bar', 322 | 323 | data: { 324 | labels: ects_data.map(function(d) { return d.term }), 325 | datasets: [{ 326 | label: 'Passed', 327 | backgroundColor: 'rgb(0, 204, 102)', 328 | data: ects_data.map(function(d) { return d.passed_sum }) 329 | }, { 330 | label: 'Tried', 331 | backgroundColor: 'rgb(255, 99, 132)', 332 | data: ects_data.map(function(d) { return d.tried_sum }) 333 | }] 334 | }, 335 | 336 | options: { 337 | maintainAspectRatio: false, 338 | title: { 339 | display: true, 340 | text: 'Cummulative ECTS' 341 | } 342 | } 343 | }); 344 | console.log("made ects cumsum chart"); 345 | return ects_cumsum_chart; 346 | } 347 | 348 | function waitForElement(selector) { 349 | return new Promise(resolve => { 350 | const interval = setInterval(() => { 351 | const el = document.querySelector(selector); 352 | if (el) { 353 | clearInterval(interval); 354 | resolve(el); 355 | } 356 | }, 100); 357 | }); 358 | } 359 | 360 | function customDataFilter(data, semester) { 361 | //Custom filter for not including a semester 362 | data = data.filter(d => d.term !== semester); 363 | return data; 364 | } 365 | 366 | main(); 367 | --------------------------------------------------------------------------------