Skip to content

Commit 8718970

Browse files
authored
Merge pull request #73 from wildjames/dev
Users are invited to households
2 parents 9a11682 + 726a4c0 commit 8718970

File tree

11 files changed

+431
-112
lines changed

11 files changed

+431
-112
lines changed

todoqueue_backend/run_server.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ python manage.py collectstatic --noinput && \
2121
echo "Starting Nginx..."
2222
nginx &
2323

24+
# Default to a host port of 8000, and if the environment variable is set then use that
25+
if [[ -z "$DJANGO_HOST_PORT" ]]; then
26+
export DJANGO_HOST_PORT=8000
27+
fi
28+
2429
# Start the Django server
2530
echo "Starting the Django server..."
2631
exec gunicorn todoqueue_backend.wsgi:application --bind 0.0.0.0:$DJANGO_HOST_PORT
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.5 on 2023-11-05 19:45
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('tasks', '0004_alter_flexibletask_description_and_more'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='Invitation',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('accepted', models.BooleanField(default=False)),
21+
('timestamp', models.DateTimeField(auto_now_add=True)),
22+
('household', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.household')),
23+
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_invitations', to=settings.AUTH_USER_MODEL)),
24+
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_invitations', to=settings.AUTH_USER_MODEL)),
25+
],
26+
),
27+
]

todoqueue_backend/tasks/models.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from rest_framework.response import Response
2121
from rest_framework.validators import UniqueValidator
2222

23+
from accounts.serializers import CustomUserSerializer
24+
2325
logger = getLogger(__name__)
2426
usermodel = get_user_model()
2527

@@ -328,6 +330,25 @@ def get_task_by_id(task_id):
328330
return None, None
329331

330332

333+
class Invitation(models.Model):
334+
household = models.ForeignKey("Household", on_delete=models.CASCADE)
335+
sender = models.ForeignKey(
336+
settings.AUTH_USER_MODEL,
337+
related_name="sent_invitations",
338+
on_delete=models.CASCADE,
339+
)
340+
recipient = models.ForeignKey(
341+
settings.AUTH_USER_MODEL,
342+
related_name="received_invitations",
343+
on_delete=models.CASCADE,
344+
)
345+
accepted = models.BooleanField(default=False)
346+
timestamp = models.DateTimeField(auto_now_add=True)
347+
348+
def __str__(self):
349+
return f"Invitation from {self.sender} to {self.recipient} for {self.household}"
350+
351+
331352
# Serializers have to live here, to avoid circular imports :(
332353

333354

@@ -336,7 +357,9 @@ class ScheduledTaskSerializer(serializers.ModelSerializer):
336357
next_due = serializers.SerializerMethodField()
337358
last_due = serializers.SerializerMethodField()
338359
mean_completion_time = serializers.SerializerMethodField()
339-
description = serializers.CharField(required=False, allow_blank=True, validators=[validate_profanity])
360+
description = serializers.CharField(
361+
required=False, allow_blank=True, validators=[validate_profanity]
362+
)
340363

341364
class Meta:
342365
model = ScheduledTask
@@ -358,7 +381,9 @@ def get_mean_completion_time(self, obj):
358381
class FlexibleTaskSerializer(serializers.ModelSerializer):
359382
staleness = serializers.SerializerMethodField()
360383
mean_completion_time = serializers.SerializerMethodField()
361-
description = serializers.CharField(required=False, allow_blank=True, validators=[validate_profanity])
384+
description = serializers.CharField(
385+
required=False, allow_blank=True, validators=[validate_profanity]
386+
)
362387

363388
class Meta:
364389
model = FlexibleTask
@@ -488,7 +513,11 @@ class Meta:
488513

489514
class CreateHouseholdSerializer(serializers.ModelSerializer):
490515
name = serializers.CharField(
491-
max_length=255, validators=[UniqueValidator(queryset=Household.objects.all()), validate_profanity]
516+
max_length=255,
517+
validators=[
518+
UniqueValidator(queryset=Household.objects.all()),
519+
validate_profanity,
520+
],
492521
)
493522

494523
class Meta:
@@ -498,3 +527,21 @@ class Meta:
498527
def create(self, validated_data):
499528
logger.info(f"Creating household with name: {validated_data['name']}")
500529
return Household.objects.create(name=validated_data["name"])
530+
531+
532+
class InvitationSerializer(serializers.ModelSerializer):
533+
sender = CustomUserSerializer(read_only=True)
534+
recipient = CustomUserSerializer(read_only=True)
535+
household = HouseholdSerializer(read_only=True)
536+
537+
class Meta:
538+
model = Invitation
539+
fields = ["id", "household", "sender", "recipient", "accepted", "timestamp"]
540+
541+
def create(self, validated_data):
542+
# Custom create method, if needed. Might be useful if I ever add push notifications.
543+
return super().create(validated_data)
544+
545+
def update(self, instance, validated_data):
546+
# Custom update method, if needed. Could push notification when invite is accepted
547+
return super().update(instance, validated_data)

todoqueue_backend/tasks/urls.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@
1111

1212
urlpatterns = [
1313
path("", include(router.urls)),
14+
path("toggle_frozen/<uuid:taskId>/", views.toggle_frozen, name="toggle_frozen"),
1415
path(
1516
"calculate_brownie_points/",
1617
views.calculate_brownie_points_view,
1718
name="calculate_brownie_points",
1819
),
19-
path(
20-
"households/<int:pk>/get_dummy_task_id/",
21-
views.get_dummy_task_id,
22-
name="get_dummy_task_id",
23-
),
2420
path(
2521
"user_statistics/", views.UserStatisticsView.as_view(), name="user_statistics"
2622
),
@@ -30,14 +26,33 @@
3026
name="create_household",
3127
),
3228
path(
33-
"households/<pk>/add_user/",
34-
views.AddUserToHouseholdView.as_view(),
35-
name="add_user_to_household",
29+
"households/<int:pk>/get_dummy_task_id/",
30+
views.get_dummy_task_id,
31+
name="get_dummy_task_id",
3632
),
33+
# path(
34+
# "households/<pk>/add_user/",
35+
# views.AddUserToHouseholdView.as_view(),
36+
# name="add_user_to_household",
37+
# ),
3738
path(
3839
"households/<pk>/remove_user/",
3940
views.RemoveUserFromHouseholdView.as_view(),
4041
name="remove_user_from_household",
4142
),
42-
path("toggle_frozen/<uuid:taskId>/", views.toggle_frozen, name="toggle_frozen"),
43+
path(
44+
"households/<int:pk>/invite_user/",
45+
views.InviteUserToHouseholdView.as_view(),
46+
name="invite_user_to_household",
47+
),
48+
path(
49+
"invitations/pending/",
50+
views.PendingInvitationsView.as_view(),
51+
name="pending_invitations",
52+
),
53+
path(
54+
"invitations/<int:invitation_id>/respond/",
55+
views.RespondToInvitationView.as_view(),
56+
name="respond_to_invitation",
57+
),
4358
]

todoqueue_backend/tasks/views.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
FlexibleTaskSerializer,
2929
Household,
3030
HouseholdSerializer,
31+
Invitation,
32+
InvitationSerializer,
3133
ScheduledTask,
3234
ScheduledTaskSerializer,
3335
AllTasksSerializer,
@@ -333,6 +335,93 @@ def post(self, request, pk):
333335
return Response("OK", 200)
334336

335337

338+
class InviteUserToHouseholdView(APIView):
339+
permission_classes = (IsAuthenticated,)
340+
341+
def post(self, request, pk):
342+
email = request.data.get("email")
343+
if not email:
344+
return Response(
345+
{"detail": "Missing email"}, status=status.HTTP_400_BAD_REQUEST
346+
)
347+
348+
try:
349+
household = Household.objects.get(pk=pk)
350+
except Household.DoesNotExist:
351+
return Response(
352+
{"detail": "Household does not exist"}, status=status.HTTP_404_NOT_FOUND
353+
)
354+
355+
try:
356+
recipient = get_user_model().objects.get(email=email)
357+
except get_user_model().DoesNotExist:
358+
return Response(
359+
{"detail": "User does not exist"}, status=status.HTTP_404_NOT_FOUND
360+
)
361+
362+
# Check if the user is already a member of the household
363+
if household.users.filter(pk=recipient.pk).exists():
364+
return Response(
365+
{"detail": "User is already a member of the household"},
366+
status=status.HTTP_400_BAD_REQUEST,
367+
)
368+
369+
# Create the invitation
370+
Invitation.objects.create(
371+
household=household, sender=request.user, recipient=recipient
372+
)
373+
374+
# TODO: Send a notification to the recipient about the invitation
375+
376+
return Response(
377+
{"detail": "Invitation sent successfully"}, status=status.HTTP_200_OK
378+
)
379+
380+
381+
class PendingInvitationsView(APIView):
382+
permission_classes = (IsAuthenticated,)
383+
384+
def get(self, request):
385+
invitations = Invitation.objects.filter(recipient=request.user, accepted=False)
386+
serializer = InvitationSerializer(invitations, many=True)
387+
return Response(serializer.data)
388+
389+
390+
class RespondToInvitationView(APIView):
391+
permission_classes = (IsAuthenticated,)
392+
393+
def post(self, request, invitation_id):
394+
action = request.data.get("action")
395+
if action not in ["accept", "decline"]:
396+
return Response(
397+
{"detail": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST
398+
)
399+
400+
try:
401+
invitation = Invitation.objects.get(
402+
id=invitation_id, recipient=request.user
403+
)
404+
except Invitation.DoesNotExist:
405+
return Response(
406+
{"detail": "Invitation does not exist"},
407+
status=status.HTTP_404_NOT_FOUND,
408+
)
409+
410+
if action == "accept":
411+
invitation.household.users.add(request.user)
412+
invitation.accepted = True
413+
invitation.save()
414+
return Response(
415+
{"detail": "You have joined the household"}, status=status.HTTP_200_OK
416+
)
417+
elif action == "decline":
418+
invitation.delete()
419+
return Response(
420+
{"detail": "You have declined the invitation"},
421+
status=status.HTTP_200_OK,
422+
)
423+
424+
336425
class RemoveUserFromHouseholdView(APIView):
337426
permission_classes = (IsAuthenticated,)
338427

0 commit comments

Comments
 (0)