From eed340615bcffe6296b3ee17fe72679261d4121f Mon Sep 17 00:00:00 2001 From: Devoalda Date: Thu, 4 Jan 2024 08:40:37 +0800 Subject: [PATCH] Initial Commit --- .gitignore | 164 +++++++++++++++++++ .idea/.gitignore | 8 + .idea/inspectionProfiles/Project_Default.xml | 49 ++++++ .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + LICENSE | 21 +++ README.MD | 39 +++++ cal.py | 66 ++++++++ 9 files changed, 367 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 LICENSE create mode 100644 README.MD create mode 100644 cal.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0068a9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +bak/ +venv/ + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..83cd2b2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,49 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8ead197 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3ce545 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Woon Jun Wei + +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/README.MD b/README.MD new file mode 100644 index 0000000..2ee56de --- /dev/null +++ b/README.MD @@ -0,0 +1,39 @@ +# Timetable to ICS + +This is a simple script to convert the timetable from SIT's website into an ICS file. + +## Usage + +Clone the repository and install the dependencies + +```bash +git clone +cd SIT_Timetable +pip install -r requirements.txt +``` + +Go to in4sit, login and go to the timetable page. + +Head to `Course Management` > `My Weekly Schedule` > `List View` + +Inspect and find the table element: + +```html + +``` + +Copy the table from `` to `
` and paste it into a file called `timetable.html` in the root of the repository and run the script. + +```bash +python cal.py +``` + +An ICS file will be generated in the root of the repository. + +## License + +MIT + +## Disclaimer + +This is a personal project and is not affiliated with SIT in any way. I am not responsible for any damages caused by the use of this script. diff --git a/cal.py b/cal.py new file mode 100644 index 0000000..92dbdfb --- /dev/null +++ b/cal.py @@ -0,0 +1,66 @@ +import ics +import datetime +from bs4 import BeautifulSoup + +# Load table.html into BeautifulSoup +soup = BeautifulSoup(open("table.html"), "html.parser") + +# Get number of tables with class PSGROUPBOXWBO +course_tables = soup.find_all("table", class_="PSGROUPBOXWBO") + +timetable_dict = {} + +timezone = datetime.timezone(datetime.timedelta(hours=8)) + +for i, table in enumerate(course_tables): + # Print out table header + course = table.find("td", class_="PAGROUPDIVIDER").text + if course not in timetable_dict: + timetable_dict[course] = [] + + # Find tables in table with id ACE_DERIVED_REGFRM1_DESCR20${0-number of course} + course_table = table.find("table", id="ACE_DERIVED_REGFRM1_DESCR20${}".format(i)) + time_table = course_table.find("table", id="CLASS_MTG_VW$scroll${}".format(i)) + # Find table with class PSLEVEL3GRID + inner_table = time_table.find("table", class_="PSLEVEL3GRID") + # Find all rows in table + rows = inner_table.find_all("tr") + for row in rows[1:]: + row_dict = {} + # Class group and type only shows on row [1] and [2], the rest are \n + row_dict["class group"] = row.find_all("td")[1].text.strip() + if row_dict["class group"] == "\xa0" or row_dict["class group"] == "": + row_dict["class group"] = timetable_dict[course][-1]["class group"] + row_dict["class type"] = row.find_all("td")[2].text.strip() + if row_dict["class type"] == "\xa0" or row_dict["class type"] == "": + row_dict["class type"] = timetable_dict[course][-1]["class type"] + row_dict["location"] = row.find_all("td")[4].text.strip() + + if row.find_all("td")[3].text.strip() != "TBA": + row_dict['start'] = datetime.datetime.strptime(row.find_all("td")[6].text.strip().split(" ")[0] + + " " + row.find_all("td")[3].text.strip().split(" ")[1] + , "%d/%m/%Y %I:%M%p").astimezone(timezone) + row_dict['end'] = datetime.datetime.strptime(row.find_all("td")[6].text.strip().split(" ")[2] + + " " + row.find_all("td")[3].text.strip().split(" ")[3] + , "%d/%m/%Y %I:%M%p").astimezone(timezone) + + timetable_dict[course].append(row_dict) + +# Create calendar +cal = ics.Calendar() + +for course in timetable_dict: + for class_group in timetable_dict[course]: + event = ics.Event() + event.name = "{} - {} - {}".format(course, class_group["class group"], class_group["class type"]) + # if start or end is empty, skip + if "start" not in class_group or "end" not in class_group: + continue + event.begin = class_group["start"] + event.end = class_group["end"] + event.location = class_group["location"] + cal.events.add(event) + +# Save calendar +with open('timetable.ics', 'w') as f: + f.writelines(cal)