--- /dev/null
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$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
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# uv.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
+# poetry.toml
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
+# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
+# pdm.lock
+# pdm.toml
+.pdm-python
+.pdm-build/
+
+# pixi
+# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
+# pixi.lock
+# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
+# in the .venv directory. It is recommended not to include this directory in version control.
+.pixi
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# Redis
+*.rdb
+*.aof
+*.pid
+
+# RabbitMQ
+mnesia/
+rabbitmq/
+rabbitmq-data/
+
+# ActiveMQ
+activemq-data/
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.envrc
+.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/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
+# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+# and can be added to the global gitignore or merged into this file. However, if you prefer,
+# you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
+
+# Streamlit
+.streamlit/secrets.toml
--- /dev/null
+[fratta.ece.gatech.edu]
+; Strings
+window_title = ECE 4180 TA Experience
+site_title = ECE 4180 Office Hours
+queue_title = Queue
+
+nobody_avatar = default_confs/pictures/ed_qr.png
+
+; Actual conf
+api_path_q = https://proxyembedded.bp4k.net/4180q.py
+api_q_element = queue.checkoffQueue,queue.helpQueue
+
+
+api_key = doesnt-matter
--- /dev/null
+[fratta.ece.gatech.edu]
+title__family = Lato
+title__size = 60
+title__weight = bold
+title__slant = roman
+title__bgcolor = #000000
+title__fgcolor = #ffff00
+
+queue__family = Lato
+queue__size = 30
+queue__weight = bold
+queue__slant = roman
+queue__bgcolor = #000000
+queue__fgcolor = #ffff00
+
+picframe__bgcolor = #000000
+
+qframe__bgcolor = #000000
+
+q_checkoff__family = Lato
+q_checkoff__size = 15
+q_checkoff__weight = normal
+q_checkoff__slant = italic
+q_checkoff__bgcolor = #006600
+q_checkoff__fgcolor = #ffffff
+
+q_help__family = Lato
+q_help__size = 15
+q_help__weight = normal
+q_help__slant = italic
+q_help__bgcolor = #ff3300
+q_help__fgcolor = #ffffff
+
+q_empty__family = Helvetica
+q_empty__size = 15
+q_empty__weight = normal
+q_empty__slant = italic
+q_empty__bgcolor = #000000
+q_empty__fgcolor = #ffffff
--- /dev/null
+import argparse
+import sys
+
+from .views import Root
+from .config import ConfigContainer
+from .config import SITE_CONF_PATH, STYLE_CONF_PATH
+
+
+def main(argv):
+ parser = argparse.ArgumentParser(
+ prog="ohdisp",
+ description="Displays TA Office Hours");
+ parser.add_argument('-c', '--config', default=SITE_CONF_PATH);
+ parser.add_argument('-d', '--domain', default="DEFAULT");
+ parser.add_argument('-s', '--stylesheet', default=STYLE_CONF_PATH);
+ parser.add_argument('-f', '--fullscreen', action="store_true");
+
+ args = parser.parse_args(argv[1:])
+
+ config = ConfigContainer(site=args.config, style=args.stylesheet);
+ config.site._set_site(args.domain);
+ config.style._set_site(args.domain);
+
+ root_tk = Root(config);
+
+ if args.fullscreen:
+ root_tk.fullscreen();
+
+ root_tk.mainloop();
+
+
+if __name__ == "__main__":
+ main(sys.argv)
--- /dev/null
+import os
+import configparser
+
+
+__all__ = ['Config']
+
+
+DEFAULT_SITE = "DEFAULT";
+SITE_CONF_PATH = "~/.ohdisp/site.conf";
+STYLE_CONF_PATH = "~/.ohdisp/style.conf";
+
+
+class Config(object):
+ def __init__(self, conf_path):
+ self._conf_path = conf_path;
+ self._config = configparser.ConfigParser(comment_prefixes=(';',));
+ self._config.read(os.path.expanduser(self._conf_path));
+
+ self._set_site(DEFAULT_SITE);
+
+ def _set_site(self, site):
+ if site not in self._config:
+ raise KeyError("'{}' does not exist in config".format(site));
+ self._site = site;
+
+ def _save(self):
+ with open(os.path.expanduser(self._conf_path), 'w') as fp:
+ self._config.write(fp);
+
+ def __getattr__(self, attr):
+ if attr.startswith("_"):
+ return super().__getattr__(attr);
+ return self._config[self._site][attr];
+
+ def __setattr__(self, attr, value):
+ if attr.startswith("_"):
+ super().__setattr__(attr, value);
+ return
+ self._config[self._site][attr] = value;
+
+
+class ConfigContainer(object):
+ def __init__(self, *_, **config_paths):
+ # map from config name to path
+ self._confs = {x: Config(config_paths[x]) for x in config_paths};
+
+ def __getattr__(self, attr):
+ return self._confs[attr];
+
--- /dev/null
+import time
+import datetime
+
+import tkinter as tk
+from tkinter.font import Font
+
+import urllib3
+import pyttsx3
+
+class QueueModel(object):
+ def __init__(self, config, *args, **kwargs):
+ self._cfg = config;
+ self._funcs = tuple(self.parse_cfg(self._cfg.site.api_q_element));
+
+ def fetch(self):
+ resp = urllib3.request(
+ "GET",
+ self._cfg.site.api_path_q)
+ json = resp.json();
+ result = [];
+ for func in self._funcs:
+ result += func(json);
+
+ return result;
+
+ def thread(self, tk_parent):
+ n_students = 0
+ cur_students = [];
+ self.build_elements(cur_students, tk_parent);
+ while True:
+ students = self.fetch();
+ nu_students = len(students);
+ if (nu_students != n_students):
+ d = self.diff_students(cur_students, students);
+ self.build_elements(students, tk_parent);
+ self.announce(d);
+ n_students = nu_students;
+ cur_students = students;
+ time.sleep(2);
+
+ def announce(self, data):
+ new_joinees, d = data
+ if new_joinees:
+ sts = "Student{} {} {} joined the queue!".format(
+ "" if len(d) < 2 else "s",
+ ", ".join(map(lambda x: x["studentName"], d)),
+ "has" if len(d) < 2 else "have",
+ );
+ pyttsx3.speak(sts)
+
+ def build_elements(self, students, tk_parent):
+ for el in tk_parent.winfo_children():
+ el.destroy()
+
+ checkoff_fnt = Font(tk_parent,
+ family=self._cfg.style.q_checkoff__family,
+ size=int(self._cfg.style.q_checkoff__size),
+ weight=self._cfg.style.q_checkoff__weight,
+ slant=self._cfg.style.q_checkoff__slant,
+ );
+
+ help_fnt = Font(tk_parent,
+ family=self._cfg.style.q_help__family,
+ size=int(self._cfg.style.q_help__size),
+ weight=self._cfg.style.q_help__weight,
+ slant=self._cfg.style.q_help__slant,
+ );
+
+ null_fnt = Font(tk_parent,
+ family=self._cfg.style.q_empty__family,
+ size=int(self._cfg.style.q_empty__size),
+ weight=self._cfg.style.q_empty__weight,
+ slant=self._cfg.style.q_empty__slant,
+ );
+
+ if len(students) == 0:
+ lbl = tk.Label(tk_parent,
+ text="The queue is empty\nFeel free to join!",
+ font=checkoff_fnt,
+ fg=self._cfg.style.q_empty__fgcolor,
+ bg=self._cfg.style.q_empty__bgcolor,
+ padx=4,
+ pady=4,
+ anchor='n'
+ );
+ lbl.pack(side='top', fill='x', anchor='n', expand=1);
+ return
+
+ for stu in students:
+ if stu["queueType"] == "checkoff":
+ lbl = tk.Label(tk_parent,
+ text="{} ({} {})".format(
+ stu["studentName"], stu["labId"],
+ stu["partId"]),
+ font=checkoff_fnt,
+ fg=self._cfg.style.q_checkoff__fgcolor,
+ bg=self._cfg.style.q_checkoff__bgcolor,
+ padx=4,
+ pady=4,
+ anchor='w'
+ );
+ else:
+ lbl = tk.Label(tk_parent,
+ text=stu["studentName"],
+ font=checkoff_fnt,
+ fg=self._cfg.style.q_help__fgcolor,
+ bg=self._cfg.style.q_help__bgcolor,
+ padx=4,
+ pady=4,
+ anchor='w'
+ );
+
+ lbl.pack(side='top', fill='x', anchor='n', expand=0);
+
+ @classmethod
+ def diff_students(cls, cur_students, students):
+ cur_students = sorted(cur_students,
+ key=lambda x: datetime.datetime.fromisoformat(
+ x["joinedAt"]));
+ students = sorted(students,
+ key=lambda x: datetime.datetime.fromisoformat(
+ x["joinedAt"]));
+
+ i = 0
+ j = 0
+ l = list()
+
+ while i < len(cur_students) and j < len(students):
+ if cur_students[i]["entryId"] == students[j]["entryId"]:
+ i += 1
+ j += 1
+ else:
+ if len(cur_students) > len(students):
+ l.append(cur_students[i]);
+ i += 1
+ elif len(students) > len(cur_students):
+ l.append(students[j]);
+ j += 1
+
+ if i < len(cur_students):
+ l += cur_students[i:]
+ elif j < len(students):
+ l += students[j:]
+
+ return (
+ len(students) > len(cur_students),
+ l
+ )
+
+ def parse_cfg(self, line):
+ for arg in line.split(","):
+ data = arg.split(".");
+ def func(dic, x=data):
+ curr = dic;
+ for i in x:
+ curr = curr[i];
+ return curr;
+ yield func;
+
--- /dev/null
+import os
+import threading
+
+import tkinter as tk
+from tkinter.font import Font
+from .model import QueueModel
+
+from PIL import ImageTk, Image
+
+
+class TADisplayFrame(tk.Frame):
+ def __init__(self, parent, config, *args, **kwargs):
+ super().__init__(parent, *args, **kwargs);
+
+ self._cnf = config;
+
+ title_font = Font(self,
+ family=self._cnf.style.title__family,
+ size=int(self._cnf.style.title__size),
+ weight=self._cnf.style.title__weight,
+ slant=self._cnf.style.title__slant,
+ );
+
+ self._title = tk.Label(self, text=self._cnf.site.site_title,
+ font=title_font,
+ bg=self._cnf.style.title__bgcolor,
+ fg=self._cnf.style.title__fgcolor);
+ self._title.pack(side='top', anchor='n', fill='x');
+
+ self._frame = StudentCheckoffFrame(self, self._cnf);
+ self._frame.pack(side='right', fill="both");
+ self._frame = TAPictureFrame(self, self._cnf);
+ self._frame.pack(side='top', fill='both', expand=1);
+
+
+class TAPictureFrame(tk.Frame):
+ def __init__(self, parent, config, *args, **kwargs):
+ super().__init__(parent, *args, **kwargs);
+
+ self._cnf = config;
+ self['bg'] = self._cnf.style.picframe__bgcolor;
+
+ im = Image.open(
+ os.path.expanduser(
+ self._cnf.site.nobody_avatar));
+ im.thumbnail((512, 512), Image.Resampling.LANCZOS);
+ self._img = ImageTk.PhotoImage(im);
+ lbl = tk.Label(self, image=self._img);
+ lbl.pack(side="top");
+ lbl2 = tk.Label(self, text=("This feature is a work in progress!"
+ "\nAsk questions on Ed Discussion"
+ " post office hours"
+ "\n\nDoing a video checkoff? Make sure "
+ "to show your code, face and buzzcard!"),
+ font=("Lato", 20, "normal"),
+ bg="#000000",
+ fg="#FFFFFF");
+ lbl2.pack(side="top");
+
+
+
+class StudentCheckoffFrame(tk.Frame):
+ def __init__(self, parent, config, *args, **kwargs):
+ super().__init__(parent, *args, **kwargs);
+
+ self._cnf = config;
+
+ self._model = QueueModel(self._cnf);
+
+ title_font = Font(self,
+ family=self._cnf.style.queue__family,
+ size=int(self._cnf.style.queue__size),
+ weight=self._cnf.style.queue__weight,
+ slant=self._cnf.style.queue__slant,
+ );
+
+ self._title = tk.Label(self, text=self._cnf.site.queue_title,
+ font=title_font,
+ bg=self._cnf.style.queue__bgcolor,
+ fg=self._cnf.style.queue__fgcolor);
+ self._title.pack(side='top', anchor='n', fill='x');
+
+ self._actualframe = tk.Frame(self,
+ bg=self._cnf.style.qframe__bgcolor);
+ self._actualframe.pack(side='top', fill='both', expand=1);
+
+ t = threading.Thread(target=self._model.thread,
+ args=(self._actualframe,),
+ daemon=True)
+ t.start()
+
+
+class Root(tk.Tk):
+ def __init__(self, config, *args, **kwargs):
+ super().__init__(*args, **kwargs);
+
+ self._cnf = config;
+
+ self.title(self._cnf.site.window_title);
+
+ self._tadf = TADisplayFrame(self, self._cnf);
+ self._tadf.pack(fill='both', expand=1);
+
+ def fullscreen(self):
+ self.attributes("-fullscreen", True);
--- /dev/null
+#!/usr/bin/python3
+
+import sys
+
+from ohdisp import main
+
+
+if __name__ == "__main__":
+ main(sys.argv)