Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project tries to adhere to [Semantic Versioning](https://semver.org/spe
- Simplified the landing page by removing the hero showcase, proof strip, and quick-link card band.
- Expanded the home page project and guide sections to show six items and gave guides and jobs full-width sections.
- Stopped capturing noisy project-like API requests in PostHog request analytics and session recording network logs while keeping explicit like/unlike events.
- Rebuilt project likes to render counts from annotated project queries and use a single toggle request instead of per-card like API reads.

### Fixed
- Made the bottom-right desktop ad use a solid surface instead of an opacity fade.
Expand Down
54 changes: 54 additions & 0 deletions api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,60 @@ def test_update_like_only_toggles_value_not_project_or_author(self):
self.assertEqual(like.project, self.project)
self.assertFalse(like.like)

def test_project_like_toggle_requires_authentication(self):
response = self.client.post(
reverse("api_project_like_toggle", kwargs={"project_id": self.project.id}),
data=json.dumps({"like": True}),
content_type="application/json",
)

self.assertIn(response.status_code, {401, 403})
self.assertFalse(Like.objects.exists())

def test_project_like_toggle_creates_like_and_returns_count(self):
self.client.force_login(self.user)

response = self.client.post(
reverse("api_project_like_toggle", kwargs={"project_id": self.project.id}),
data=json.dumps({"like": True}),
content_type="application/json",
)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["like"], True)
self.assertEqual(response.json()["like_count"], 1)
self.assertTrue(Like.objects.get(author=self.user, project=self.project).like)

def test_project_like_toggle_updates_existing_like_and_returns_count(self):
Like.objects.create(author=self.user, project=self.project, like=True)
Like.objects.create(author=self.other_user, project=self.project, like=True)
self.client.force_login(self.user)

response = self.client.post(
reverse("api_project_like_toggle", kwargs={"project_id": self.project.id}),
data=json.dumps({"like": False}),
content_type="application/json",
)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["like"], False)
self.assertEqual(response.json()["like_count"], 1)
self.assertFalse(Like.objects.get(author=self.user, project=self.project).like)

def test_project_like_toggle_toggles_when_like_value_is_omitted(self):
Like.objects.create(author=self.user, project=self.project, like=True)
self.client.force_login(self.user)

response = self.client.post(
reverse("api_project_like_toggle", kwargs={"project_id": self.project.id}),
data=json.dumps({}),
content_type="application/json",
)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["like"], False)
self.assertEqual(response.json()["like_count"], 0)


class SearchProjectsApiTests(TestCase):
def test_search_projects_filters_to_public_active_projects_and_limits_results(self):
Expand Down
3 changes: 2 additions & 1 deletion api/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.urls import path

from . import views
from .views import CreateLikeProjectAPIView, CreatePostAPIView, UpdateLikeProjectAPIView
from .views import CreateLikeProjectAPIView, CreatePostAPIView, ProjectLikeToggleAPIView, UpdateLikeProjectAPIView

urlpatterns = [
path("like/", CreateLikeProjectAPIView.as_view()),
path("like/<int:pk>/", UpdateLikeProjectAPIView.as_view()),
path("projects/<int:project_id>/like/", ProjectLikeToggleAPIView.as_view(), name="api_project_like_toggle"),
path("search/projects/", views.search_projects, name="api_search_projects"),
path("posts/", CreatePostAPIView.as_view(), name="api_create_post"),
]
58 changes: 58 additions & 0 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from blog.models import Post
from builtwithdjango.analytics import capture
Expand Down Expand Up @@ -98,6 +99,63 @@ def perform_destroy(self, instance):
)


class ProjectLikeToggleAPIView(APIView):
permission_classes = [IsAuthenticated]

def get_requested_like(self, request):
if "like" not in request.data:
return None

value = request.data["like"]
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized_value = value.lower()
if normalized_value in {"true", "1", "yes", "on"}:
return True
if normalized_value in {"false", "0", "no", "off"}:
return False

return bool(value)

def post(self, request, project_id):
project = generics.get_object_or_404(Project, pk=project_id)
requested_like = self.get_requested_like(request)
like_value = requested_like
if like_value is None:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
existing_like = Like.objects.filter(author=request.user, project=project).first()
like_value = not bool(existing_like and existing_like.like)

like, _ = Like.objects.update_or_create(
author=request.user,
project=project,
defaults={"like": like_value},
)
like_count = Like.objects.filter(project=project, like=True).count()
Comment thread
greptile-apps[bot] marked this conversation as resolved.

capture(
request,
"project liked" if like.like else "project unliked",
properties={
"project_id": like.project_id,
"author_id": like.author_id,
"like_id": like.id,
"like_value": like.like,
"like_count": like_count,
},
groups={"project": str(like.project_id)},
)

return Response(
{
"project": project.id,
"like": like.like,
"like_count": like_count,
},
status=status.HTTP_200_OK,
)


@api_view(["GET"])
@permission_classes([AllowAny])
def search_projects(request):
Expand Down
Loading
Loading