From 64f22ab33cb22d435ca5b3ecc9c29bba49df4828 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:14:13 +0000 Subject: [PATCH 01/19] added migration --- ...ylog_operation_alter_activitylog_status.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 HeroHours/migrations/0021_alter_activitylog_operation_alter_activitylog_status.py diff --git a/HeroHours/migrations/0021_alter_activitylog_operation_alter_activitylog_status.py b/HeroHours/migrations/0021_alter_activitylog_operation_alter_activitylog_status.py new file mode 100644 index 0000000..ac09211 --- /dev/null +++ b/HeroHours/migrations/0021_alter_activitylog_operation_alter_activitylog_status.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.9 on 2026-02-14 23:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('HeroHours', '0020_alter_users_last_in'), + ] + + operations = [ + migrations.AlterField( + model_name='activitylog', + name='operation', + field=models.CharField(choices=[('Check In', 'Check In'), ('Check Out', 'Check Out'), ('None', 'None'), ('Auto Check Out', 'Auto Check Out'), ('Reset', 'Reset')], max_length=15), + ), + migrations.AlterField( + model_name='activitylog', + name='status', + field=models.CharField(choices=[('Success', 'Success'), ('Error', 'Error'), ('User Not Found', 'User Not Found'), ('Inactive User', 'Inactive User')], max_length=20), + ), + ] \ No newline at end of file From dd08db06bf8948aba9d0b76271ac9a7c619a4497 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:14:38 +0000 Subject: [PATCH 02/19] formatting --- HeroHours/migrations/0001_initial.py | 2 +- HeroHours/migrations/0002_alter_users_total_seconds.py | 2 +- HeroHours/migrations/0003_users_last_in_users_last_out.py | 2 +- .../migrations/0004_alter_users_last_in_alter_users_last_out.py | 2 +- HeroHours/migrations/0005_alter_users_total_hours.py | 2 +- HeroHours/migrations/0006_alter_users_total_hours.py | 2 +- HeroHours/migrations/0007_alter_users_total_hours.py | 2 +- .../migrations/0008_activitylog_alter_users_total_hours.py | 2 +- HeroHours/migrations/0009_alter_activitylog_message.py | 2 +- HeroHours/migrations/0010_alter_activitylog_options.py | 2 +- .../0011_alter_activitylog_options_alter_activitylog_status.py | 2 +- ...12_alter_activitylog_options_alter_users_options_and_more.py | 2 +- HeroHours/migrations/0013_alter_activitylog_options.py | 2 +- .../migrations/0014_users_is_active_alter_activitylog_status.py | 2 +- HeroHours/migrations/0015_alter_users_last_in.py | 2 +- HeroHours/migrations/0016_alter_users_total_hours.py | 2 +- HeroHours/migrations/0017_activitylog_entered.py | 2 +- HeroHours/migrations/0019_alter_activitylog_operation.py | 2 +- HeroHours/migrations/0020_alter_users_last_in.py | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/HeroHours/migrations/0001_initial.py b/HeroHours/migrations/0001_initial.py index 3395912..1c6dfd9 100644 --- a/HeroHours/migrations/0001_initial.py +++ b/HeroHours/migrations/0001_initial.py @@ -25,4 +25,4 @@ class Migration(migrations.Migration): 'db_table': 'Users', }, ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0002_alter_users_total_seconds.py b/HeroHours/migrations/0002_alter_users_total_seconds.py index 531bce1..4a42f4e 100644 --- a/HeroHours/migrations/0002_alter_users_total_seconds.py +++ b/HeroHours/migrations/0002_alter_users_total_seconds.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Total_Seconds', field=models.FloatField(default=0), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0003_users_last_in_users_last_out.py b/HeroHours/migrations/0003_users_last_in_users_last_out.py index e2419bd..42b80d0 100644 --- a/HeroHours/migrations/0003_users_last_in_users_last_out.py +++ b/HeroHours/migrations/0003_users_last_in_users_last_out.py @@ -20,4 +20,4 @@ class Migration(migrations.Migration): name='Last_Out', field=models.DateTimeField(null=True), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0004_alter_users_last_in_alter_users_last_out.py b/HeroHours/migrations/0004_alter_users_last_in_alter_users_last_out.py index 69e4b20..31acddb 100644 --- a/HeroHours/migrations/0004_alter_users_last_in_alter_users_last_out.py +++ b/HeroHours/migrations/0004_alter_users_last_in_alter_users_last_out.py @@ -20,4 +20,4 @@ class Migration(migrations.Migration): name='Last_Out', field=models.DateTimeField(null=True), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0005_alter_users_total_hours.py b/HeroHours/migrations/0005_alter_users_total_hours.py index ae3b0c0..2152b55 100644 --- a/HeroHours/migrations/0005_alter_users_total_hours.py +++ b/HeroHours/migrations/0005_alter_users_total_hours.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Total_Hours', field=models.TimeField(), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0006_alter_users_total_hours.py b/HeroHours/migrations/0006_alter_users_total_hours.py index 05df508..6264c01 100644 --- a/HeroHours/migrations/0006_alter_users_total_hours.py +++ b/HeroHours/migrations/0006_alter_users_total_hours.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Total_Hours', field=models.TimeField(), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0007_alter_users_total_hours.py b/HeroHours/migrations/0007_alter_users_total_hours.py index db2795a..b42fcef 100644 --- a/HeroHours/migrations/0007_alter_users_total_hours.py +++ b/HeroHours/migrations/0007_alter_users_total_hours.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Total_Hours', field=models.TimeField(), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0008_activitylog_alter_users_total_hours.py b/HeroHours/migrations/0008_activitylog_alter_users_total_hours.py index 3cc179b..c78f55d 100644 --- a/HeroHours/migrations/0008_activitylog_alter_users_total_hours.py +++ b/HeroHours/migrations/0008_activitylog_alter_users_total_hours.py @@ -29,4 +29,4 @@ class Migration(migrations.Migration): name='Total_Hours', field=models.TimeField(), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0009_alter_activitylog_message.py b/HeroHours/migrations/0009_alter_activitylog_message.py index f986993..0d2b3cc 100644 --- a/HeroHours/migrations/0009_alter_activitylog_message.py +++ b/HeroHours/migrations/0009_alter_activitylog_message.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='message', field=models.TextField(default=''), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0010_alter_activitylog_options.py b/HeroHours/migrations/0010_alter_activitylog_options.py index 0ef05c5..ea20c54 100644 --- a/HeroHours/migrations/0010_alter_activitylog_options.py +++ b/HeroHours/migrations/0010_alter_activitylog_options.py @@ -14,4 +14,4 @@ class Migration(migrations.Migration): name='activitylog', options={'ordering': ['-id']}, ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0011_alter_activitylog_options_alter_activitylog_status.py b/HeroHours/migrations/0011_alter_activitylog_options_alter_activitylog_status.py index b19602b..b28cabc 100644 --- a/HeroHours/migrations/0011_alter_activitylog_options_alter_activitylog_status.py +++ b/HeroHours/migrations/0011_alter_activitylog_options_alter_activitylog_status.py @@ -19,4 +19,4 @@ class Migration(migrations.Migration): name='status', field=models.CharField(choices=[('success', 'Success'), ('error', 'Error')], max_length=20), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0012_alter_activitylog_options_alter_users_options_and_more.py b/HeroHours/migrations/0012_alter_activitylog_options_alter_users_options_and_more.py index da56584..49bc67d 100644 --- a/HeroHours/migrations/0012_alter_activitylog_options_alter_users_options_and_more.py +++ b/HeroHours/migrations/0012_alter_activitylog_options_alter_users_options_and_more.py @@ -28,4 +28,4 @@ class Migration(migrations.Migration): name='status', field=models.CharField(choices=[('success', 'Success'), ('error', 'Error'), ('user not found', 'User Not Found')], max_length=20), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0013_alter_activitylog_options.py b/HeroHours/migrations/0013_alter_activitylog_options.py index 96aef25..c3abd9f 100644 --- a/HeroHours/migrations/0013_alter_activitylog_options.py +++ b/HeroHours/migrations/0013_alter_activitylog_options.py @@ -14,4 +14,4 @@ class Migration(migrations.Migration): name='activitylog', options={'ordering': ['-timestamp']}, ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0014_users_is_active_alter_activitylog_status.py b/HeroHours/migrations/0014_users_is_active_alter_activitylog_status.py index a315497..cd93ca1 100644 --- a/HeroHours/migrations/0014_users_is_active_alter_activitylog_status.py +++ b/HeroHours/migrations/0014_users_is_active_alter_activitylog_status.py @@ -20,4 +20,4 @@ class Migration(migrations.Migration): name='status', field=models.CharField(choices=[('success', 'Success'), ('error', 'Error'), ('user not found', 'User Not Found'), ('inactive user', 'Inactive User')], max_length=20), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0015_alter_users_last_in.py b/HeroHours/migrations/0015_alter_users_last_in.py index d4adff2..7d5abff 100644 --- a/HeroHours/migrations/0015_alter_users_last_in.py +++ b/HeroHours/migrations/0015_alter_users_last_in.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Last_In', field=models.DateTimeField(auto_now_add=True, null=True), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0016_alter_users_total_hours.py b/HeroHours/migrations/0016_alter_users_total_hours.py index 331d137..f15560e 100644 --- a/HeroHours/migrations/0016_alter_users_total_hours.py +++ b/HeroHours/migrations/0016_alter_users_total_hours.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Total_Hours', field=models.DurationField(), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0017_activitylog_entered.py b/HeroHours/migrations/0017_activitylog_entered.py index aa3bd14..fe10cfa 100644 --- a/HeroHours/migrations/0017_activitylog_entered.py +++ b/HeroHours/migrations/0017_activitylog_entered.py @@ -22,4 +22,4 @@ class Migration(migrations.Migration): preserve_default=False, ), migrations.RunPython(code=copy_field), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0019_alter_activitylog_operation.py b/HeroHours/migrations/0019_alter_activitylog_operation.py index 51f26bb..bd63518 100644 --- a/HeroHours/migrations/0019_alter_activitylog_operation.py +++ b/HeroHours/migrations/0019_alter_activitylog_operation.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='operation', field=models.CharField(choices=[('checkIn', 'Check In'), ('checkOut', 'Check Out'), ('none', 'None'), ('autoCheckOut', 'Auto Check Out')], max_length=14), ), - ] + ] \ No newline at end of file diff --git a/HeroHours/migrations/0020_alter_users_last_in.py b/HeroHours/migrations/0020_alter_users_last_in.py index 8618cdf..ea44dfb 100644 --- a/HeroHours/migrations/0020_alter_users_last_in.py +++ b/HeroHours/migrations/0020_alter_users_last_in.py @@ -15,4 +15,4 @@ class Migration(migrations.Migration): name='Last_In', field=models.DateTimeField(null=True), ), - ] + ] \ No newline at end of file From 2d2a7c7c41e16e673055be73f3e09c50a0160ca3 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:14:49 +0000 Subject: [PATCH 03/19] javascript changes --- HeroHours/static/js/hours.js | 49 +++++++++++++++++++----------------- HeroHours/static/js/login.js | 6 ++--- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/HeroHours/static/js/hours.js b/HeroHours/static/js/hours.js index d778ec1..31f259e 100644 --- a/HeroHours/static/js/hours.js +++ b/HeroHours/static/js/hours.js @@ -11,6 +11,7 @@ function playSound(status) { switch (status) { case "Success": + case "Check In": audio = successAudio; break; case "User Not Found": @@ -53,7 +54,7 @@ window.onload = function () { function updateTime() { const timeDiv = document.querySelector(".time"); const now = new Date(); - const prehours = now.getHours() == 12? 12 : now.getHours() % 12; + const prehours = now.getHours() === 0 ? 12 : (now.getHours() > 12 ? now.getHours() - 12 : now.getHours()); const hours = prehours.toString().padStart(2, "0"); const minutes = now.getMinutes().toString().padStart(2, "0"); const seconds = now.getSeconds().toString().padStart(2, "0"); @@ -85,7 +86,6 @@ document.addEventListener("DOMContentLoaded", function (event) { }); async function handleFormSubmission(event) { - console.time('benchmark'); let serialized_data = new URLSearchParams(new FormData(this)).toString(); let data = serialized_data.split("&")[1].split("=")[1]; if (data === "-404" || data === "%2B404" || data === "*"||data === 'admin'||data === '---') { @@ -100,24 +100,20 @@ async function handleFormSubmission(event) { return; } // submit the form ourselves - const req = await fetch(queryUrl, { - headers: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", - 'X-Requested-With': 'XMLHttpRequest' - }, - body: serialized_data.toString().replaceAll("ID+",""), - method: "POST", - }).then(async function (response) { - console.log(response) - console.timeLog('benchmark'); + try { + const response = await fetch(queryUrl, { + headers: { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + 'X-Requested-With': 'XMLHttpRequest' + }, + body: serialized_data.toString().replaceAll("ID+",""), + method: "POST", + }); //handle the form response ourselves document.getElementById("subID").reset(); // reset the input box if (response.ok) { - console.log(new Date()) - console.debug(response); // read the body let body = await response.json(); - console.debug(body); if (body.status == "Sent") return; // Play the sound based on the response playSound(body.status); @@ -135,23 +131,30 @@ async function handleFormSubmission(event) { console.error("Error:", response); addRow({ id: 0, - userID: data, + entered: data, operation: "None", status: response.statusText, message: "", }); } - }); - console.timeEnd('benchmark'); + } catch (error) { + console.error("Fetch error:", error); + document.getElementById("subID").reset(); // reset the input box on error + addRow({ + id: 0, + entered: data, + operation: "None", + status: "Error", + message: error.message || "", + }); + } } -async function addRow(item) { - console.log(item); +function addRow(item) { // Get the table body let table = document.getElementById("logBody"); let colorClass; let operationClass; - console.log(item); switch (item.status) { case "User Not Found": colorClass = "warning"; @@ -201,6 +204,6 @@ async function addRow(item) { cell3.innerHTML = `${item.status} ${item.message}`; } document.addEventListener('keydown', function(event) { - if( event.keyCode == 17 || event.keyCode == 74 ) + if (event.key === 'Control' || event.key === 'j') event.preventDefault(); - }); + }); \ No newline at end of file diff --git a/HeroHours/static/js/login.js b/HeroHours/static/js/login.js index e20de79..885fd8b 100644 --- a/HeroHours/static/js/login.js +++ b/HeroHours/static/js/login.js @@ -24,7 +24,7 @@ let username = document.getElementsByName("username")[0]; function updateTime() { const timeDiv = document.querySelector(".time"); const now = new Date(); - const prehours = now.getHours() % 12; + const prehours = now.getHours() === 0 ? 12 : (now.getHours() > 12 ? now.getHours() - 12 : now.getHours()); const hours = prehours.toString().padStart(2, "0"); const minutes = now.getMinutes().toString().padStart(2, "0"); const seconds = now.getSeconds().toString().padStart(2, "0"); @@ -65,6 +65,6 @@ let username = document.getElementsByName("username")[0]; username.focus(); }); document.addEventListener('keydown', function(event) { - if( event.keyCode == 17 || event.keyCode == 74 ) + if (event.key === 'Control' || event.key === 'j') event.preventDefault(); - }); + }); \ No newline at end of file From 1ce340f9a0deb5319c02b3e5b138de226f6ab5b6 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:15:05 +0000 Subject: [PATCH 04/19] bulk and import users --- HeroHours/management/commands/bulk.py | 22 +++++++++---------- HeroHours/management/commands/import_users.py | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/HeroHours/management/commands/bulk.py b/HeroHours/management/commands/bulk.py index 4e02bb4..441ff76 100644 --- a/HeroHours/management/commands/bulk.py +++ b/HeroHours/management/commands/bulk.py @@ -1,23 +1,21 @@ -import csv -import datetime +from datetime import datetime from django.core.management.base import BaseCommand -from HeroHours.models import Users -from HeroHours.views import handle_bulk_updates # Replace 'yourapp' with your actual app name +from HeroHours.views import handle_bulk_updates class Command(BaseCommand): - help = 'Run a comand at a specific time' + help = 'Run a command at a specific time' def add_arguments(self, parser): parser.add_argument('userID', type=str, help='The userID/Command to run') - parser.add_argument('time', type=str, help='The time to use') + parser.add_argument('time', type=str, help='The time to use (format: YYYY MM DD HH MM)') def handle(self, *args, **options): userID = options["userID"] time_string = options["time"].split() - year = time_string[0] - month = time_string[1] - day = time_string[2] - hour = time_string[3] - minute = time_string[4] - handle_bulk_updates(user_id=userID,time=datetime(year,month,day,hour,minute)) + year = int(time_string[0]) + month = int(time_string[1]) + day = int(time_string[2]) + hour = int(time_string[3]) + minute = int(time_string[4]) + handle_bulk_updates(user_id=userID, at_time=datetime(year, month, day, hour, minute)) \ No newline at end of file diff --git a/HeroHours/management/commands/import_users.py b/HeroHours/management/commands/import_users.py index 0211be7..fa37f64 100644 --- a/HeroHours/management/commands/import_users.py +++ b/HeroHours/management/commands/import_users.py @@ -27,4 +27,4 @@ def handle(self, *args, **options): # Bulk create users in the database Users.objects.bulk_create(users) - self.stdout.write(self.style.SUCCESS('Successfully imported users')) + self.stdout.write(self.style.SUCCESS('Successfully imported users')) \ No newline at end of file From 7c8f66ad5060e8f9d27d18a88440a69e735c87f7 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:25:25 +0000 Subject: [PATCH 05/19] api --- HeroHours_api/apps.py | 2 +- HeroHours_api/authentication.py | 2 +- HeroHours_api/urls.py | 10 +++------- HeroHours_api/views.py | 4 ---- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/HeroHours_api/apps.py b/HeroHours_api/apps.py index a8d6a63..8b6e65f 100644 --- a/HeroHours_api/apps.py +++ b/HeroHours_api/apps.py @@ -3,4 +3,4 @@ class HerohoursApiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'HeroHours_api' + name = 'HeroHours_api' \ No newline at end of file diff --git a/HeroHours_api/authentication.py b/HeroHours_api/authentication.py index 7630e2e..f66232f 100644 --- a/HeroHours_api/authentication.py +++ b/HeroHours_api/authentication.py @@ -98,4 +98,4 @@ def get_authorization_key(request): if isinstance(auth, str): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) - return auth + return auth \ No newline at end of file diff --git a/HeroHours_api/urls.py b/HeroHours_api/urls.py index 598605c..ebe0748 100644 --- a/HeroHours_api/urls.py +++ b/HeroHours_api/urls.py @@ -1,11 +1,7 @@ -from django.urls import path, re_path +from django.urls import path +from .views import SheetPullAPI, MeetingPullAPI -from HeroHours_api import views -from .views import ( - SheetPullAPI, - MeetingPullAPI -) urlpatterns = [ path('sheet/users/', SheetPullAPI.as_view(), name='sheet'), - path('sheet////', MeetingPullAPI.as_view(),name='sheet meeting') + path('sheet////', MeetingPullAPI.as_view(), name='sheet meeting'), ] \ No newline at end of file diff --git a/HeroHours_api/views.py b/HeroHours_api/views.py index d35fe51..71fc269 100644 --- a/HeroHours_api/views.py +++ b/HeroHours_api/views.py @@ -1,14 +1,10 @@ -from django.http import HttpResponse from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response - from rest_framework.views import APIView -from rest_framework.settings import api_settings from rest_framework_csv import renderers as csv_renderers from django.db.models import Subquery from HeroHours.models import Users, ActivityLog -from HeroHoursRemake import settings from HeroHours_api.authentication import URLTokenAuthentication From 682a7ebbef6aecacd803958bfe45f7ecdeb0deb5 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:25:34 +0000 Subject: [PATCH 06/19] flake --- flake.lock | 2 +- flake.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.lock b/flake.lock index 28302f3..6b709d0 100644 --- a/flake.lock +++ b/flake.lock @@ -57,4 +57,4 @@ }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/flake.nix b/flake.nix index 3e01fd6..7329433 100644 --- a/flake.nix +++ b/flake.nix @@ -54,4 +54,4 @@ ''; }; }); -} +} \ No newline at end of file From 9821a58e4be676d8ee956c99557a32ca1f2505e3 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:25:38 +0000 Subject: [PATCH 07/19] .env --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 3eba598..b7a8b1d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ - SECRET_KEY = '' APP_SCRIPT_URL = 'https://script.google.com/macros/s/oooooooooooooooooooooooooo/exec' DATABASE_URL = '' -DJANGO_DATABASE = 'default' \ No newline at end of file +DJANGO_DATABASE = 'default' +DEBUG = 'False' \ No newline at end of file From 9c27d08c4d00767c67c89db9f1dd4af637bb0487 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:25:50 +0000 Subject: [PATCH 08/19] herohours --- HeroHours/admin.py | 43 +++++++++++++++++++++--------------------- HeroHours/apps.py | 2 +- HeroHours/consumers.py | 9 +-------- manage.py | 5 ++++- 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/HeroHours/admin.py b/HeroHours/admin.py index 8a8fb89..1a8cb23 100644 --- a/HeroHours/admin.py +++ b/HeroHours/admin.py @@ -1,13 +1,13 @@ import csv import json -from datetime import datetime from types import SimpleNamespace import django.contrib.auth.models as authModels from django.contrib import admin from django.contrib.admin import SimpleListFilter from django.contrib.auth.decorators import user_passes_test -from django.core.exceptions import PermissionDenied, BadRequest +from django.core.exceptions import PermissionDenied +from django.db.models import F, DurationField, ExpressionWrapper from django.forms import model_to_dict from django.http import HttpResponse from django.shortcuts import redirect, render @@ -19,7 +19,6 @@ from .models import Users, ActivityLog from rest_framework.authtoken.admin import TokenAdmin from django.contrib.admin.utils import (unquote) -from django.contrib.admin.options import get_content_type_for_model from django.template.response import TemplateResponse # Register your models here. @@ -31,18 +30,21 @@ def check_out(modeladmin, request, queryset): getall = queryset.filter(Checked_In=True) updated_users = [] updated_log = [] + right_now = timezone.now() for user in getall: lognew = models.ActivityLog( user_id=user.User_ID, + entered=user.User_ID, operation='Check Out', - status='Success', # Initial status + status='Success', ) user.Checked_In = False - user.Total_Hours = ( - datetime.combine(datetime.today(), user.Total_Hours) + (timezone.now() - user.Last_In)).time() - #print((timezone.now() - user.Last_In).total_seconds()) - user.Total_Seconds += round((timezone.now() - user.Last_In).total_seconds()) - user.Last_Out = timezone.now() + if not user.Last_In: + user.Last_In = right_now + user.Total_Hours = ExpressionWrapper(F('Total_Hours') + (right_now - user.Last_In), + output_field=DurationField()) + user.Total_Seconds = F('Total_Seconds') + round((right_now - user.Last_In).total_seconds()) + user.Last_Out = right_now updated_log.append(lognew) updated_users.append(user) models.Users.objects.bulk_update(updated_users, ["Checked_In", "Total_Hours", "Total_Seconds", "Last_Out"]) @@ -53,18 +55,20 @@ def check_out(modeladmin, request, queryset): def check_in(modeladmin, request, queryset): updated_users = [] updated_log = [] + right_now = timezone.now() getall = queryset.filter(Checked_In=False) for user in getall: lognew = models.ActivityLog( user_id=user.User_ID, + entered=user.User_ID, operation='Check In', - status='Success', # Initial status + status='Success', ) user.Checked_In = True - user.Last_In = timezone.now() + user.Last_In = right_now updated_log.append(lognew) updated_users.append(user) - models.Users.objects.bulk_update(updated_users, ["Checked_In", "Total_Hours", "Total_Seconds", "Last_Out"]) + models.Users.objects.bulk_update(updated_users, ["Checked_In", "Last_In"]) models.ActivityLog.objects.bulk_create(updated_log) @admin.action(description="Reset Members") @@ -74,8 +78,9 @@ def reset(modeladmin, request, queryset): for user in queryset: lognew = models.ActivityLog( user_id=user.User_ID, + entered=user.User_ID, operation='Reset', - status='Success', # Initial status + status='Success', ) user.Total_Seconds = 0 user.Total_Hours = '0:00:00' @@ -86,6 +91,7 @@ def reset(modeladmin, request, queryset): updated_log.append( models.ActivityLog( user_id=user.User_ID, + entered=user.User_ID, operation='Check Out', status='Success', ) @@ -96,7 +102,6 @@ def reset(modeladmin, request, queryset): models.ActivityLog.objects.bulk_create(updated_log) def create_staff_user_action(modeladmin, request, queryset): - print(request) selected_user = queryset.first() userdata = model_to_dict(selected_user) @@ -306,7 +311,6 @@ def is_superuser(user): def add_user(request): form_data_dict = request.POST.dict() form_data = SimpleNamespace(**form_data_dict) - print(form_data) username = form_data.username password = form_data.password hidden_data = json.loads(form_data.hidden_data) @@ -314,9 +318,7 @@ def add_user(request): lname = hidden_data['Last_Name'] group_name = form_data.group_name - if authModels.User.objects.filter(username=username).exists(): - print('User already exists') - else: + if not authModels.User.objects.filter(username=username).exists(): user = authModels.User.objects.create_user(username=username, first_name=fname, last_name=lname) @@ -325,11 +327,8 @@ def add_user(request): user.save() group = authModels.Group.objects.get(name=group_name) - print(group) user.groups.add(group) - print('nicely done') - return redirect('/admin/') @@ -339,4 +338,4 @@ def add_user(request): admin.site.register(model_or_iterable=ActivityLog, admin_class=ActivityAdminView) admin.site.site_header = 'HERO Hours Admin' admin.site.site_title = 'HERO Hours Admin' -admin.site.index_title = 'User Administration' +admin.site.index_title = 'User Administration' \ No newline at end of file diff --git a/HeroHours/apps.py b/HeroHours/apps.py index 512a6f7..b87084f 100644 --- a/HeroHours/apps.py +++ b/HeroHours/apps.py @@ -3,4 +3,4 @@ class HeroHoursConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'HeroHours' + name = 'HeroHours' \ No newline at end of file diff --git a/HeroHours/consumers.py b/HeroHours/consumers.py index 9af4f8b..3b49ec8 100644 --- a/HeroHours/consumers.py +++ b/HeroHours/consumers.py @@ -1,16 +1,9 @@ -import json - -from channels.generic.websocket import AsyncWebsocketConsumer -from django.contrib.auth.decorators import permission_required from djangochannelsrestframework.decorators import action from djangochannelsrestframework.observer import model_observer from djangochannelsrestframework.observer.generics import ObserverModelInstanceMixin -from djangochannelsrestframework.permissions import WrappedDRFPermission, IsAuthenticated +from djangochannelsrestframework.permissions import IsAuthenticated from rest_framework import serializers -from rest_framework.permissions import DjangoModelPermissions -from . import models -from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import ( ListModelMixin, RetrieveModelMixin, diff --git a/manage.py b/manage.py index 5d0a384..b1f6603 100644 --- a/manage.py +++ b/manage.py @@ -7,6 +7,9 @@ def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'HeroHoursRemake.settings') + if len(sys.argv) >= 2 and sys.argv[1] == 'runserver': + if not any(arg.startswith(('127.0.0.1:', 'localhost:', '0.0.0.0:')) for arg in sys.argv[2:]): + sys.argv.append('127.0.0.1:5892') try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -19,4 +22,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From 5c3f2f379be8e5414497bb08134901b5e063aed4 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:26:10 +0000 Subject: [PATCH 09/19] more herohours+requirements --- HeroHours/forms.py | 3 +- HeroHours/models.py | 20 +++++----- HeroHours/routing.py | 2 +- HeroHours/tests.py | 2 +- HeroHours/urls.py | 15 ++++--- HeroHours/views.py | 82 +++++++++++++-------------------------- HeroHoursRemake/celery.py | 2 +- requirements.txt | 14 +------ 8 files changed, 52 insertions(+), 88 deletions(-) diff --git a/HeroHours/forms.py b/HeroHours/forms.py index 5455db6..1f6e47c 100644 --- a/HeroHours/forms.py +++ b/HeroHours/forms.py @@ -3,7 +3,6 @@ class CustomActionForm(forms.Form): - print("running form") username = forms.CharField(label='Username', max_length=100) password = forms.CharField(label='Password', widget=forms.PasswordInput) group_name = forms.ChoiceField(label='Group Name', required=True) @@ -16,4 +15,4 @@ def __init__(self, *args, **kwargs): self.fields['group_name'].choices = group_choices # Set the choices for the field - hidden_data = forms.CharField(widget=forms.HiddenInput(), required=False) + hidden_data = forms.CharField(widget=forms.HiddenInput(), required=False) \ No newline at end of file diff --git a/HeroHours/models.py b/HeroHours/models.py index 25984d5..324bfea 100644 --- a/HeroHours/models.py +++ b/HeroHours/models.py @@ -15,7 +15,6 @@ class Users(models.Model): def get_total_hours(self): - #print(f"Total Seconds: {self.Total_Seconds}") hours, remainder = divmod(int(self.Total_Seconds), 3600) minutes, seconds = divmod(remainder, 60) return f"{hours}h {minutes}m {seconds}s" @@ -32,22 +31,23 @@ def __str__(self): class ActivityLog(models.Model): OPERATION_CHOICES = [ - ('checkIn', 'Check In'), - ('checkOut', 'Check Out'), - ('none', "None"), - ('autoCheckOut','Auto Check Out'), + ('Check In', 'Check In'), + ('Check Out', 'Check Out'), + ('None', "None"), + ('Auto Check Out', 'Auto Check Out'), + ('Reset', 'Reset'), ] STATUS_CHOICES = [ - ('success', 'Success'), - ('error', 'Error'), - ('user not found', 'User Not Found'), - ('inactive user', 'Inactive User'), + ('Success', 'Success'), + ('Error', 'Error'), + ('User Not Found', 'User Not Found'), + ('Inactive User', 'Inactive User'), ] user = models.ForeignKey(Users, models.CASCADE, blank=True, null=True) entered = models.TextField() - operation = models.CharField(max_length=14, choices=OPERATION_CHOICES) + operation = models.CharField(max_length=15, choices=OPERATION_CHOICES) status = models.CharField(max_length=20, choices=STATUS_CHOICES) message = models.TextField(default='') # Optional message field timestamp = models.DateTimeField(auto_now_add=True) # Automatically set the timestamp when creating diff --git a/HeroHours/routing.py b/HeroHours/routing.py index cb86451..b17041a 100644 --- a/HeroHours/routing.py +++ b/HeroHours/routing.py @@ -4,4 +4,4 @@ websocket_urlpatterns = [ path('ws/live/',LiveConsumer.as_asgi()), -] +] \ No newline at end of file diff --git a/HeroHours/tests.py b/HeroHours/tests.py index 7ce503c..c2629a3 100644 --- a/HeroHours/tests.py +++ b/HeroHours/tests.py @@ -1,3 +1,3 @@ from django.test import TestCase -# Create your tests here. +# Create your tests here. \ No newline at end of file diff --git a/HeroHours/urls.py b/HeroHours/urls.py index cf8c453..652e802 100644 --- a/HeroHours/urls.py +++ b/HeroHours/urls.py @@ -1,7 +1,7 @@ -import debug_toolbar +from django.conf import settings from django.contrib.auth.views import LoginView -from django.urls import path, include, re_path -from . import views, consumers +from django.urls import path, include +from . import views from .admin import add_user @@ -10,8 +10,13 @@ path("insert/", views.handle_entry, name='in-out'), path("send_data_to_google_sheet/",views.send_data_to_google_sheet,name='send_data_to_google_sheet'), path('login/', LoginView.as_view(template_name='login.html'), name='login'), - path('__debug__/', include(debug_toolbar.urls)), path('custom/', add_user,name='custom'), path('pull_sheet/',views.sheet_pull,name='pull_sheet'), path('live/', views.live_view, name='live_view'), -] \ No newline at end of file +] + +if settings.DEBUG: + import debug_toolbar + urlpatterns += [ + path('__debug__/', include(debug_toolbar.urls)), + ] \ No newline at end of file diff --git a/HeroHours/views.py b/HeroHours/views.py index 2ca5df8..e963a7d 100644 --- a/HeroHours/views.py +++ b/HeroHours/views.py @@ -1,21 +1,19 @@ import base64 import json -import time +import logging from datetime import timedelta import requests import os from django.contrib.auth import authenticate, logout -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import permission_required from django.core.exceptions import BadRequest, PermissionDenied from django.db.models import F, DurationField, ExpressionWrapper from django.shortcuts import render, redirect from django.utils import timezone from django.core import serializers -from django.utils.http import urlsafe_base64_decode from dotenv import load_dotenv, find_dotenv -from kombu.exceptions import HttpError from . import models from django.http import JsonResponse, HttpResponse @@ -23,6 +21,8 @@ load_dotenv(find_dotenv()) +logger = logging.getLogger(__name__) + # Create your views here. @permission_required("HeroHours.change_users") @@ -30,9 +30,7 @@ def index(request): # Query all users from the database usersData = models.Users.objects.filter(Is_Active=True).order_by('Last_Name','First_Name') users_checked_in = models.Users.objects.filter(Checked_In=True).count() - local_log_entries = models.ActivityLog.objects.all()[:9] #limits to loading only 9 entries - #print(local_log_entries) - print(timezone.now()) + local_log_entries = models.ActivityLog.objects.all()[:9] # Pass the users data to the template return render(request, 'members.html', @@ -41,21 +39,15 @@ def index(request): @permission_required("HeroHours.change_users", raise_exception=True) def handle_entry(request): - start_time = time.time() user_input = request.POST.get('user_input') right_now = timezone.now() - #profiler = cProfile.Profile() - #profiler.enable() # Handle special commands first - if handle_special_commands(user_input): - elapsed_time = time.time() - start_time - print(f"input(before) execution time: {elapsed_time:.4f} seconds") - return handle_special_commands(user_input) + special_result = handle_special_commands(user_input) + if special_result: + return special_result if user_input in ['-404', '+404']: - elapsed_time = time.time() - start_time - print(f"input(before) execution time: {elapsed_time:.4f} seconds") return handle_bulk_updates(user_input) if user_input == "---": logout(request) @@ -78,42 +70,30 @@ def handle_entry(request): 'count': count}) except Exception as e: return JsonResponse({'status': "Error", 'newlog': {'userID': user_input, 'operation': "None", 'status': 'Error', - 'message': e.__str__()}, 'state': None, 'count': count}) + 'message': str(e)}, 'state': None, 'count': count}) # Perform Check-In or Check-Out operations - elapsed_time = time.time() - start_time - print(f"input(before) execution time: {elapsed_time:.4f} seconds") operation_result = check_in_or_out(user, right_now, log, count) - print(timezone.now()) - #profiler.disable() - #stats = pstats.Stats(profiler) - #stats.strip_dirs() - #stats.sort_stats('cumulative').print_stats(10) # Return JSON response with status and user info return JsonResponse(operation_result) def handle_special_commands(user_id): - start_time = time.time() if user_id == "Send": - elapsed_time = time.time() - start_time - print(f"input(send) execution time: {elapsed_time:.4f} seconds") return redirect('send_data_to_google_sheet') if user_id in ['+00', '+01', '*']: - elapsed_time = time.time() - start_time - print(f"input(special) execution time: {elapsed_time:.4f} seconds") return redirect('index') if user_id == "admin": - elapsed_time = time.time() - start_time - print(f"input(admin) execution time: {elapsed_time:.4f} seconds") return redirect('/admin/') + return None + -def handle_bulk_updates(user_id, time = None): - if time == None: - time = timezone.now() +def handle_bulk_updates(user_id, at_time=None): + if at_time is None: + at_time = timezone.now() updated_users = [] updated_log = [] @@ -130,33 +110,32 @@ def handle_bulk_updates(user_id, time = None): if user_id == '-404': user.Checked_In = True - user.Last_In = time + user.Last_In = at_time else: if not user.Last_In: - user.Last_In = time + user.Last_In = at_time user.Checked_In = False threshold = int(os.environ.get('AUTO_LOGOUT_THRESHOLD_SECONDS',3600)) - if (time - user.Last_In) > timedelta(seconds=threshold): - user.Total_Hours = ExpressionWrapper(F('Total_Hours') + ((time-timedelta(seconds=threshold)) - user.Last_In), + if (at_time - user.Last_In) > timedelta(seconds=threshold): + user.Total_Hours = ExpressionWrapper(F('Total_Hours') + ((at_time-timedelta(seconds=threshold)) - user.Last_In), output_field=DurationField()) - user.Total_Seconds = F('Total_Seconds') + round(((time-timedelta(seconds=threshold)) - user.Last_In).total_seconds()) + user.Total_Seconds = F('Total_Seconds') + round(((at_time-timedelta(seconds=threshold)) - user.Last_In).total_seconds()) else: - user.Total_Hours = ExpressionWrapper(F('Total_Hours') + (time - user.Last_In), + user.Total_Hours = ExpressionWrapper(F('Total_Hours') + (at_time - user.Last_In), output_field=DurationField()) - user.Total_Seconds = F('Total_Seconds') + round((time - user.Last_In).total_seconds()) - user.Last_Out = time + user.Total_Seconds = F('Total_Seconds') + round((at_time - user.Last_In).total_seconds()) + user.Last_Out = at_time updated_log.append(log) updated_users.append(user) - models.Users.objects.bulk_update(updated_users, ["Checked_In", "Total_Hours", "Total_Seconds", "Last_Out"]) + models.Users.objects.bulk_update(updated_users, ["Checked_In", "Total_Hours", "Total_Seconds", "Last_In", "Last_Out"]) models.ActivityLog.objects.bulk_create(updated_log) # Redirect to index after bulk updates return redirect('index') def check_in_or_out(user, right_now, log, count): - start_time = time.time() count2=count if user.Checked_In: count2 -= 1 @@ -176,7 +155,7 @@ def check_in_or_out(user, right_now, log, count): user.Checked_In = not user.Checked_In log.status = 'Success' - operation = "Check Out" if not state else "Success" + operation = "Check Out" if not state else "Check In" if not user.Is_Active: log.operation = "None" state = None @@ -187,8 +166,6 @@ def check_in_or_out(user, right_now, log, count): # Save log and user updates log.save() - elapsed_time = time.time() - start_time - print(f"input(in or out) execution time: {elapsed_time:.4f} seconds") return { 'status': operation, 'state': state, @@ -197,7 +174,7 @@ def check_in_or_out(user, right_now, log, count): } -APP_SCRIPT_URL = os.environ['APP_SCRIPT_URL'] +APP_SCRIPT_URL = os.environ.get('APP_SCRIPT_URL', '') @permission_required("HeroHours.change_users", raise_exception=True) @@ -205,7 +182,6 @@ def send_data_to_google_sheet(request): users = models.Users.objects.all() serialized_data = serializers.serialize('json', users, use_natural_foreign_keys=True) serialized_data2 = serializers.serialize('json', models.ActivityLog.objects.all(), use_natural_foreign_keys=True) - print(serialized_data) together = [serialized_data, serialized_data2] all_data = json.dumps(obj=together) count = users.filter(Checked_In=True).count() @@ -213,17 +189,14 @@ def send_data_to_google_sheet(request): # Send POST request to the Apps Script API try: response = requests.post(APP_SCRIPT_URL, json=json.loads(all_data)) - print(response) # Handle the response (for example, check if it was successful) if response.status_code == 200: result = response.json() - print(result) return JsonResponse({'status': 'Sent', 'result': result, 'count': count}) else: return JsonResponse({'status': 'Sent', 'message': 'Failed to send data', 'count': count}) except Exception as e: - print("failed") - print(e) + logger.error("Failed to send data to Google Sheet: %s", e) return JsonResponse({'status': 'error', 'message': str(e), 'count': count}) def sheet_pull(request): key = request.GET.get('key') @@ -233,12 +206,11 @@ def sheet_pull(request): username, password = base64.b64decode(key).decode('ascii').split(":") user = authenticate(request, username=username, password=password) if not user: - print(user) raise PermissionDenied() members = models.Users.objects.all() response = 'User_ID,First_Name,Last_Name,Total_Hours,Total_Seconds,Last_In,Last_Out,Is_Active,\n' for member in members: - response += f"{member.User_ID},{member.First_Name},{member.Last_Name},{member.get_p()},{member.Total_Seconds},{member.Last_In},{member.Last_Out},{member.Is_Active}\n" + response += f"{member.User_ID},{member.First_Name},{member.Last_Name},{member.get_total_hours()},{member.Total_Seconds},{member.Last_In},{member.Last_Out},{member.Is_Active}\n" return HttpResponse(response,content_type='text/csv') diff --git a/HeroHoursRemake/celery.py b/HeroHoursRemake/celery.py index c97a1a9..b2e4e4e 100644 --- a/HeroHoursRemake/celery.py +++ b/HeroHoursRemake/celery.py @@ -14,4 +14,4 @@ @app.task(bind=True) def debug_task(self): - print(f'Request: {self.request!r}') + print(f'Request: {self.request!r}') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1763583..37371cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,7 @@ async-timeout==4.0.3 attrs==25.4.0 autobahn==25.12.2 Automat==25.4.16 -automium==0.2.6 -automium_web==0.1.1 billiard==4.2.1 -blinker==1.8.2 cbor2==5.8.0 celery==5.4.0 certifi==2024.8.30 @@ -18,27 +15,19 @@ click==8.1.7 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 -colorama==0.4.6 constantly==23.10.4 cryptography==46.0.3 daphne==4.2.1 dj-database-url==2.2.0 Django==5.2.9 django-debug-toolbar==4.4.6 -django-sslserver==0.22 djangochannelsrestframework==1.3.0 djangorestframework==3.15.2 djangorestframework-csv==3.0.2 -Flask==3.0.3 -gunicorn==23.0.0 hyperlink==21.0.0 idna==3.10 Incremental==24.11.0 -itsdangerous==2.2.0 -Jinja2==3.1.4 kombu==5.4.2 -legacy==0.1.7 -MarkupSafe==2.1.5 msgpack==1.1.2 packaging==24.1 prompt_toolkit==3.0.48 @@ -65,6 +54,5 @@ ujson==5.11.0 urllib3==2.2.3 vine==5.1.0 wcwidth==0.2.13 -Werkzeug==3.0.4 whitenoise==6.7.0 -zope.interface==8.1.1 +zope.interface==8.1.1 \ No newline at end of file From 39a4d890ac8ab9ba69d511dd0acb67d3bb96a037 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 16:26:14 +0000 Subject: [PATCH 10/19] rest --- HeroHoursRemake/settings.py | 25 ++++++++++++++++++------- HeroHoursRemake/urls.py | 2 +- HeroHoursRemake/wsgi.py | 2 +- Procfile | 5 +---- templates/admin/custom_action_form.html | 2 +- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/HeroHoursRemake/settings.py b/HeroHoursRemake/settings.py index 9e3393e..344443c 100644 --- a/HeroHoursRemake/settings.py +++ b/HeroHoursRemake/settings.py @@ -46,7 +46,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'HeroHours.apps.HeroHoursConfig', - 'debug_toolbar', + 'HeroHours_api.apps.HerohoursApiConfig', 'rest_framework', 'rest_framework.authtoken', ] @@ -54,16 +54,18 @@ LOGIN_URL = '/HeroHours/login/' MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - #'HeroHours.middleware.TimeItMiddleware', - 'debug_toolbar.middleware.DebugToolbarMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', ] + +if DEBUG: + INSTALLED_APPS += ['debug_toolbar'] + MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] INTERNAL_IPS = ['127.0.0.1'] ROOT_URLCONF = 'HeroHoursRemake.urls' @@ -101,7 +103,7 @@ 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', }, - 'default': dj_database_url.config(default=os.environ['DATABASE_URL'], conn_max_age=600, ssl_require=not DEBUG) + 'default': dj_database_url.config(default=os.environ.get('DATABASE_URL', ''), conn_max_age=600, ssl_require=not DEBUG) } default_database = os.environ.get('DJANGO_DATABASE', 'default') DATABASES['default'] = DATABASES[default_database] @@ -144,7 +146,11 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') -STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') @@ -156,4 +162,9 @@ CSRF_TRUSTED_ORIGINS = ['https://hero-hours-2bf608a75758.herokuapp.com'] SECURE_SSL_REDIRECT = not DEBUG APPEND_SLASH = True -SESSION_COOKIE_AGE=39600 +SESSION_COOKIE_AGE = 39600 + +# Security settings for production +SESSION_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SECURE = not DEBUG +SECURE_CONTENT_TYPE_NOSNIFF = True \ No newline at end of file diff --git a/HeroHoursRemake/urls.py b/HeroHoursRemake/urls.py index e453bd0..6e3c210 100644 --- a/HeroHoursRemake/urls.py +++ b/HeroHoursRemake/urls.py @@ -33,4 +33,4 @@ def root_redirect(request): path('admin/', admin.site.urls), path('test/', home), path('', root_redirect), -] +] \ No newline at end of file diff --git a/HeroHoursRemake/wsgi.py b/HeroHoursRemake/wsgi.py index c160810..ec0203f 100644 --- a/HeroHoursRemake/wsgi.py +++ b/HeroHoursRemake/wsgi.py @@ -13,4 +13,4 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'HeroHoursRemake.settings') -application = get_wsgi_application() +application = get_wsgi_application() \ No newline at end of file diff --git a/Procfile b/Procfile index c9a9b82..ed44585 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1 @@ -web: daphne HeroHoursRemake.asgi:application --port $PORT --bind 0.0.0.0 --proxy-headers -heroku run python manage.py collectstatic -heroku run python manage.py makemigrations -heroku run python manage.py migrate +web: daphne HeroHoursRemake.asgi:application --port $PORT --bind 0.0.0.0 --proxy-headers \ No newline at end of file diff --git a/templates/admin/custom_action_form.html b/templates/admin/custom_action_form.html index 8f56be0..3779ef0 100644 --- a/templates/admin/custom_action_form.html +++ b/templates/admin/custom_action_form.html @@ -18,4 +18,4 @@

Enter Information for Creating a Staff User

}) {{ admin_tools }} -{% endblock %} +{% endblock %} \ No newline at end of file From 4a79669b70c19ee63521b192bca4dcf10dd2ed05 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:37:31 -0600 Subject: [PATCH 11/19] Alter fields and add indexes for activitylog and users This migration modifies several fields in the activitylog and users models, including altering field types and adding new indexes for improved query performance. --- ...message_alter_activitylog_user_and_more.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 HeroHours/migrations/0022_alter_activitylog_message_alter_activitylog_user_and_more.py diff --git a/HeroHours/migrations/0022_alter_activitylog_message_alter_activitylog_user_and_more.py b/HeroHours/migrations/0022_alter_activitylog_message_alter_activitylog_user_and_more.py new file mode 100644 index 0000000..62256bc --- /dev/null +++ b/HeroHours/migrations/0022_alter_activitylog_message_alter_activitylog_user_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 5.2.11 on 2026-02-15 17:08 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('HeroHours', '0021_alter_activitylog_operation_alter_activitylog_status'), + ] + + operations = [ + migrations.AlterField( + model_name='activitylog', + name='message', + field=models.TextField(blank=True, default=''), + ), + migrations.AlterField( + model_name='activitylog', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity_logs', to='HeroHours.users'), + ), + migrations.AlterField( + model_name='users', + name='Last_In', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='users', + name='Last_Out', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='users', + name='Total_Seconds', + field=models.FloatField(default=0, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AddIndex( + model_name='activitylog', + index=models.Index(fields=['-timestamp'], name='HeroHours_a_timesta_b5495e_idx'), + ), + migrations.AddIndex( + model_name='activitylog', + index=models.Index(fields=['user', '-timestamp'], name='HeroHours_a_user_id_cb5e0f_idx'), + ), + migrations.AddIndex( + model_name='activitylog', + index=models.Index(fields=['operation', 'status'], name='HeroHours_a_operati_d5bf01_idx'), + ), + migrations.AddIndex( + model_name='users', + index=models.Index(fields=['Last_Name', 'First_Name'], name='Users_Last_Na_e25c91_idx'), + ), + migrations.AddIndex( + model_name='users', + index=models.Index(fields=['Checked_In'], name='Users_Checked_6fa301_idx'), + ), + migrations.AddIndex( + model_name='users', + index=models.Index(fields=['Is_Active'], name='Users_Is_Acti_bc9d4d_idx'), + ), + ] From 2bda47fad1fd433e456e71620ef1a07b44007000 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:40:37 -0600 Subject: [PATCH 12/19] Refactor admin.py for better structure and functionality Reorganize imports and enhance admin actions for clarity. --- HeroHours/admin.py | 51 ++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/HeroHours/admin.py b/HeroHours/admin.py index 1a8cb23..0473c8d 100644 --- a/HeroHours/admin.py +++ b/HeroHours/admin.py @@ -1,25 +1,29 @@ +# Standard library imports import csv import json from types import SimpleNamespace +# Third-party imports import django.contrib.auth.models as authModels from django.contrib import admin from django.contrib.admin import SimpleListFilter +from django.contrib.admin.utils import unquote from django.contrib.auth.decorators import user_passes_test from django.core.exceptions import PermissionDenied -from django.db.models import F, DurationField, ExpressionWrapper +from django.db.models import DurationField, ExpressionWrapper, F from django.forms import model_to_dict from django.http import HttpResponse from django.shortcuts import redirect, render +from django.template.response import TemplateResponse from django.utils import timezone -from django.utils.translation import gettext_lazy as _ from django.utils.text import capfirst -from HeroHours.forms import CustomActionForm -from . import models -from .models import Users, ActivityLog +from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.admin import TokenAdmin -from django.contrib.admin.utils import (unquote) -from django.template.response import TemplateResponse + +# Local imports +from . import models +from .forms import CustomActionForm +from .models import ActivityLog, Users # Register your models here. @@ -101,6 +105,7 @@ def reset(modeladmin, request, queryset): models.Users.objects.bulk_update(updated_users, ["Checked_In", "Total_Hours", "Total_Seconds", "Last_Out", "Last_In"]) models.ActivityLog.objects.bulk_create(updated_log) +@admin.action(description="Create a Staff User") def create_staff_user_action(modeladmin, request, queryset): selected_user = queryset.first() userdata = model_to_dict(selected_user) @@ -110,9 +115,6 @@ def create_staff_user_action(modeladmin, request, queryset): return render(request, 'admin/custom_action_form.html', {'form': form}) -create_staff_user_action.short_description = "Create a Staff User" - - class TotalHoursFilter(SimpleListFilter): title = _('Total Hours') # Display title in the admin filter sidebar parameter_name = 'total_hours' # URL parameter @@ -126,7 +128,7 @@ def lookups(self, request, model_admin): ('25hours', _('Less than 25 hours')), ('o25hours', _('Over 25 hours')), - ('o50hours',_('Over 50 hours')) + ('o50hours', _('Over 50 hours')) ] def queryset(self, request, queryset): @@ -157,12 +159,12 @@ def export_as_csv(self, request, queryset): field_names = [field.name for field in meta.fields] response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) + response['Content-Disposition'] = f'attachment; filename={meta}.csv' writer = csv.writer(response) writer.writerow(field_names) for obj in queryset: - row = writer.writerow([getattr(obj, field) for field in field_names]) + writer.writerow([getattr(obj, field) for field in field_names]) return response @@ -175,12 +177,10 @@ class MemberAdmin(admin.ModelAdmin): search_fields = ['User_ID', 'Last_Name', 'First_Name'] list_filter = ['Checked_In', TotalHoursFilter] + @admin.display(description="Total Hours", ordering="Total_Seconds") def display_total_hours(self, obj): return obj.get_total_hours() - display_total_hours.short_description = "Total Hours" - display_total_hours.admin_order_field = "Total_Seconds" - """ Custom history view, modified from Django source @@ -277,31 +277,28 @@ class ActivityAdminView(admin.ModelAdmin): search_fields = ['timestamp'] actions = [export_as_csv] + @admin.display(description='Date') def get_date_only(self, obj): return timezone.localtime(obj.timestamp).date() + + @admin.display(description='Entered') def get_entered_data(self, obj): return obj.entered - get_entered_data.short_description = 'Entered' - - - get_date_only.short_description = 'Date' + @admin.display(description='Name') def get_name(self, obj): if obj.user: return f'{obj.user.First_Name} {obj.user.Last_Name}' - return 'None' - get_name.short_description = 'Name' + return 'None' + @admin.display(description='Status') def get_status(self, obj): return obj.status - get_status.short_description = 'Status' - + @admin.display(description='Operation') def get_op(self, obj): return obj.operation - get_op.short_description = 'Operation' - def is_superuser(user): return user.is_superuser @@ -338,4 +335,4 @@ def add_user(request): admin.site.register(model_or_iterable=ActivityLog, admin_class=ActivityAdminView) admin.site.site_header = 'HERO Hours Admin' admin.site.site_title = 'HERO Hours Admin' -admin.site.index_title = 'User Administration' \ No newline at end of file +admin.site.index_title = 'User Administration' From cffe76c4b67398812fc1273159e49d3707328a29 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:41:16 -0600 Subject: [PATCH 13/19] Enhance documentation for serializers and WebSocket consumer Added docstrings to MemberSerializer and LiveConsumer classes, as well as update_activity method, to clarify their purposes and usage. --- HeroHours/consumers.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/HeroHours/consumers.py b/HeroHours/consumers.py index 3b49ec8..fc23c1b 100644 --- a/HeroHours/consumers.py +++ b/HeroHours/consumers.py @@ -14,10 +14,19 @@ class MemberSerializer(serializers.ModelSerializer): + """Serializer for Users model used in WebSocket communications.""" class Meta: model = Users fields = ['User_ID', 'First_Name', 'Last_Name', 'Checked_In', 'Total_Seconds', 'Last_In', 'Last_Out'] + + class LiveConsumer(ObserverModelInstanceMixin, RetrieveModelMixin, ListModelMixin, GenericAsyncAPIConsumer): + """ + WebSocket consumer for real-time member updates. + + Provides live updates when members check in/out via the update_activity observer. + Requires authentication to connect. + """ queryset = Users.objects.all().order_by('Last_Name','First_Name') serializer_class = MemberSerializer permission_classes = [IsAuthenticated] @@ -30,11 +39,31 @@ async def update_activity( subscribing_request_ids=[], **kwargs ): + """ + Observer that sends updates when Users model changes. + + Args: + message: Serialized user data + observer: Observer instance + subscribing_request_ids: List of request IDs subscribed to updates + """ await self.send_json({'data':message.data, 'request_ids':subscribing_request_ids}) @update_activity.serializer def update_activity(self, instance: Users, action, **kwargs) -> MemberSerializer: - """This will return the comment serializer""" + """ + Serializer for the update_activity observer. + + Refreshes instance from database if any field contains F() expressions + to ensure accurate data is sent to subscribers. + + Args: + instance: Users model instance that changed + action: Type of change (create, update, delete) + + Returns: + MemberSerializer: Serialized user data + """ for field in instance._meta.fields: if isinstance(getattr(instance, field.name), BaseExpression): instance.refresh_from_db() @@ -43,4 +72,10 @@ def update_activity(self, instance: Users, action, **kwargs) -> MemberSerializer @action() async def subscribe_all(self, request_id, **kwargs): - await self.update_activity.subscribe(request_id=request_id) \ No newline at end of file + """ + Action to subscribe to all Users model updates. + + Args: + request_id: Unique request identifier for this subscription + """ + await self.update_activity.subscribe(request_id=request_id) From fdc6aee60a72ef21721559d21e59ec180476704f Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:42:29 -0600 Subject: [PATCH 14/19] Enhance Users and ActivityLog models with validations Added validators and improved model fields for Users and ActivityLog. --- HeroHours/models.py | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/HeroHours/models.py b/HeroHours/models.py index 324bfea..27dfc86 100644 --- a/HeroHours/models.py +++ b/HeroHours/models.py @@ -1,20 +1,31 @@ from django.db import models +from django.core.validators import MinValueValidator # Create your models here. class Users(models.Model): + """ + Model representing a HERO member with check-in/check-out tracking. + + Tracks volunteer hours, check-in status, and activity for each member. + """ User_ID = models.IntegerField(primary_key=True) First_Name = models.CharField(max_length=50) Last_Name = models.CharField(max_length=50) Total_Hours = models.DurationField() Checked_In = models.BooleanField(default=False) - Total_Seconds = models.FloatField(default=0) - Last_In = models.DateTimeField(null=True) - Last_Out = models.DateTimeField(null=True) + Total_Seconds = models.FloatField(default=0, validators=[MinValueValidator(0)]) + Last_In = models.DateTimeField(null=True, blank=True) + Last_Out = models.DateTimeField(null=True, blank=True) Is_Active = models.BooleanField(default=True) - - def get_total_hours(self): + def get_total_hours(self) -> str: + """ + Format total seconds as a human-readable hours/minutes/seconds string. + + Returns: + str: Formatted string like "10h 30m 45s" + """ hours, remainder = divmod(int(self.Total_Seconds), 3600) minutes, seconds = divmod(remainder, 60) return f"{hours}h {minutes}m {seconds}s" @@ -24,12 +35,22 @@ class Meta: db_table = 'Users' verbose_name = "Members" verbose_name_plural = "Members" + indexes = [ + models.Index(fields=['Last_Name', 'First_Name']), + models.Index(fields=['Checked_In']), + models.Index(fields=['Is_Active']), + ] def __str__(self): return f"{self.First_Name} {self.Last_Name}: {self.User_ID} - {self.Total_Hours}" class ActivityLog(models.Model): + """ + Model representing activity log entries for check-in/check-out operations. + + Tracks all user interactions including check-ins, check-outs, and errors. + """ OPERATION_CHOICES = [ ('Check In', 'Check In'), ('Check Out', 'Check Out'), @@ -45,15 +66,20 @@ class ActivityLog(models.Model): ('Inactive User', 'Inactive User'), ] - user = models.ForeignKey(Users, models.CASCADE, blank=True, null=True) + user = models.ForeignKey(Users, models.CASCADE, blank=True, null=True, related_name='activity_logs') entered = models.TextField() operation = models.CharField(max_length=15, choices=OPERATION_CHOICES) status = models.CharField(max_length=20, choices=STATUS_CHOICES) - message = models.TextField(default='') # Optional message field + message = models.TextField(default='', blank=True) # Optional message field timestamp = models.DateTimeField(auto_now_add=True) # Automatically set the timestamp when creating def __str__(self): return f"{self.user_id} - {self.operation} - {self.status} - {self.timestamp}" class Meta: - ordering = ['-timestamp'] # Order by most recent logs first \ No newline at end of file + ordering = ['-timestamp'] # Order by most recent logs first + indexes = [ + models.Index(fields=['-timestamp']), + models.Index(fields=['user', '-timestamp']), + models.Index(fields=['operation', 'status']), + ] From f7157122480aa2a0a9f811bfd31d5216bd1b4d51 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:42:58 -0600 Subject: [PATCH 15/19] Refactor views.py with type hints and rate limiting Refactor views in HeroHours to include type hints and improve code structure. Added rate limiting to specific views and updated variable names for consistency. --- HeroHours/views.py | 140 ++++++++++++++++++++++++++++++++------------- 1 file changed, 101 insertions(+), 39 deletions(-) diff --git a/HeroHours/views.py b/HeroHours/views.py index e963a7d..868a163 100644 --- a/HeroHours/views.py +++ b/HeroHours/views.py @@ -1,23 +1,25 @@ -import base64 +# Standard library imports import json import logging -from datetime import timedelta - -import requests import os +from datetime import datetime, timedelta +from typing import Optional -from django.contrib.auth import authenticate, logout +# Third-party imports +import requests +from django.contrib.auth import logout from django.contrib.auth.decorators import permission_required -from django.core.exceptions import BadRequest, PermissionDenied -from django.db.models import F, DurationField, ExpressionWrapper -from django.shortcuts import render, redirect -from django.utils import timezone from django.core import serializers -from dotenv import load_dotenv, find_dotenv +from django.db.models import DurationField, ExpressionWrapper, F +from django.forms.models import model_to_dict +from django.http import HttpRequest, HttpResponse, JsonResponse +from django.shortcuts import redirect, render +from django.utils import timezone +from django_ratelimit.decorators import ratelimit +from dotenv import find_dotenv, load_dotenv +# Local imports from . import models -from django.http import JsonResponse, HttpResponse -from django.forms.models import model_to_dict load_dotenv(find_dotenv()) @@ -26,20 +28,38 @@ # Create your views here. @permission_required("HeroHours.change_users") -def index(request): +def index(request: HttpRequest) -> HttpResponse: + """ + Main dashboard view displaying active members and check-in status. + + Args: + request: HTTP request object + + Returns: + HttpResponse: Rendered members.html template with user data + """ # Query all users from the database - usersData = models.Users.objects.filter(Is_Active=True).order_by('Last_Name','First_Name') + users_data = models.Users.objects.filter(Is_Active=True).order_by('Last_Name', 'First_Name') users_checked_in = models.Users.objects.filter(Checked_In=True).count() local_log_entries = models.ActivityLog.objects.all()[:9] # Pass the users data to the template return render(request, 'members.html', - {'usersData': usersData, "checked_in": users_checked_in, 'local_log_entries': local_log_entries}) + {'usersData': users_data, "checked_in": users_checked_in, 'local_log_entries': local_log_entries}) @permission_required("HeroHours.change_users", raise_exception=True) -def handle_entry(request): - user_input = request.POST.get('user_input') +@ratelimit(key='user', rate='60/m', method='POST') +def handle_entry(request: HttpRequest) -> JsonResponse: + user_input = request.POST.get('user_input', '').strip() + + # Input validation: limit length and sanitize + if not user_input: + return JsonResponse({'status': 'Error', 'message': 'No input provided'}) + + if len(user_input) > 100: + return JsonResponse({'status': 'Error', 'message': 'Input too long'}) + right_now = timezone.now() # Handle special commands first @@ -69,8 +89,9 @@ def handle_entry(request): {'status': 'User Not Found', 'user_id': None, 'operation': None, 'newlog': model_to_dict(log), 'count': count}) except Exception as e: + logger.error(f"Error in handle_entry: {str(e)}") return JsonResponse({'status': "Error", 'newlog': {'userID': user_input, 'operation': "None", 'status': 'Error', - 'message': str(e)}, 'state': None, 'count': count}) + 'message': 'An error occurred'}, 'state': None, 'count': count}) # Perform Check-In or Check-Out operations operation_result = check_in_or_out(user, right_now, log, count) @@ -78,7 +99,16 @@ def handle_entry(request): return JsonResponse(operation_result) -def handle_special_commands(user_id): +def handle_special_commands(user_id: str) -> Optional[HttpResponse]: + """ + Process special command inputs like 'Send', 'admin', etc. + + Args: + user_id: Input string from user + + Returns: + HttpResponse or None: Redirect response if special command, None otherwise + """ if user_id == "Send": return redirect('send_data_to_google_sheet') @@ -91,7 +121,17 @@ def handle_special_commands(user_id): return None -def handle_bulk_updates(user_id, at_time=None): +def handle_bulk_updates(user_id: str, at_time: Optional[datetime] = None) -> HttpResponse: + """ + Bulk check-in or check-out all users (DEBUG mode only for check-in). + + Args: + user_id: '-404' for bulk check-in, '+404' for auto check-out + at_time: Optional datetime for the operation, defaults to now + + Returns: + HttpResponse: Redirect to index page + """ if at_time is None: at_time = timezone.now() updated_users = [] @@ -135,10 +175,22 @@ def handle_bulk_updates(user_id, at_time=None): return redirect('index') -def check_in_or_out(user, right_now, log, count): - count2=count +def check_in_or_out(user: models.Users, right_now: datetime, log: models.ActivityLog, count: int) -> dict: + """ + Toggle user check-in status and update hours. + + Args: + user: Users model instance + right_now: Current datetime + log: ActivityLog instance to save + count: Current count of checked-in users + + Returns: + dict: Status information including operation, state, log, and count + """ + new_count = count if user.Checked_In: - count2 -= 1 + new_count -= 1 state = False log.operation = 'Check Out' if not user.Last_In: @@ -148,7 +200,7 @@ def check_in_or_out(user, right_now, log, count): user.Total_Seconds = F('Total_Seconds') + round((right_now - user.Last_In).total_seconds()) user.Last_Out = right_now else: - count2 += 1 + new_count += 1 state = True log.operation = 'Check In' user.Last_In = right_now @@ -161,7 +213,7 @@ def check_in_or_out(user, right_now, log, count): state = None log.status = "Inactive User" else: - count = count2 + count = new_count user.save() # Save log and user updates @@ -178,7 +230,17 @@ def check_in_or_out(user, right_now, log, count): @permission_required("HeroHours.change_users", raise_exception=True) -def send_data_to_google_sheet(request): +@ratelimit(key='user', rate='10/m', method='POST') +def send_data_to_google_sheet(request: HttpRequest) -> JsonResponse: + """ + Export all users and activity logs to Google Sheets via Apps Script. + + Args: + request: HTTP request object + + Returns: + JsonResponse: Status of the export operation + """ users = models.Users.objects.all() serialized_data = serializers.serialize('json', users, use_natural_foreign_keys=True) serialized_data2 = serializers.serialize('json', models.ActivityLog.objects.all(), use_natural_foreign_keys=True) @@ -198,27 +260,27 @@ def send_data_to_google_sheet(request): except Exception as e: logger.error("Failed to send data to Google Sheet: %s", e) return JsonResponse({'status': 'error', 'message': str(e), 'count': count}) -def sheet_pull(request): - key = request.GET.get('key') - if not key: - raise BadRequest() - - username, password = base64.b64decode(key).decode('ascii').split(":") - user = authenticate(request, username=username, password=password) - if not user: - raise PermissionDenied() + + +@permission_required("HeroHours.view_users", raise_exception=True) +@ratelimit(key='user', rate='30/m', method='GET') +def sheet_pull(request: HttpRequest) -> HttpResponse: + """ + Export users data to CSV format. + This view is deprecated. Use the API endpoint /api/sheet-pull/ with token authentication instead. + """ members = models.Users.objects.all() response = 'User_ID,First_Name,Last_Name,Total_Hours,Total_Seconds,Last_In,Last_Out,Is_Active,\n' for member in members: response += f"{member.User_ID},{member.First_Name},{member.Last_Name},{member.get_total_hours()},{member.Total_Seconds},{member.Last_In},{member.Last_Out},{member.Is_Active}\n" - return HttpResponse(response,content_type='text/csv') + return HttpResponse(response, content_type='text/csv') -def logout_view(request): +def logout_view(request: HttpRequest) -> HttpResponse: logout(request) return redirect('login') @permission_required("HeroHours.change_users") -def live_view(request): - return render(request, 'live.html') \ No newline at end of file +def live_view(request: HttpRequest) -> HttpResponse: + return render(request, 'live.html') From b63227a51ea0b2a5645cade54337215ff85219e4 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:43:45 -0600 Subject: [PATCH 16/19] Add custom throttling for API views Implemented custom throttling classes for SheetPull and MeetingList APIs to limit request rates. --- HeroHours_api/views.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/HeroHours_api/views.py b/HeroHours_api/views.py index 71fc269..9959e76 100644 --- a/HeroHours_api/views.py +++ b/HeroHours_api/views.py @@ -1,20 +1,29 @@ +# Third-party imports +from django.db.models import Subquery from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView from rest_framework_csv import renderers as csv_renderers -from django.db.models import Subquery -from HeroHours.models import Users, ActivityLog + +# Local imports +from HeroHours.models import ActivityLog, Users from HeroHours_api.authentication import URLTokenAuthentication # Create your views here. +class SheetPullThrottle(UserRateThrottle): + rate = '30/hour' + class SheetPullRenderer(csv_renderers.CSVRenderer): header = ['Id','Last Name','First Name','Is Active','Hours','Checked In','Last In','Last Out'] + class SheetPullAPI(APIView): renderer_classes = [SheetPullRenderer] authentication_classes = [URLTokenAuthentication] permission_classes = [IsAuthenticated] + throttle_classes = [SheetPullThrottle] def get(self, request, *args, **kwargs): members = Users.objects.all().order_by('Last_Name','First_Name') @@ -30,6 +39,9 @@ def get(self, request, *args, **kwargs): } for member in members] return Response(content, status=status.HTTP_200_OK) +class MeetingListThrottle(UserRateThrottle): + rate = '30/hour' + class MeetingListRender(csv_renderers.CSVRenderer): header = ['user_id','user__Last_Name','user__First_Name'] labels = { @@ -37,17 +49,29 @@ class MeetingListRender(csv_renderers.CSVRenderer): 'user__Last_Name': 'Last Name', 'user__First_Name': 'First Name', } + class MeetingPullAPI(APIView): renderer_classes = [MeetingListRender] authentication_classes = [URLTokenAuthentication] permission_classes = [IsAuthenticated] + throttle_classes = [MeetingListThrottle] def get(self, request, day, month, year, *args, **kwargs): + # Validate input parameters + try: + day = int(day) + month = int(month) + year = int(year) + if not (1 <= day <= 31 and 1 <= month <= 12 and 1900 <= year <= 2100): + return Response({'error': 'Invalid date parameters'}, status=status.HTTP_400_BAD_REQUEST) + except (ValueError, TypeError): + return Response({'error': 'Invalid date parameters'}, status=status.HTTP_400_BAD_REQUEST) + query = ActivityLog.objects.filter(id__in=Subquery( ActivityLog.objects.all() - .filter(timestamp__day=str(day),timestamp__month=str(month),timestamp__year=str(year),operation='Check In') \ - .order_by('user_id').distinct('user_id').values('id') - )).order_by('user__Last_Name','user__First_Name').values('user_id','user__First_Name','user__Last_Name') + .filter(timestamp__day=day, timestamp__month=month, timestamp__year=year, operation='Check In') + .order_by('user_id').distinct('user_id').values('id') + )).order_by('user__Last_Name', 'user__First_Name').values('user_id', 'user__First_Name', 'user__Last_Name') members = list(query) - return Response(members, status=status.HTTP_200_OK) \ No newline at end of file + return Response(members, status=status.HTTP_200_OK) From e18a44b9d2dda2c5850b4e464bcf49f23f07f084 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:44:17 -0600 Subject: [PATCH 17/19] Refactor settings.py for path handling and security Refactor settings to use Path for file paths and add security settings for production. --- HeroHoursRemake/settings.py | 89 ++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/HeroHoursRemake/settings.py b/HeroHoursRemake/settings.py index 344443c..c1ec299 100644 --- a/HeroHoursRemake/settings.py +++ b/HeroHoursRemake/settings.py @@ -10,11 +10,11 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ -from pathlib import Path import os +from pathlib import Path import dj_database_url -from dotenv import load_dotenv, find_dotenv +from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv()) @@ -72,7 +72,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -91,7 +91,10 @@ CHANNEL_LAYERS = { "default": { - "BACKEND": "channels.layers.InMemoryChannelLayer", + "BACKEND": "channels_redis.core.RedisChannelLayer" if not DEBUG else "channels.layers.InMemoryChannelLayer", + "CONFIG": { + "hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')], + } if not DEBUG else {}, } } @@ -145,7 +148,7 @@ # https://docs.djangoproject.com/en/5.1/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +STATIC_ROOT = BASE_DIR / 'staticfiles' STORAGES = { "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", @@ -153,7 +156,7 @@ } MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_ROOT = BASE_DIR / 'media' # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -167,4 +170,76 @@ # Security settings for production SESSION_COOKIE_SECURE = not DEBUG CSRF_COOKIE_SECURE = not DEBUG -SECURE_CONTENT_TYPE_NOSNIFF = True \ No newline at end of file +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_BROWSER_XSS_FILTER = True +X_FRAME_OPTIONS = 'DENY' +SECURE_HSTS_SECONDS = 31536000 if not DEBUG else 0 # 1 year +SECURE_HSTS_INCLUDE_SUBDOMAINS = not DEBUG +SECURE_HSTS_PRELOAD = not DEBUG + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle', + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '30/hour', + 'user': '100/hour', + }, +} + +# Logging configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {message}', + 'style': '{', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': BASE_DIR / 'logs' / 'django.log', + 'maxBytes': 1024 * 1024 * 5, # 5 MB + 'backupCount': 5, + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'] if not DEBUG else ['console'], + 'level': 'INFO', + 'propagate': False, + }, + 'HeroHours': { + 'handlers': ['console', 'file'] if not DEBUG else ['console'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + 'HeroHours_api': { + 'handlers': ['console', 'file'] if not DEBUG else ['console'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} From e2db389f865f3706431bcb2276264a310c810840 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:44:46 -0600 Subject: [PATCH 18/19] Add db and logs directories to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5537113..8d55343 100644 --- a/.gitignore +++ b/.gitignore @@ -291,4 +291,5 @@ fabric.properties db-data -db \ No newline at end of file +db +logs/ From 2076fd5b3475e51094837e8b66d9a3b039b02005 Mon Sep 17 00:00:00 2001 From: Ruthie Date: Sun, 15 Feb 2026 13:45:08 -0600 Subject: [PATCH 19/19] Upgrade package versions in requirements.txt Updated several package versions for better compatibility and security. --- requirements.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 37371cb..d1740f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,17 +10,19 @@ celery==5.4.0 certifi==2024.8.30 cffi==2.0.0 channels==4.3.2 +channels-redis==4.2.1 charset-normalizer==3.3.2 click==8.1.7 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 constantly==23.10.4 -cryptography==46.0.3 +cryptography==46.0.5 daphne==4.2.1 dj-database-url==2.2.0 -Django==5.2.9 +Django==5.2.11 django-debug-toolbar==4.4.6 +django-ratelimit==4.1.0 djangochannelsrestframework==1.3.0 djangorestframework==3.15.2 djangorestframework-csv==3.0.2 @@ -51,8 +53,8 @@ txaio==25.12.2 typing_extensions==4.12.2 tzdata==2024.2 ujson==5.11.0 -urllib3==2.2.3 +urllib3==2.6.3 vine==5.1.0 wcwidth==0.2.13 whitenoise==6.7.0 -zope.interface==8.1.1 \ No newline at end of file +zope.interface==8.1.1