From d4331e7d8be271e3fcc3a40cc73379506c3650ba Mon Sep 17 00:00:00 2001 From: James Wild Date: Sat, 14 Oct 2023 23:59:00 +0100 Subject: [PATCH 1/2] minor tweaks --- todoqueue_frontend/src/App.js | 5 ++--- todoqueue_frontend/src/components/Tasks.js | 8 ++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/todoqueue_frontend/src/App.js b/todoqueue_frontend/src/App.js index a4a16cb..84fed4c 100644 --- a/todoqueue_frontend/src/App.js +++ b/todoqueue_frontend/src/App.js @@ -24,7 +24,7 @@ const App = () => { const [selectedHousehold, setSelectedHousehold] = useState(null); const [showHouseholdSelector, setShowHouseholdSelector] = useState(false); - // Try and prevent chrom from translating the page when there are few words on screen + // Try and prevent chrome from translating the page when there are few words on screen useEffect(() => { document.documentElement.lang = 'en'; document.documentElement.setAttribute('xml:lang', 'en'); @@ -47,9 +47,8 @@ const App = () => { } }; - // run immediately, then start a timer that runs every 1000ms updateHouseholds(); - const interval = setInterval(updateHouseholds, 1000); + const interval = setInterval(updateHouseholds, 10000); return () => clearInterval(interval); }, [selectedHousehold]); diff --git a/todoqueue_frontend/src/components/Tasks.js b/todoqueue_frontend/src/components/Tasks.js index 333fe8c..bea1066 100644 --- a/todoqueue_frontend/src/components/Tasks.js +++ b/todoqueue_frontend/src/components/Tasks.js @@ -61,6 +61,7 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => { // Fetch tasks, and users at regular intervals + // TODO: Is it wise to make these requests so often? Can it be offloaded to the client? useEffect(() => { // run immediately, then start a timer that runs every 1000ms try { @@ -93,12 +94,7 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => { } console.log("Brownie points changed"); - const timeout = setTimeout(() => { - setShowFlipAnimation(true); - }, 500); - - // Clean up timeout when the component is unmounted - return () => clearTimeout(timeout); + setShowFlipAnimation(true); }, [browniePoints]); From 27463c4a3a8271fa484f1b18a76bb71a27c5c466 Mon Sep 17 00:00:00 2001 From: James Wild Date: Sun, 15 Oct 2023 00:55:03 +0100 Subject: [PATCH 2/2] Add a toggle for the scoreboard, to show rolling BP --- todoqueue_backend/accounts/serializers.py | 17 +++++-- todoqueue_backend/tasks/models.py | 3 +- todoqueue_backend/tasks/views.py | 40 +++++++++++++---- todoqueue_frontend/src/App.css | 25 ++++++++++- todoqueue_frontend/src/components/Tasks.js | 52 ++++++++++++++-------- 5 files changed, 104 insertions(+), 33 deletions(-) diff --git a/todoqueue_backend/accounts/serializers.py b/todoqueue_backend/accounts/serializers.py index 6382c2c..487dbf8 100644 --- a/todoqueue_backend/accounts/serializers.py +++ b/todoqueue_backend/accounts/serializers.py @@ -3,8 +3,10 @@ from rest_framework.validators import UniqueValidator from logging import getLogger + logger = getLogger(__name__) + class CustomUserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() @@ -18,6 +20,13 @@ class Meta: ) +class CustomUserWithBrowniePointsSerializer(CustomUserSerializer): + rolling_brownie_points = serializers.IntegerField(read_only=True, default=0) + + class Meta(CustomUserSerializer.Meta): + fields = CustomUserSerializer.Meta.fields + ("rolling_brownie_points",) + + class CustomUserRegistrationSerializer(serializers.ModelSerializer): email = serializers.EmailField( validators=[UniqueValidator(queryset=get_user_model().objects.all())] @@ -46,6 +55,8 @@ class ResetPasswordSerializer(serializers.Serializer): confirm_new_password = serializers.CharField(write_only=True) def validate(self, data): - if data['new_password'] != data['confirm_new_password']: - raise serializers.ValidationError({"confirm_new_password": "Passwords do not match"}) - return data \ No newline at end of file + if data["new_password"] != data["confirm_new_password"]: + raise serializers.ValidationError( + {"confirm_new_password": "Passwords do not match"} + ) + return data diff --git a/todoqueue_backend/tasks/models.py b/todoqueue_backend/tasks/models.py index 87df966..8e7c264 100644 --- a/todoqueue_backend/tasks/models.py +++ b/todoqueue_backend/tasks/models.py @@ -10,11 +10,10 @@ from django.contrib.contenttypes.models import ContentType from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models.signals import m2m_changed, pre_delete +from django.db.models.signals import m2m_changed from django.dispatch import receiver from django.utils import timezone from rest_framework import serializers -from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.validators import UniqueValidator diff --git a/todoqueue_backend/tasks/views.py b/todoqueue_backend/tasks/views.py index 3f2a6ea..3cb34e1 100644 --- a/todoqueue_backend/tasks/views.py +++ b/todoqueue_backend/tasks/views.py @@ -1,8 +1,13 @@ +from datetime import timedelta from logging import INFO, basicConfig, getLogger -from accounts.serializers import CustomUserSerializer +from accounts.serializers import CustomUserWithBrowniePointsSerializer from django.contrib.auth import get_user_model +from django.db.models import Sum, Q +from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 +from django.utils import timezone + from rest_framework import status, viewsets from rest_framework.decorators import ( action, @@ -217,8 +222,22 @@ def list_users(self, request, pk=None): {"detail": "Not allowed."}, status=status.HTTP_403_FORBIDDEN ) + # Date calculations + start_datetime = timezone.now() - timedelta(minutes=5) + + # Prefetch worklogs from the last 7 days and annotate the sum of brownie points + users = household.users.annotate( + rolling_brownie_points=Coalesce( + Sum( + "worklog__brownie_points", + filter=Q(worklog__timestamp__gte=start_datetime), + ), + 0, + ) + ) + # Serialize the users of the household - user_serializer = CustomUserSerializer(household.users.all(), many=True) + user_serializer = CustomUserWithBrowniePointsSerializer(users, many=True) return Response(user_serializer.data) @@ -393,7 +412,7 @@ def award_brownie_points(request, pk): logger.info(f"Awarding brownie points") logger.info(f"Household PK: {pk}") # Retrieve brownie_points from query parameters - brownie_points = request.GET.get('brownie_points') + brownie_points = request.GET.get("brownie_points") logger.info(f"Brownie points: {brownie_points}") if brownie_points is None: return Response( @@ -405,17 +424,22 @@ def award_brownie_points(request, pk): brownie_points = int(brownie_points) except ValueError: return Response( - {"error": "Invalid brownie_points value"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Invalid brownie_points value"}, + status=status.HTTP_400_BAD_REQUEST, ) user = request.user - household = get_object_or_404(Household, pk=pk) # It'll return 404 if the household does not exist + household = get_object_or_404( + Household, pk=pk + ) # It'll return 404 if the household does not exist if user not in household.users.all(): return Response({"error": "Not allowed"}, status=status.HTTP_403_FORBIDDEN) try: - user.brownie_point_credit.setdefault(str(household.id), 0) # This ensures the key exists + user.brownie_point_credit.setdefault( + str(household.id), 0 + ) # This ensures the key exists user.brownie_point_credit[str(household.id)] += brownie_points user.save() except: @@ -424,6 +448,4 @@ def award_brownie_points(request, pk): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - return Response( - {"success": "Credited brownie points"}, status=status.HTTP_200_OK - ) \ No newline at end of file + return Response({"success": "Credited brownie points"}, status=status.HTTP_200_OK) diff --git a/todoqueue_frontend/src/App.css b/todoqueue_frontend/src/App.css index 231da90..e34c496 100644 --- a/todoqueue_frontend/src/App.css +++ b/todoqueue_frontend/src/App.css @@ -567,6 +567,25 @@ h1 { background-color: rgba(255, 0, 0, 0.145); } +.toggle-switch { + position: absolute; + left: -130px; + top: 50%; + transform: translateY(-50%); +} + +.toggle-switch input[type="checkbox"] { + display: none; +} + +.toggle-switch label { + cursor: pointer; + background-color: #224d76; + width: 7em; + padding: 5px 10px; + border-radius: 15px; +} + .user-stats-container { position: absolute; bottom: 15px; @@ -580,7 +599,6 @@ h1 { .user-stats-container:hover { background-color: #333; - transform: translateX(-50%) scale(1.05); /* combined transform */ cursor: pointer; } @@ -598,6 +616,11 @@ h1 { margin: 0 1rem; } +.user-stats-flex:hover { + transform: scale(1.05); + cursor: pointer; +} + .user-name { font-size: 1.5rem; font-weight: bold; diff --git a/todoqueue_frontend/src/components/Tasks.js b/todoqueue_frontend/src/components/Tasks.js index bea1066..7e05b45 100644 --- a/todoqueue_frontend/src/components/Tasks.js +++ b/todoqueue_frontend/src/components/Tasks.js @@ -29,6 +29,7 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => { const isInitialRender = useRef(true); const [browniePoints, setBrowniePoints] = useState(0); const [showFlipAnimation, setShowFlipAnimation] = useState(false); + const [viewMode, setViewMode] = useState('total'); // Toggle scoreboard between 'total' or 'rolling' // Define an enumeration for the popups const PopupType = { @@ -63,7 +64,6 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => { // Fetch tasks, and users at regular intervals // TODO: Is it wise to make these requests so often? Can it be offloaded to the client? useEffect(() => { - // run immediately, then start a timer that runs every 1000ms try { fetchSetTasks(); fetchSetUsers(); @@ -132,7 +132,7 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => { setUsers([]); return; } - console.log("Setting users: ", users); + console.log("Setting users: ", data); setUsers(data); }; @@ -370,30 +370,46 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => { {selectedHousehold ? ( -
-
- { +
+
+ setViewMode(prevMode => prevMode === 'total' ? 'rolling' : 'total')} + /> + +
+
+ {viewMode === 'total' ? users.sort((a, b) => (b.brownie_point_credit[selectedHousehold] - b.brownie_point_debit[selectedHousehold]) - (a.brownie_point_credit[selectedHousehold] - a.brownie_point_debit[selectedHousehold]) ) .slice(0, 5) - .map((user, index) => { - return ( -
- {user.username} - -
- ); - }) + .map((user, index) => ( +
+ {user.username} + +
+ )) + : + users.sort((a, b) => b.rolling_brownie_points - a.rolling_brownie_points) + .slice(0, 5) + .map((user, index) => ( +
+ {user.username} + +
+ )) }
+ ) : null}