Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ services:
network_mode: host
environment:
- DISPLAY=:0
- CHECK_IN_API_URL=http://10.8.0.1:8000
env_file:
- .env
volumes:
Expand Down
13 changes: 13 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[mypy]
strict = true
explicit_package_bases = true
mypy_path = src

[mypy-adafruit_pn532.*]
ignore_missing_imports = true

[mypy-qtawesome]
ignore_missing_imports = true

[mypy-pyqttoast.*]
ignore_missing_imports = true
8 changes: 7 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
pydantic==2.13.4
pyserial==3.5
requests==2.32.5
adafruit-circuitpython-pn532
RPi.GPIO; sys_platform == 'linux'
pyqt6==6.11.0
pyqt6-stubs
qtawesome
pyqt6-sip
pyqt6-sip
pyqt-toast-notification==1.3.3
mypy==2.1.0
types-pyserial
types-requests
8 changes: 1 addition & 7 deletions run_dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,4 @@ for arg in "$@"; do
fi
done

output_file="log.txt"

echo "" >> "$output_file"

date "+%Y-%m-%d %H:%M:%S" >> "$output_file"

python src/main.py "$@" 2>&1 | tee -a log.txt
python src/main.py "$@"
33 changes: 0 additions & 33 deletions src/app_context.py

This file was deleted.

File renamed without changes
Binary file removed src/assets/check_in_manual/button_check_in.png
Binary file not shown.
Binary file removed src/assets/check_in_rfid/icon_check_in.png
Binary file not shown.
Binary file not shown.
Binary file removed src/assets/create_account_barcode/outline_1.png
Binary file not shown.
Binary file removed src/assets/create_account_barcode/outline_2.png
Binary file not shown.
Binary file removed src/assets/create_account_manual/outline_1.png
Binary file not shown.
Binary file removed src/assets/create_account_manual/outline_2.png
Binary file not shown.
Binary file removed src/assets/create_account_manual/register.png
Binary file not shown.
File renamed without changes
File renamed without changes
Binary file removed src/assets/shared/button_generic.png
Binary file not shown.
Binary file removed src/assets/shared/field.png
Binary file not shown.
Binary file removed src/assets/shared/icon_checked_box.png
Binary file not shown.
Binary file removed src/assets/shared/icon_home.png
Binary file not shown.
Binary file removed src/assets/shared/icon_unchecked_box.png
Binary file not shown.
Binary file removed src/assets/shared/outline_full.png
Binary file not shown.
Binary file removed src/assets/sign_waiver/button_done_scanning.png
Binary file not shown.
Binary file removed src/assets/sign_waiver/outline_1.png
Binary file not shown.
Binary file removed src/assets/sign_waiver/outline_2.png
Binary file not shown.
Binary file removed src/assets/sign_waiver/outline_3.png
Binary file not shown.
Binary file removed src/assets/sign_waiver/qr_waiver.png
Binary file not shown.
3 changes: 0 additions & 3 deletions src/config.py

This file was deleted.

164 changes: 73 additions & 91 deletions src/controllers/account_controller.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,89 @@
from __future__ import annotations

import logging
from threading import Thread
from typing import Literal

from PyQt6.QtCore import QTimer
from pyqttoast import ToastPreset

from controllers.api_controller import ApiController, ExternalApiError
from controllers.api_controller import ExternalApiError
from misc.api_models import CreateAccountResponse, StudentResponse
from misc.global_context import context


class AccountController:
def __init__(self, ctx):
self.ctx = ctx

def go_to_review_from_barcode(self, barcode):
self.ctx.nav.show_status("Looking up student...")
logging.info(f"looking up student by barcode: {barcode}")
Thread(target=self._lookup_barcode_worker, args=(barcode,), daemon=True).start()

def _lookup_barcode_worker(self, barcode):
try:
student = ApiController.lookup_by_barcode(barcode)
except ExternalApiError as e:
self.ctx.dispatcher.call.emit(lambda: self._on_external_api_error(e.api))
return
self.ctx.dispatcher.call.emit(lambda s=student: self._on_barcode_result(s))
def __init__(self) -> None:
logging.info("account controller initialized")

def _on_barcode_result(self, student):
self.ctx.nav.hide_status()
if student is None:
self.ctx.nav.show_status("Student not found. Please enter your details manually.")
QTimer.singleShot(3000, self.ctx.nav.hide_status)
return
self.ctx.nav.go_to_create_account_review(
pid=student["pid"],
first_name=student["first_name"],
last_name=student["last_name"],
email=student["email"],
)
def lookup(self, by: Literal["pid", "barcode"], value: str) -> None:
context().main_window.show_toast_async("Looking Up Student", "", ToastPreset.INFORMATION)
logging.info(f"looking up student by {by}: {value}")

def go_to_review_from_pid(self, pid):
self.ctx.nav.show_status("Looking up student...")
logging.info(f"looking up student by PID: {pid}")
Thread(target=self._lookup_pid_worker, args=(pid,), daemon=True).start()
def worker() -> None:
try:
resp = context().api_controller.request("GET", f"/accounts/{by}/{value}")
if resp.status_code == 404:
student: StudentResponse | None = None
else:
resp.raise_for_status()
student = StudentResponse.model_validate(resp.json())
except ExternalApiError as e:
context().main_window.show_toast_async(f"System Error: {e.api}", "Please talk to a staff member", ToastPreset.ERROR)
return
except Exception as e:
logging.error(f"error looking up student by {by}: {e}")
student = None

def _lookup_pid_worker(self, pid):
try:
student = ApiController.lookup_by_pid(pid)
except ExternalApiError as e:
self.ctx.dispatcher.call.emit(lambda: self._on_external_api_error(e.api))
return
self.ctx.dispatcher.call.emit(lambda s=student: self._on_pid_result(s, pid))

def _on_pid_result(self, student, pid):
self.ctx.nav.hide_status()
if student is None:
self.ctx.nav.show_status("Student not found. Please check your PID.")
QTimer.singleShot(3000, self.ctx.nav.hide_status)
return
self.ctx.nav.go_to_create_account_review(
pid=pid,
first_name=student["first_name"],
last_name=student["last_name"],
email=student["email"],
)
if student is None:
context().main_window.show_toast_async("Student Not Found", "Please enter your details manually", ToastPreset.ERROR)
return
context().dispatcher.call.emit(
lambda s=student: context().navigation_controller.go_to_create_account_review(
pid=s.student.pid,
first_name=s.student.first_name,
last_name=s.student.last_name,
email=s.student.email,
)
)

def create_account_from_review(self, *, first_name, last_name, email, pid):
if pid:
self._create(pid=pid)
else:
self._create(first_name=first_name, last_name=last_name, email=email)
Thread(target=worker, daemon=True).start()

def _create(self, *, barcode=None, pid=None, first_name=None, last_name=None, email=None):
self.ctx.nav.show_status("Account creation in progress!")
def create_account(
self,
*,
barcode: str | None = None,
pid: str | None = None,
first_name: str | None = None,
last_name: str | None = None,
email: str | None = None,
) -> None:
context().main_window.show_toast_async("Account creation in progress!", "", ToastPreset.INFORMATION)
logging.info(f"creating account: pid={pid} barcode={barcode}")
Thread(
target=self._create_worker,
kwargs=dict(barcode=barcode, pid=pid, first_name=first_name, last_name=last_name, email=email),
daemon=True,
).start()

def _on_external_api_error(self, api: str):
self.ctx.nav.hide_status()
self.ctx.nav.show_status(f"system error ({api.upper()} api). please talk to a staff member.")
QTimer.singleShot(4000, self.ctx.nav.hide_status)
def worker() -> None:
payload = {k: v for k, v in {
"rfid": context().session.rfid,
"barcode": barcode,
"pid": pid,
"first_name": first_name,
"last_name": last_name,
"email": email,
}.items() if v is not None}
try:
resp = context().api_controller.request("POST", "/accounts", json=payload)
resp.raise_for_status()
result: CreateAccountResponse | None = CreateAccountResponse.model_validate(resp.json())
except ExternalApiError as e:
context().main_window.show_toast_async(f"System Error: {e.api}", "Please talk to a staff member", ToastPreset.ERROR)
return
except Exception as e:
logging.error(f"error creating account: {e}")
result = None

def _create_worker(self, *, barcode, pid, first_name, last_name, email):
try:
result = ApiController.create_account(
self.ctx.rfid,
barcode=barcode,
pid=pid,
first_name=first_name,
last_name=last_name,
email=email,
)
except ExternalApiError as e:
self.ctx.dispatcher.call.emit(lambda: self._on_external_api_error(e.api))
return
self.ctx.dispatcher.call.emit(lambda r=result: self._on_create_result(r))
if result is None:
context().main_window.show_toast_async("Error", "Could not create account, please try manually", ToastPreset.ERROR)
return
logging.info("account creation succeeded")
context().dispatcher.call.emit(context().navigation_controller.pop)

def _on_create_result(self, result):
self.ctx.nav.hide_status()
if result is None:
self.ctx.nav.show_status("ERROR! Could not create account, please try manually.")
QTimer.singleShot(3000, self.ctx.nav.hide_status)
return
logging.info("account creation succeeded")
self.ctx.nav.pop()
Thread(target=worker, daemon=True).start()
Loading
Loading