Skip to content

Commit

Permalink
Implement subscribe/leave toggle feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathaniel81 committed Mar 13, 2024
1 parent 2f07cbc commit 9816f52
Show file tree
Hide file tree
Showing 19 changed files with 548 additions and 119 deletions.
6 changes: 5 additions & 1 deletion backend/accounts/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ def authenticate(self, request):

if raw_token is None:
return None
validated_token = self.get_validated_token(raw_token)

try:
validated_token = self.get_validated_token(raw_token)
except Exception as e:
return None
enforce_csrf(request)

return self.get_user(validated_token), validated_token
10 changes: 6 additions & 4 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SubrabbitSerializer_detailed(serializers.ModelSerializer):
subscribers = UserSerializer(many=True, read_only=True)
moderators = UserSerializer(many=True, read_only=True)
members_count = serializers.SerializerMethodField()
is_subscriber = serializers.SerializerMethodField()
# is_subscriber = serializers.SerializerMethodField()
# posts = PostSerializer(many=True, read_only=True)

class Meta:
Expand All @@ -27,6 +27,8 @@ class Meta:
def get_members_count(self, obj):
return obj.subscribers.count()

def get_is_subscriber(self, obj):
user = self.context['request'].user
return obj.subscribers.filter(id=user.id).exists()
# def get_is_subscriber(self, obj):
# user = self.context.get('user')
# print(user)
# # user = self.context['request'].user
# return obj.subscribers.filter(id=user.id).exists()
3 changes: 3 additions & 0 deletions backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@

urlpatterns = [
path('subrabbits/', views.SubrabbitListCreateView.as_view(), name='subrabbits'),
path('subrabbit/<str:name>/', views.SubrabbitDetail.as_view(), name='subrabbit-detail'),
path('subrabbit/<str:name>/subscribe/', views.SubscribeView.as_view(), name='subscribe'),
path('subrabbit/<str:name>/unsubscribe/', views.UnsubscribeView.as_view(), name='unsubcribe'),
]
66 changes: 61 additions & 5 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .permissions import IsAuthenticatedOrReadOnly
from .serializers import SubrabbitSerializer, SubrabbitSerializer_detailed

# from rest_framework.response import Response
from rest_framework.response import Response
# from rest_framework import serializers


Expand All @@ -18,10 +18,21 @@ class SubrabbitListCreateView(generics.ListCreateAPIView):
queryset = Subrabbit.objects.all().order_by('-created_at')

# Apply authentication to unsafe HTTP methods
def get_authenticators(self):
if self.request.method in SAFE_METHODS:
return []
return super().get_authenticators()
# def get_authenticators(self):
# if self.request.method in SAFE_METHODS:
# return []
# return super().get_authenticators()

def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if self.request.method == 'GET': #and request.user.is_authenticated:
user = request.user
print(user)
# instance = self.get_object()
# is_subscriber = instance.subscribers.filter(id=user.id).exists()
# self.is_subscriber = is_subscriber
# else:
# self.is_subscriber = False

# Dynamically select serializer based on HTTP method
def get_serializer_class(self):
Expand Down Expand Up @@ -67,3 +78,48 @@ def handle_validation_error(self, e):
return Response(e.detail, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
else:
return Response(e.detail, status=status.HTTP_400_BAD_REQUEST)

class SubrabbitDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
authentication_classes = [CustomAuthentication]
queryset = Subrabbit.objects.all()
lookup_field = 'name'

def get_serializer_class(self):
if self.request.method == 'GET':
return SubrabbitSerializer_detailed
return SubrabbitSerializer

def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
data = serializer.data
if request.user.is_authenticated:
user = request.user
instance = self.get_object()
is_subscriber = instance.subscribers.filter(id=user.id).exists()
data['isSubscriber'] = is_subscriber
return Response(data)

class SubscribeView(generics.UpdateAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
authentication_classes = [CustomAuthentication]
queryset = Subrabbit.objects.all()
lookup_field = 'name'

def update(self, request, *args, **kwargs):
subrabbit = self.get_object()
subrabbit.subscribers.add(request.user)
return Response({'message': 'subscribed successfully'}, status=status.HTTP_204_NO_CONTENT)


class UnsubscribeView(generics.UpdateAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
authentication_classes = [CustomAuthentication]
queryset = Subrabbit.objects.all()
lookup_field = 'name'

def update(self, request, *args, **kwargs):
subrabbit = self.get_object()
subrabbit.subscribers.remove(request.user)
return Response({'message': 'unsubscribed successfully'} ,status=status.HTTP_204_NO_CONTENT)
Binary file modified backend/db.sqlite3
Binary file not shown.
10 changes: 10 additions & 0 deletions backend/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"axios": "^1.6.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"date-fns": "^3.4.0",
"lucide-react": "^0.349.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
2 changes: 2 additions & 0 deletions backend/frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ const Navbar = () => {
<p className='hidden text-zinc-700 text-sm font-medium md:block'>Rabbit</p>
</Link>
<div className="flex justify-between gap-5">

{/* <SearchBar /> */}

{user ? (
<>
<DropdownMenu>
Expand Down
170 changes: 170 additions & 0 deletions backend/frontend/src/components/SubrabbitSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { Button, buttonVariants } from '@/components/ui/Button';
import { useToast } from '@/hooks/useToast';
import { SubscribeToSubrabbitPayload, SubrabbitSubscriptionValidator } from '@/lib/validators/subrabbit';
import { AppDispatch, RootState } from '@/redux/store';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { format } from 'date-fns';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';
import { handleAxiosError, getCsrfToken } from '@/lib/utils';
import { Subrabbit } from '@/types/subrabbit';



type SubscribeComponentProps = {
queryKey: string[];
subrabbit: Subrabbit;
};

const SubrabbitSidebar = ({ subrabbit, queryKey }: SubscribeComponentProps) => {
const userLogin = useSelector((state: RootState) => state.userInfo);
const { user } = userLogin;
const navigate = useNavigate();
const dispatch = useDispatch<AppDispatch>();
const { toast } = useToast()
const queryClient = useQueryClient();
const location = useLocation();
const path = location.pathname;



const { mutate: subscribe, isPending: isSubLoading } = useMutation({
mutationFn: async () => {
const payload: SubscribeToSubrabbitPayload = {
subrabbitId: subrabbit.id
}
const result = SubrabbitSubscriptionValidator.safeParse(payload);
if (!result.success) {
console.error(result.error);
throw new Error(result.error.errors[0].message);
}
try {
const { data } = await axios.put(`/api/subrabbit/${subrabbit?.name}/subscribe/`, payload, {
withCredentials: true,
headers: {
"Content-Type": "application/json",
"x-csrftoken": getCsrfToken()
},
});
return data as string
/*eslint-disable*/
} catch (err: any) {
return handleAxiosError(err, dispatch, payload, subrabbit);
}
},
onError: (err) => {
console.log(err)
return toast({
title: 'There was a problem.',
description: 'Something went wrong. Please try again.',
variant: 'destructive',
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKey, exact: true });
toast({
title: 'subscribed!',
description: `You are now subscribed from/${subrabbit?.name}`,
})
},
});

const { mutate: unsubscribe, isPending: isUnsubLoading } = useMutation({
mutationFn: async () => {
const payload: SubscribeToSubrabbitPayload = {
subrabbitId: subrabbit.id
}
const result = SubrabbitSubscriptionValidator.safeParse(payload);
if (!result.success) {
console.error(result.error);
throw new Error(result.error.errors[0].message);
}
try {
const { data } = await axios.put(`/api/subrabbit/${subrabbit?.name}/unsubscribe/`, payload, {
withCredentials: true,
headers: {
"Content-Type": "application/json",
"x-csrftoken": getCsrfToken()
},
});
return data as string
/*eslint-disable*/
} catch (err: any) {
return handleAxiosError(err, dispatch, payload, subrabbit);
}
},
onError: (err) => {
console.log(err)
return toast({
title: 'There was a problem.',
description: 'Something went wrong. Please try again.',
variant: 'destructive',
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKey, exact: true });
toast({
title: 'unsubscribed!',
description: `You are now unsubscribed from/${subrabbit?.name}`,
})
},
});


return (
<dl className='divide-y divide-gray-100 px-6 py-4 text-sm leading-6 bg-white'>
<div className='flex justify-between gap-x-4 py-3'>
<dt className='text-gray-500'>Created</dt>
<dd className='text-gray-700'>
{subrabbit?.created_at && (
<time dateTime={new Date(subrabbit?.created_at).toDateString()}>
{format(new Date(subrabbit?.created_at), 'MMMM d, yyyy')}
</time>
)}
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className='text-gray-500'>Members</dt>
<dd className='flex items-start gap-x-2'>
<div className='text-gray-900'>{subrabbit?.members_count}</div>
</dd>
</div>
{subrabbit?.creator?.id === user?.user_id ? (
<div className='flex justify-between gap-x-4 py-3'>
<dt className='text-gray-500'>You created this community</dt>
</div>
) : null}

{subrabbit?.creator?.id !== user?.user_id ? (
subrabbit?.isSubscriber ? (
<Button
className='w-full mt-1 mb-4'
isLoading={isUnsubLoading}
onClick={() => unsubscribe()}
>
Leave community
</Button>
) : (
<Button
className='w-full mt-1 mb-4'
isLoading={isSubLoading}
onClick={() => subscribe()}
>
Join to post
</Button>
)
) : null}
<div
className={buttonVariants({
variant: 'outline',
className: 'w-full mb-6 cursor-pointer',
})}
onClick={()=> navigate(path + `/submit`)}>
Create Post
</div>
</dl>
)
}

export default SubrabbitSidebar
Loading

0 comments on commit 9816f52

Please sign in to comment.