├── requirements.txt ├── constants.py ├── LICENSE ├── README.md ├── .gitignore └── calare /requirements.txt: -------------------------------------------------------------------------------- 1 | ics==0.7 2 | python-dateutil==2.7.3 3 | pytz==2019.3 4 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | '''' 2 | Constants used to inform Calare's date picker 3 | ''' 4 | 5 | from datetime import date 6 | from dateutil.rrule import MO, TU, WE, TH, FR 7 | 8 | NUM_WEEKS = 10 9 | MIDSEM_WEEK_NUMBER = 6 10 | TUT_LENGTH_HOURS = 1 11 | LAB_LENGTH_HOURS = 2 12 | 13 | 14 | WEEKDAYS = { 15 | "MON": MO, 16 | "TUE": TU, 17 | "WED": WE, 18 | "THU": TH, 19 | "FRI": FR 20 | } 21 | 22 | CLASS_DAYS = { 23 | "M": MO, 24 | "T": TU, 25 | "W": WE, 26 | "H": TH, 27 | "F": FR 28 | } 29 | 30 | START_DATES = { 31 | "20": { 32 | "T2": date(2020, 6, 1), 33 | "T3": date(2020, 9, 14) 34 | }, 35 | "21": { 36 | "T0": date(2021, 1, 4), 37 | "T1": date(2021, 2, 14), # sunday before W1 38 | "T2": date(2021, 5, 30), 39 | "T3": date(2021, 9, 12) 40 | }, 41 | "22": { 42 | "T1": date(2022, 2, 13), 43 | "T2": date(2022, 5, 29), 44 | "T3": date(2022, 9, 11) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Spanish Pear 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # calare 2 | > [!WARNING] 3 | > The functionality in this repo has been upstreamed to [CSESoc Notangles](https://notangles.devsoc.app/). 4 | > As such, the repo is now archived 5 | 6 | ## Introduction 7 | 8 | Disclaimer - this is by no means a reflection of my coding abilities, it's a small tool that I spun up quickly, to work. Pls don't hire/fire me based on the quality of this code :( 9 | 10 | ## Introduction 11 | 12 | A tool designed for UNSW Tutors who wish to create an ICS file with their teaching timetable :) 13 | 14 | ## Assumptions 15 | 16 | Calare works under the assumption that you teach a 1 hour tutorial, followed by a 2 hour lab. 17 | 18 | It also does not schedule classes for midsemester break (currently set to week 6). 19 | 20 | To edit these assumptions, see [constants.py](https://github.com/MrSpanishPear/calare/blob/master/constants.py) 21 | 22 | Additionally, this tool currently only works for offerings that are in the range: 23 | ``` 24 | 20T3 <= offering <= 22T1 25 | ``` 26 | 27 | It will be extended when 2022 term dates are availiable. 28 | 29 | ## Usage 30 | 31 | First, install the needed requirements with: 32 | ``` 33 | pip3 install -r requirements.txt 34 | ``` 35 | 36 | Running the command 37 | 38 | ``` 39 | ./calare 40 | ``` 41 | 42 | or 43 | 44 | ``` 45 | python3 calare 46 | ``` 47 | 48 | will lead you through an interactive sequence of steps. 49 | 50 | You will initially be asked to provide 51 | * Your Name 52 | * Course you are teaching 53 | * Offering (ie 20T3) 54 | 55 | Then, you will infinitely be asked to add classes, and you will be prompted for: 56 | * Class Code (ie H13A) 57 | * Tutor or Assistant Tutor? 58 | * Do you want the tutorial to be added if you are an assistant tutor? 59 | 60 | Finally, the ICS file will be created for you to input into your Outlook or GCal :) 61 | 62 | Enjoy! 63 | 64 | ## Notes 65 | 66 | Daylight savings bug fixed in [5da477c](https://github.com/MrSpanishPear/calare/commit/60c802b3bd11f46641df73a867b2b5f18255fba7) 67 | 68 | Pretty fragile, but hopefully you can modify it to suit your needs! If you have any troubles, please tweet me @SpanishPear :D 69 | 70 | 71 | ## Future Features 72 | - [ ] Ability to parse the tutor timetable page, ie [1511](http://www.cse.unsw.edu.au/~give/Admindata/21T1/COMP1511_timetable.html) 73 | - [ ] Ability to provide text file input, make it more unix'y 74 | - [ ] Web interface?? idk 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # secret 132 | credentials.py 133 | *.ics 134 | *.in 135 | 136 | 137 | .DS_Store/ 138 | .DS_Store 139 | .DS_Store/* -------------------------------------------------------------------------------- /calare: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | import os 4 | import time 5 | from datetime import datetime 6 | import pytz 7 | from ics import Calendar, Event 8 | from dateutil.rrule import rrule, WEEKLY 9 | from constants import WEEKDAYS, CLASS_DAYS, MIDSEM_WEEK_NUMBER, NUM_WEEKS, TUT_LENGTH_HOURS, LAB_LENGTH_HOURS, START_DATES 10 | 11 | 12 | def utc_to_local(utc_dt): 13 | ''' 14 | Should convert UTC to local time, and handle daylight savings 15 | https://stackoverflow.com/a/48602841 16 | ''' 17 | local_tz = pytz.timezone('Australia/Sydney') 18 | local_dt = local_tz.localize(utc_dt, is_dst=None) 19 | return local_dt.astimezone(pytz.utc) 20 | 21 | 22 | def create_cal_event(event_name, event_start, event_end): 23 | ''' 24 | Create's an event 25 | @param event_name:str - Name of the event 26 | @param event_start:str - Time of the event start, "yyyy-mm-dd HH:MM:SS" 27 | @param event_end:str - Time of event end 28 | ''' 29 | e = Event() 30 | e.name = event_name 31 | e.begin = event_start 32 | e.end = event_end 33 | return e 34 | 35 | 36 | def add_class_event(cal, start_time, end_time, dt, event_name): 37 | 38 | start_dt = utc_to_local( 39 | datetime(dt.year, dt.month, dt.day, start_time, 0, 0)) 40 | start = start_dt.strftime("%Y-%m-%d %H:%M:%S") 41 | end_dt = utc_to_local( 42 | datetime(dt.year, dt.month, dt.day, end_time, 0, 0)) 43 | end = end_dt.strftime("%Y-%m-%d %H:%M:%S") 44 | 45 | cal.events.add(create_cal_event(event_name, start, end)) 46 | 47 | 48 | def add_classes(cal, offering_code): 49 | ''' 50 | Continuously adds classes to the ICS. 51 | Uses user input of ClassCode and other settings 52 | ''' 53 | addclass = input("Add a class? [y/n]: ") 54 | while addclass.startswith('y'): 55 | print("---- Adding Class ----") 56 | print("If you enter some wrong information, answer q to restart that question\n") 57 | 58 | course = input("Enter Course Code: ") 59 | 60 | while (class_code := input("Class code? Ie H13A: ")).lower().startswith("q"): 61 | continue 62 | 63 | while (tutor_input := input("T or AT? : ").lower()).startswith("q"): 64 | continue 65 | 66 | tutor_bool = tutor_input.startswith('t') 67 | show_tut = True 68 | 69 | if not tutor_bool: 70 | 71 | while (show_tut_input := input( 72 | "Add Tutorial and lab(y)? Or just Lab(n)? [y/n/q]: ").lower()).startswith("q"): 73 | continue 74 | 75 | show_tut = show_tut_input.startswith('y') 76 | 77 | # ie 20T3 splits to 20 and T3 78 | year = offering_code[:2] 79 | term = offering_code[2:] 80 | start_date = START_DATES.get(year).get(term) 81 | 82 | tut_start = int(class_code[1:3]) 83 | lab_start = int(tut_start) + TUT_LENGTH_HOURS 84 | lab_end = int(lab_start) + LAB_LENGTH_HOURS 85 | 86 | # generate the weeks 87 | weekday = CLASS_DAYS[class_code[0]] 88 | date_rule = rrule(WEEKLY, dtstart=start_date, 89 | count=NUM_WEEKS, byweekday=(weekday)) 90 | 91 | for i, dt in enumerate(date_rule): 92 | # midsem break - no classes 93 | if i == MIDSEM_WEEK_NUMBER - 1: 94 | continue 95 | 96 | # add tutorial 97 | if show_tut: 98 | event_name = " ".join([course.upper(), class_code, "TUT"]) 99 | add_class_event(cal, tut_start, lab_start, dt, event_name) 100 | 101 | # add lab 102 | event_name = " ".join([course.upper(), class_code, "LAB"]) 103 | add_class_event(cal, lab_start, lab_end, dt, event_name) 104 | addclass = input("Add a class? [y/n]: ") 105 | if not addclass.startswith('y'): return 106 | 107 | 108 | def add_help_session(cal, offering_code): 109 | while input("Add a help-session? [y/n]: ").lower().startswith('y'): 110 | print("\n---- Adding Session ----") 111 | print("If you enter some wrong information, answer q to restart thtat question\n") 112 | 113 | course = input("Enter Course Code: ") 114 | 115 | while (day := input("Day of week? (eg THU): ")).lower().startswith("q"): 116 | continue 117 | 118 | while (start_time := input("Start time (eg 1700): ")).lower().startswith("q"): 119 | continue 120 | 121 | while (end_time := input("End time (eg 1900): ")).lower().startswith("q"): 122 | continue 123 | 124 | # TODO -- Error Handling of invalid input 125 | while (weeks := input("Weeks? (eg 3,4,5,7,8,9,10): ")).lower().startswith("q"): 126 | continue 127 | 128 | weeks = list(map(int, weeks.split(","))) 129 | 130 | year = offering_code[:2] 131 | term = offering_code[2:] 132 | start_date = START_DATES.get(year).get(term) 133 | 134 | # generate the weeks 135 | weekday = WEEKDAYS[day] 136 | date_rule = rrule(WEEKLY, dtstart=start_date, 137 | count=NUM_WEEKS, byweekday=(weekday)) 138 | 139 | for i, dt in enumerate(date_rule): 140 | # only add weeks requestd lol 141 | if i not in weeks: 142 | continue 143 | 144 | # add session 145 | event_name = " ".join([course.upper(), "Help Session"]) 146 | add_class_event(cal, int(start_time[:2]), 147 | int(end_time[:2]), dt, event_name) 148 | 149 | 150 | def get_offering(): 151 | '''' 152 | Gets the offering from the user, and validate 153 | ''' 154 | offering_v = input("Enter Offering, ie 20T3: ") 155 | 156 | # BASIC parsing because ceebs 157 | while START_DATES.get(offering_v[:2]).get(offering_v[2:]) is None: 158 | offering_v = input("Invalid Offering (not a 20/21 term) - try again: ") 159 | 160 | return offering_v 161 | 162 | 163 | def print_intro(): 164 | ''' 165 | Prints the intro to the program 166 | ''' 167 | print("Welcome to Calare\n") 168 | print("This is a tool that will generate an ICS cal for your classes given classcodes and other info") 169 | print("This tool is designed for Tutors doing 9 x 1 hour tutorials followed by 2 hour labs") 170 | print("OR Assistant Tutors doing 2 hour Labs") 171 | print("If you have other needs, this will be open source if you'd like : ^)\n\n") 172 | 173 | 174 | if __name__ == "__main__": 175 | c = Calendar() 176 | print_intro() 177 | 178 | name = input("Enter Name: ") 179 | offering = get_offering() 180 | 181 | class_or_help_session = int( 182 | input("Add Class (1), Help Session(2) or finish(3): ").lower()) 183 | 184 | while (class_or_help_session): 185 | if class_or_help_session == 1: 186 | add_classes(c, offering) 187 | 188 | elif class_or_help_session == 2: 189 | add_help_session(c, offering) 190 | 191 | elif class_or_help_session == 3: 192 | break 193 | 194 | class_or_help_session = int(input("Add Class (1), Help Session(2) or finish(3): ").lower()) 195 | 196 | 197 | filename = '-'.join([name, offering, str(int(time.time())), 198 | '-calare-tutor-timetable.ics']) 199 | 200 | if os.path.isfile('./' + filename): 201 | print("Looks like the file", filename, 202 | "already exists :(\nPlease delete/rename it") 203 | sys.exit(1) 204 | 205 | with open(filename, 'w') as output_file: 206 | print(f"Outputting to {filename}...") 207 | output_file.writelines(c) 208 | 209 | print("\nDone! Enjoy and please star this project on Github if it's been helpful!") 210 | --------------------------------------------------------------------------------