diff --git a/HTMLTemplates/certifications_v2.mako b/HTMLTemplates/certifications_v2.mako new file mode 100644 index 0000000..5c8bd1d --- /dev/null +++ b/HTMLTemplates/certifications_v2.mako @@ -0,0 +1,161 @@ +<%def name="scripts()"> + + +<%def name="head()"> + + + +<%def name="title()">Certification Monitor +<%inherit file="base.mako"/> + +
+
Certification Monitor
+
+ TFI logo +
+
+
+ +
+ +
+
+
diff --git a/HTMLTemplates/links.mako b/HTMLTemplates/links.mako index 6494fbc..fa84084 100644 --- a/HTMLTemplates/links.mako +++ b/HTMLTemplates/links.mako @@ -23,9 +23,9 @@ ${self.logo()}
Personal

General @@ -47,9 +47,7 @@ ${self.logo()}
Keyholder

% endif % endif -
BFF Stations +
Check-in Stations


To add feature requests or report issues, please go to:https://github.com/alan412/CheckMeIn/issues -
\ No newline at end of file +
diff --git a/README.md b/README.md index c712858..1c41823 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ a system for checking into and out of a building # Setup +Start in the repo. e.g. ```cd .../CheckMeIn``` for wherever you clone the repo. You'll need a python venv, set it up like this: 1. ```python3 -m venv venv``` 2. ```source venv/bin/activate``` @@ -23,13 +24,13 @@ DO NOT push these renamed files to the origin repository. Once you are satisfied that you have the dependencies met, and the unit tests are passing, then to run the server, you will execute: -```python3 checkmein.py development.conf``` +```python3 checkMeIn.py development.conf``` You can connect to your server using a local browser at "http://localhost:8089" Note: -* When first starting, assuming you ran the tests, you may choose to -```mkdir data``` -```cp testData test.db data/checkmein.db``` +* When first starting, assuming you ran the tests, you may choose to +```mkdir data``` +```cp testData/test.db data/checkMeIn.db``` This gives you a database with a couple of members and an admin user whose name is 'admin' and password is 'password'. diff --git a/checkMeIn.py b/checkMeIn.py index 12302d8..ed41cd4 100644 --- a/checkMeIn.py +++ b/checkMeIn.py @@ -3,6 +3,7 @@ from mako.lookup import TemplateLookup import cherrypy import cherrypy.process.plugins +import os import engine from webBase import WebBase, Cookie @@ -24,8 +25,9 @@ def update(self, msg): cherrypy.engine.publish(self.updateChannel, fullMessage) def __init__(self): + templates_dir = os.path.join(os.path.dirname(__file__), 'HTMLTemplates') self.lookup = TemplateLookup( - directories=['HTMLTemplates'], default_filters=['h']) + directories=[templates_dir], default_filters=['h'], filesystem_checks=True) self.updateChannel = 'updates' self.engine = engine.Engine( cherrypy.config["database.path"], cherrypy.config["database.name"], self.update) diff --git a/docs.py b/docs.py index 1114eb9..5409c4a 100644 --- a/docs.py +++ b/docs.py @@ -25,7 +25,7 @@ def getDocumentation(): returns="Returns a webpage", notes=["This shows a list of links that barcode might find useful based off their role", "If barcode is left off, it is a list of links for display stations"]), - Doc('Unlock', '/unlock?location=BFF&barcode=', + Doc('Unlock', '/unlock?location=TFI&barcode=', returns="Returns the station webpage", notes=["This records the door was unlocked and checks the person that unlocks it in. For use of door app ONLY"]), Doc('Get Keyholder list', '/admin/getKeyholderJSON', diff --git a/etc/Caddyfile b/etc/Caddyfile new file mode 100644 index 0000000..92658be --- /dev/null +++ b/etc/Caddyfile @@ -0,0 +1,4 @@ +checkmein.stage.theforgeinitiative.org { + tls + reverse_proxy localhost:8447 +} \ No newline at end of file diff --git a/etc/caddy.service b/etc/caddy.service new file mode 100644 index 0000000..966c85d --- /dev/null +++ b/etc/caddy.service @@ -0,0 +1,20 @@ +[Unit] +Description=Caddy +Documentation=https://caddyserver.com/docs/ +After=network.target network-online.target +Requires=network-online.target + +[Service] +Type=notify +User=caddy +Group=caddy +ExecStart=/usr/local/bin/caddy run --environ --config /opt/caddy/Caddyfile +ExecReload=/usr/local/bin/caddy reload --config /opt/caddy/Caddyfile --force +TimeoutStopSec=5s +LimitNOFILE=1048576 +PrivateTmp=true +ProtectSystem=full +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/etc/checkmein.service b/etc/checkmein.service new file mode 100644 index 0000000..9369d98 --- /dev/null +++ b/etc/checkmein.service @@ -0,0 +1,15 @@ +[Unit] +Description=CheckMeIn +After=network-online.target +Requires=network-online.target + +[Service] +User=checkmein +Restart=on-failure +RestartSec=30 +AmbientCapabilities=CAP_NET_BIND_SERVICE +WorkingDirectory=/opt/checkmein/src +ExecStart=/opt/checkmein/venv/bin/python3 checkMeIn.py production.conf + +[Install] +WantedBy=multi-user.target diff --git a/production.conf b/production.conf index 4fa4de7..e1f85fd 100644 --- a/production.conf +++ b/production.conf @@ -1,7 +1,4 @@ [global] -log.error_file : os.path.join(os.getcwd(), 'checkMeIn.log') -log.access_file : '' -log.screen : False server.socket_host : '127.0.0.1' server.socket_port : 8447 database.path : 'data/' diff --git a/static/tools/3dprinter.webp b/static/tools/3dprinter.webp new file mode 100644 index 0000000..74f90fd Binary files /dev/null and b/static/tools/3dprinter.webp differ diff --git a/static/tools/Stretcher shrinker.webp b/static/tools/Stretcher shrinker.webp new file mode 100644 index 0000000..0d8c1e2 Binary files /dev/null and b/static/tools/Stretcher shrinker.webp differ diff --git a/static/tools/blind rivet gun.webp b/static/tools/blind rivet gun.webp new file mode 100644 index 0000000..b5ad45f Binary files /dev/null and b/static/tools/blind rivet gun.webp differ diff --git a/static/tools/cnc.webp b/static/tools/cnc.webp new file mode 100644 index 0000000..d160751 Binary files /dev/null and b/static/tools/cnc.webp differ diff --git a/static/tools/dremel.webp b/static/tools/dremel.webp new file mode 100644 index 0000000..4a748ac Binary files /dev/null and b/static/tools/dremel.webp differ diff --git a/static/tools/drill.webp b/static/tools/drill.webp new file mode 100644 index 0000000..4351838 Binary files /dev/null and b/static/tools/drill.webp differ diff --git a/static/tools/drillpress.webp b/static/tools/drillpress.webp new file mode 100644 index 0000000..fcf03d7 Binary files /dev/null and b/static/tools/drillpress.webp differ diff --git a/static/tools/grinder.webp b/static/tools/grinder.webp new file mode 100644 index 0000000..cbb12f6 Binary files /dev/null and b/static/tools/grinder.webp differ diff --git a/static/tools/images.json b/static/tools/images.json new file mode 100644 index 0000000..487ad23 --- /dev/null +++ b/static/tools/images.json @@ -0,0 +1,21 @@ +{ + "1": "/static/tools/sheet%20metal%20brake.webp", + "2": "/static/tools/blind%20rivet%20gun.webp", + "3": "/static/tools/Stretcher%20shrinker.webp", + "4": "/static/tools/3dprinter.webp", + "5": "/static/tools/drill.webp", + "6": "/static/tools/solderingiron.webp", + "7": "/static/tools/dremel.webp", + "8": "/static/tools/8.svg", + "9": "/static/tools/drillpress.webp", + "10": "/static/tools/10.svg", + "11": "/static/tools/11.svg", + "12": "/static/tools/tablemountedjigsaw.webp", + "13": "/static/tools/13.svg", + "14": "/static/tools/14.svg", + "15": "/static/tools/cnc.webp", + "16": "/static/tools/lathe.webp", + "17": "/static/tools/tablesaw.webp", + "18": "/static/tools/miter.webp", + "19": "/static/tools/grinder.webp" +} diff --git a/static/tools/lathe.webp b/static/tools/lathe.webp new file mode 100644 index 0000000..9044fb7 Binary files /dev/null and b/static/tools/lathe.webp differ diff --git a/static/tools/miter.webp b/static/tools/miter.webp new file mode 100644 index 0000000..6606086 Binary files /dev/null and b/static/tools/miter.webp differ diff --git a/static/tools/scrollsaw.webp b/static/tools/scrollsaw.webp new file mode 100644 index 0000000..0bbc6ea Binary files /dev/null and b/static/tools/scrollsaw.webp differ diff --git a/static/tools/sheet metal brake.webp b/static/tools/sheet metal brake.webp new file mode 100644 index 0000000..127e13f Binary files /dev/null and b/static/tools/sheet metal brake.webp differ diff --git a/static/tools/solderingiron.webp b/static/tools/solderingiron.webp new file mode 100644 index 0000000..4bd64b6 Binary files /dev/null and b/static/tools/solderingiron.webp differ diff --git a/static/tools/tablemountedjigsaw.webp b/static/tools/tablemountedjigsaw.webp new file mode 100644 index 0000000..19be3f7 Binary files /dev/null and b/static/tools/tablemountedjigsaw.webp differ diff --git a/static/tools/tablesaw.webp b/static/tools/tablesaw.webp new file mode 100644 index 0000000..2d98a7a Binary files /dev/null and b/static/tools/tablesaw.webp differ diff --git a/tests/misc_tests.py b/tests/misc_tests.py index 49cda73..a0f064e 100644 --- a/tests/misc_tests.py +++ b/tests/misc_tests.py @@ -29,5 +29,5 @@ def test_metrics(self): def test_unlock(self): with self.patch_session(): - self.getPage("/unlock?location=BFF&barcode=100091") + self.getPage("/unlock?location=TFI&barcode=100091") self.assertStatus('303 See Other') diff --git a/tests/sampleData.py b/tests/sampleData.py index bb31dd1..db5a446 100644 --- a/tests/sampleData.py +++ b/tests/sampleData.py @@ -6,7 +6,7 @@ def timeAgo(days=0, hours=0): def testData(): - return{ + d = { "visits": [ { "start": timeAgo(days=7, hours=1), @@ -123,21 +123,54 @@ def testData(): ], "certifications": [ - {"barcode": "100091", - "tool_id": 1, - "level": 30, - "date": "", - "certifier": "LEGACY"}, - {"barcode": "100091", - "tool_id": 1, - "level": 40, - "date": timeAgo(days=8), - "certifier": "LEGACY"}, - {"barcode": "100032", - "tool_id": 1, - "level": 10, - "date": timeAgo(days=10), - "certifier": "100091"} + # Member 100091 across many tools with increasing levels + {"barcode": "100091", "tool_id": 1, "level": 10, "date": timeAgo(days=60), "certifier": "LEGACY"}, + {"barcode": "100091", "tool_id": 2, "level": 20, "date": timeAgo(days=50), "certifier": "LEGACY"}, + {"barcode": "100091", "tool_id": 3, "level": 30, "date": timeAgo(days=40), "certifier": "LEGACY"}, + {"barcode": "100091", "tool_id": 4, "level": 40, "date": timeAgo(days=30), "certifier": "LEGACY"}, + {"barcode": "100091", "tool_id": 5, "level": 10, "date": timeAgo(days=25), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 6, "level": 20, "date": timeAgo(days=20), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 7, "level": 30, "date": timeAgo(days=15), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 8, "level": 40, "date": timeAgo(days=14), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 9, "level": 10, "date": timeAgo(days=13), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 10, "level": 20, "date": timeAgo(days=12), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 11, "level": 30, "date": timeAgo(days=11), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 12, "level": 40, "date": timeAgo(days=10), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 13, "level": 10, "date": timeAgo(days=9), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 14, "level": 20, "date": timeAgo(days=8), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 15, "level": 30, "date": timeAgo(days=7), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 16, "level": 40, "date": timeAgo(days=6), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 17, "level": 10, "date": timeAgo(days=5), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 18, "level": 20, "date": timeAgo(days=4), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 19, "level": 30, "date": timeAgo(days=3), "certifier": "100091"}, + + # Member 100032 with a mix including NONE to show absence, and CERTIFIED + {"barcode": "100032", "tool_id": 1, "level": 0, "date": "", "certifier": "LEGACY"}, + {"barcode": "100032", "tool_id": 4, "level": 10, "date": timeAgo(days=20), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 10, "level": 10, "date": timeAgo(days=18), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 15, "level": 20, "date": timeAgo(days=10), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 18, "level": 10, "date": timeAgo(days=2), "certifier": "100091"} + , + {"barcode": "100091", "tool_id": 1, "level": 30, "date": timeAgo(days=2), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 2, "level": 40, "date": timeAgo(days=2), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 3, "level": 10, "date": timeAgo(days=1), "certifier": "100091"}, + {"barcode": "100091", "tool_id": 4, "level": 20, "date": timeAgo(days=1), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 5, "level": 30, "date": timeAgo(days=12), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 6, "level": 20, "date": timeAgo(days=9), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 7, "level": 30, "date": timeAgo(days=7), "certifier": "100091"}, + {"barcode": "100032", "tool_id": 8, "level": 40, "date": timeAgo(days=6), "certifier": "100091"}, + {"barcode": "100090", "tool_id": 1, "level": 10, "date": timeAgo(days=14), "certifier": "100091"}, + {"barcode": "100090", "tool_id": 4, "level": 10, "date": timeAgo(days=13), "certifier": "100091"}, + {"barcode": "100090", "tool_id": 10, "level": 20, "date": timeAgo(days=11), "certifier": "100091"}, + {"barcode": "100090", "tool_id": 15, "level": 30, "date": timeAgo(days=10), "certifier": "100091"}, + {"barcode": "100090", "tool_id": 17, "level": 10, "date": timeAgo(days=5), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 3, "level": 20, "date": timeAgo(days=16), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 6, "level": 30, "date": timeAgo(days=15), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 9, "level": 10, "date": timeAgo(days=12), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 12, "level": 20, "date": timeAgo(days=11), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 14, "level": 30, "date": timeAgo(days=8), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 18, "level": 10, "date": timeAgo(days=4), "certifier": "100091"}, + {"barcode": "100093", "tool_id": 19, "level": 20, "date": timeAgo(days=3), "certifier": "100091"} ], "customReports": [{ "report_id": 1, @@ -151,7 +184,7 @@ def testData(): }], "unlocks": [{ "time": timeAgo(hours=1), - "location": "BFF", + "location": "TFI", "barcode": "100091" }], "guests": [{ @@ -174,3 +207,100 @@ def testData(): "value": "15" }] } + extra_members = [] + base_id = 200000 + first_names = [ + "Alex","Jordan","Taylor","Casey","Riley","Morgan","Avery","Quinn","Skyler","Reese", + "Jamie","Cameron","Drew","Peyton","Rowan","Hayden","Emerson","Parker","Sage","Charlie" + ] + last_names = [ + "Smith","Johnson","Brown","Taylor","Anderson","Thomas","Jackson","White","Harris","Martin", + "Thompson","Garcia","Martinez","Robinson","Clark","Rodriguez","Lewis","Lee","Walker","Hall" + ] + def gen_name(idx): + fn = first_names[idx % len(first_names)] + ln = last_names[(idx // len(first_names)) % len(last_names)] + return fn, ln + for i in range(1, 251): + b = str(base_id + i) + fn, ln = gen_name(i) + extra_members.append({ + "barcode": b, + "displayName": f"{fn} {ln}", + "firstName": fn, + "lastName": ln, + "email": f"{fn.lower()}.{ln.lower()}@email.com", + "membershipExpires": timeAgo(days=-30), + }) + for i in range(251, 271): + b = str(base_id + i) + fn, ln = gen_name(i) + extra_members.append({ + "barcode": b, + "displayName": f"{fn} {ln}", + "firstName": fn, + "lastName": ln, + "email": f"{fn.lower()}.{ln.lower()}@email.com", + "membershipExpires": timeAgo(days=-30), + }) + d["members"].extend(extra_members) + for m in d["members"]: + dn = str(m.get("displayName", "")) + if dn.startswith("Member ") or dn.startswith("Member"): + fn, ln = gen_name(int(m["barcode"]) % 1000) + m["firstName"], m["lastName"] = fn, ln + m["displayName"] = f"{fn} {ln}" + m["email"] = f"{fn.lower()}.{ln.lower()}@email.com" + levels = [10, 20, 30, 40] + tool_ids = list(range(1, 20)) + extra_certs = [] + count = 0 + for idx, m in enumerate(extra_members): + for j in range(2): + if count >= 50: + break + t = tool_ids[(idx + j) % len(tool_ids)] + lvl = levels[(idx + j) % len(levels)] + extra_certs.append({ + "barcode": m["barcode"], + "tool_id": t, + "level": lvl, + "date": timeAgo(days=idx % 30 + 1), + "certifier": "100091" + }) + count += 1 + if count >= 50: + break + d["certifications"].extend(extra_certs) + # Add 5 more Basic (Red Dot) certifications for 3D printers (tool_id 4) + extra_basic = [] + for i, m in enumerate(extra_members[:5], start=1): + extra_basic.append({ + "barcode": m["barcode"], + "tool_id": 4, + "level": 1, # Basic (Red Dot) + "date": timeAgo(days=i), + "certifier": "100091" + }) + d["certifications"].extend(extra_basic) + extra_visits = [] + now_start = timeAgo(hours=1) + # Ensure a couple of baseline members are checked in as well + extra_visits.append({ + "start": now_start, + "barcode": "100091", + "status": "In" + }) + extra_visits.append({ + "start": now_start, + "barcode": "100032", + "status": "In" + }) + for m in extra_members[:50]: + extra_visits.append({ + "start": now_start, + "barcode": m["barcode"], + "status": "In" + }) + d["visits"].extend(extra_visits) + return d diff --git a/utils.py b/utils.py index ece8ff6..de6b7e9 100644 --- a/utils.py +++ b/utils.py @@ -2,7 +2,7 @@ import email.utils import smtplib -FROM_EMAIL = "tfi@checkmein.site" +FROM_EMAIL = "noreply@theforgeinitiative.org" FROM_NAME = "TFI CheckMeIn" diff --git a/webCertifications.py b/webCertifications.py index 4abf55e..55c5337 100644 --- a/webCertifications.py +++ b/webCertifications.py @@ -1,5 +1,7 @@ import cherrypy from webBase import WebBase +import os +import json class WebCertifications(WebBase): @@ -127,3 +129,70 @@ def all(self): dbConnection) return self.showCertifications(message, tools, certifications) + + @cherrypy.expose + def v2(self, debug=None): + with self.dbConnect() as dbConnection: + tools = self.engine.certifications.getAllTools(dbConnection) + users = self.engine.certifications.getInBuildingUserList(dbConnection) + # Load curated images mapping if available + images_map = {} + try: + static_root = os.path.join(os.getcwd(), 'static', 'tools') + with open(os.path.join(static_root, 'images.json'), 'r', encoding='utf-8') as f: + images_map = json.load(f) + except Exception: + images_map = {} + tool_map = {} + for t in tools: + tool_id = t[0] + tool_name = t[1] + # Resolve curated image: by id first, then by lowercased name + curated_url = ( + images_map.get(str(tool_id)) + or images_map.get(tool_name) + or images_map.get(tool_name.lower()) + or images_map.get(tool_name.replace(' ', '_').lower()) + ) + base_static = f"/static/tools/{tool_id}" + tool_map[t[0]] = { + 'id': t[0], + 'name': tool_name, + 'group': t[2], + 'members': [], + 'image_url': curated_url or f"{base_static}.avif", + 'img_avif': f"{base_static}.avif", + 'img_png': f"{base_static}.png", + 'img_jpg': f"{base_static}.jpg" + } + for user_id, tooluser in users.items(): + for tid, tl in tooluser.tools.items(): + level = int(tl[1]) if tl and tl[1] is not None else 0 + if level > 0: + base_label = self.engine.certifications.getLevelName(level) + # Customize labels per request + if level == 1: + label = 'Basic (Red Dot)' + elif level == 10: + label = 'Certified (Green Dot)' + else: + label = base_label + tool_map.get(tid, {}).get('members', []).append({ + 'displayName': tooluser.displayName, + 'barcode': tooluser.barcode, + 'level': level, + 'level_name': label + }) + for v in tool_map.values(): + v['members'] = sorted(v['members'], key=lambda m: (m['level'], m['displayName'])) + tools_data = [tool_map[t[0]] for t in tools] + levels = [ + {'value': 10, 'name': self.engine.certifications.getLevelName(10), 'class': 'level-10'}, + {'value': 20, 'name': self.engine.certifications.getLevelName(20), 'class': 'level-20'}, + {'value': 30, 'name': self.engine.certifications.getLevelName(30), 'class': 'level-30'}, + {'value': 40, 'name': self.engine.certifications.getLevelName(40), 'class': 'level-40'}, + ] + if debug: + cherrypy.response.headers['Content-Type'] = 'application/json; charset=utf-8' + return json.dumps(tools_data).encode('utf-8') + return self.template('certifications_v2.mako', tools=tools_data, levels=levels, tools_json=json.dumps(tools_data))