Skip to content

Commit

Permalink
Merge pull request #42 from wildjames/dev
Browse files Browse the repository at this point in the history
One-shot BP awards; rolling total scoreboard
  • Loading branch information
wildjames authored Oct 14, 2023
2 parents 5d9e30a + 27463c4 commit 8af3546
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 42 deletions.
17 changes: 14 additions & 3 deletions todoqueue_backend/accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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())]
Expand Down Expand Up @@ -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
if data["new_password"] != data["confirm_new_password"]:
raise serializers.ValidationError(
{"confirm_new_password": "Passwords do not match"}
)
return data
3 changes: 1 addition & 2 deletions todoqueue_backend/tasks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 31 additions & 9 deletions todoqueue_backend/tasks/views.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand All @@ -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
)
return Response({"success": "Credited brownie points"}, status=status.HTTP_200_OK)
25 changes: 24 additions & 1 deletion todoqueue_frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -580,7 +599,6 @@ h1 {

.user-stats-container:hover {
background-color: #333;
transform: translateX(-50%) scale(1.05); /* combined transform */
cursor: pointer;
}

Expand All @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions todoqueue_frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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]);

Expand Down
60 changes: 36 additions & 24 deletions todoqueue_frontend/src/components/Tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -61,8 +62,8 @@ 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();
Expand Down Expand Up @@ -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]);


Expand Down Expand Up @@ -136,7 +132,7 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => {
setUsers([]);
return;
}
console.log("Setting users: ", users);
console.log("Setting users: ", data);
setUsers(data);
};

Expand Down Expand Up @@ -374,30 +370,46 @@ const Tasks = ({ selectedHousehold, setShowHouseholdSelector }) => {


{selectedHousehold ? (
<div
className="user-stats-container"
onClick={handleOpenAwardBrowniePointsPopup}
>
<div className="user-stats-flex">
{
<div className="user-stats-container">
<div className="toggle-switch">
<input
type="checkbox"
id="viewModeSwitch"
checked={viewMode === 'rolling'}
onChange={() => setViewMode(prevMode => prevMode === 'total' ? 'rolling' : 'total')}
/>
<label htmlFor="viewModeSwitch">
{viewMode === "total" ? "All Time" : "Last 7 days"}
</label>
</div>
<div className="user-stats-flex" onClick={handleOpenAwardBrowniePointsPopup}>
{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 (
<div key={index} className="user-row">
<span className="user-name">{user.username}</span>
<SimpleFlipper
value={user.brownie_point_credit[selectedHousehold] - user.brownie_point_debit[selectedHousehold]}
/>
</div>
);
})
.map((user, index) => (
<div key={index} className="user-row">
<span className="user-name">{user.username}</span>
<SimpleFlipper
value={user.brownie_point_credit[selectedHousehold] - user.brownie_point_debit[selectedHousehold]}
/>
</div>
))
:
users.sort((a, b) => b.rolling_brownie_points - a.rolling_brownie_points)
.slice(0, 5)
.map((user, index) => (
<div key={index} className="user-row">
<span className="user-name">{user.username}</span>
<SimpleFlipper value={user.rolling_brownie_points} />
</div>
))
}
</div>
</div>

) : null}


Expand Down

0 comments on commit 8af3546

Please sign in to comment.