Skip to content

Commit

Permalink
Add dynamic sorting and search to user profile page
Browse files Browse the repository at this point in the history
  • Loading branch information
anorthall committed Nov 3, 2023
1 parent 1f1192e commit 782c264
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 361 deletions.
125 changes: 0 additions & 125 deletions app/logger/tests/test_user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from ..factories import TripFactory
from ..models import Caver, Trip
from ..views.userprofile import UserProfile as UserProfileView

User = get_user_model()

Expand Down Expand Up @@ -63,34 +62,6 @@ def setUp(self):
notes="User2 trip notes",
)

def test_user_profile_page_trip_list(self):
"""Test the trip list on the user profile page"""
self.client.force_login(self.user)
response = self.client.get(reverse("log:user", args=[self.user.username]))
self.assertEqual(response.status_code, 200)

# Test pagination and that the correct trips are displayed
for i in range(1, 50):
self.assertContains(response, f"User1 Cave {i}")
self.assertNotContains(response, f"User2 Cave {i}")

for i in range(51, 100):
self.assertNotContains(response, f"User1 Cave {i}")
self.assertNotContains(response, f"User2 Cave {i}")

# Test edit links appear
for trip in self.user.trips.order_by("-start")[:50]:
self.assertContains(response, reverse("log:trip_update", args=[trip.uuid]))

# Test the next page
response = self.client.get(
reverse("log:user", args=[self.user.username]) + "?page=2",
)
self.assertEqual(response.status_code, 200)
for i in range(51, 100):
self.assertContains(response, f"User1 Cave {i}")
self.assertNotContains(response, f"User2 Cave {i}")

def test_user_profile_page_title(self):
"""Test the user profile page title"""
self.client.force_login(self.user)
Expand Down Expand Up @@ -135,23 +106,6 @@ def test_friend_only_trips_do_not_appear_on_profile_page_trip_list(self):
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "User1 Cave")

@tag("privacy")
def test_that_friend_only_trips_appear_to_friends(self):
"""Test that friend only trips appear on the user profile page to friends"""
for trip in self.user.trips:
trip.privacy = Trip.FRIENDS
trip.save()

self.user.friends.add(self.user2)
self.user2.friends.add(self.user)

self.client.force_login(self.user2)
response = self.client.get(reverse("log:user", args=[self.user.username]))
self.assertEqual(response.status_code, 200)

for trip in self.user.trips.order_by("-start")[:50]:
self.assertContains(response, trip.cave_name)

@tag("privacy")
def test_user_profile_page_with_various_privacy_settings(self):
"""Test the user profile page with various privacy settings"""
Expand Down Expand Up @@ -274,85 +228,6 @@ def test_no_friends_appear_for_an_unauthenticated_user(self):
self.assertNotContains(response, self.user3.get_absolute_url())
self.assertNotContains(response, "Friends")

def test_user_profile_page_loads_with_all_allowed_ordering(self):
"""Test that the user profile page loads with all allowed ordering"""
self.client.force_login(self.user)
for order in UserProfileView.allowed_ordering:
# Ascending
response = self.client.get(self.user.get_absolute_url() + "?sort=" + order)
self.assertEqual(response.status_code, 200)

# Descending
response = self.client.get(self.user.get_absolute_url() + "?sort=-" + order)
self.assertEqual(response.status_code, 200)

@tag("privacy")
def test_user_profile_htmx_search_view_does_not_show_private_trips(self):
"""Test that the user profile HTMX search view does not show private trips"""
self.client.force_login(self.user)
trip = TripFactory(user=self.user2, privacy=Trip.PUBLIC)
response = self.client.post(
reverse("log:user_search", args=[self.user2.username]),
{"query": trip.cave_name},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, trip.get_absolute_url())

trip.privacy = Trip.PRIVATE
trip.save()
response = self.client.post(
reverse("log:user_search", args=[self.user2.username]),
{"query": trip.cave_name},
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, trip.get_absolute_url())

@tag("privacy")
def test_user_profile_htmx_search_view_does_not_show_friend_only_trips(self):
"""Test that the user profile search view does not show friend only trips"""
# Test the trip is shown when public
self.client.force_login(self.user)
trip = TripFactory(user=self.user2, privacy=Trip.PUBLIC)
response = self.client.post(
reverse("log:user_search", args=[self.user2.username]),
{"query": trip.cave_name},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, trip.get_absolute_url())

# Test the trip is not shown when friends only
trip.privacy = Trip.FRIENDS
trip.save()
response = self.client.post(
reverse("log:user_search", args=[self.user2.username]),
{"query": trip.cave_name},
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, trip.get_absolute_url())

# Test the trip is shown when friends
self.user.friends.add(self.user2)
self.user2.friends.add(self.user)

response = self.client.post(
reverse("log:user_search", args=[self.user2.username]),
{"query": trip.cave_name},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, trip.get_absolute_url())

@tag("privacy")
def test_user_profile_htmx_search_view_respects_profile_privacy(self):
"""Test that the user profile search view respects profile privacy"""
self.user2.privacy = User.PRIVATE
self.user2.save()

self.client.force_login(self.user)
response = self.client.post(
reverse("log:user_search", args=[self.user2.username]), {"query": "query"}
)
self.assertEqual(response.status_code, 403)

@tag("privacy")
def test_cave_location_privacy(self):
"""Test that the cave location shows for a user"""
Expand Down
6 changes: 3 additions & 3 deletions app/logger/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
path("", views.Index.as_view(), name="index"),
path("u/<slug:username>/", views.UserProfile.as_view(), name="user"),
path(
"u/<slug:username>/search/,",
views.HTMXTripListSearchView.as_view(),
name="user_search",
"u/<slug:username>/trip-table/",
views.ProfileTripsTable.as_view(),
name="profile_trips_table",
),
path("trips/", views.TripsRedirect.as_view(), name="trip_list"),
path("trip/edit/<uuid:uuid>/", views.TripUpdate.as_view(), name="trip_update"),
Expand Down
168 changes: 83 additions & 85 deletions app/logger/views/userprofile.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.generic import ListView
from django.views.generic import ListView, TemplateView
from django_ratelimit.decorators import ratelimit
from stats import statistics
from users.models import CavingUser as User
Expand All @@ -12,11 +11,55 @@


@method_decorator(ratelimit(key="user_or_ip", rate="500/h"), name="dispatch")
class UserProfile(ListView):
class UserProfile(TemplateView):
"""List all of a user's trips and their profile information"""

model = Trip
template_name = "logger/profile.html"
slug_field = "username"

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.profile_user = None

def setup(self, *args, **kwargs):
"""Assign self.profile_user and perform permissions checks"""
super().setup(*args, **kwargs)
self.profile_user = get_object_or_404(User, username=self.kwargs["username"])

def get_context_data(self, **kwargs):
context = super().get_context_data()
context["profile_user"] = self.profile_user
context["page_title"] = self.get_page_title()
context["mutual_friends"] = self.profile_user.mutual_friends(self.request.user)
context["user_has_trips"] = self.profile_user.trips.exists()

if self.request.user not in self.profile_user.friends.all():
if self.profile_user.allow_friend_username:
context["can_add_friend"] = True

if not self.profile_user.is_viewable_by(self.request.user):
context["private_profile"] = True

if self.profile_user.public_statistics:
context["stats"] = statistics.yearly(
self.profile_user.trips.exclude(type=Trip.SURFACE)
)

return context

def get_page_title(self):
if self.profile_user.page_title:
return self.profile_user.page_title
else:
return f"{self.profile_user.name}'s trips"


class ProfileTripsTable(ListView):
"""List all of a user's trips and their profile information"""

model = Trip
template_name = "logger/profile_trips_table.html"
context_object_name = "trips"
slug_field = "username"
paginate_by = 50
Expand All @@ -33,27 +76,46 @@ class UserProfile(ListView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.profile_user = None
self.allow_sort = True
self.query = None

def setup(self, *args, **kwargs):
"""Assign self.profile_user and perform permissions checks"""
super().setup(*args, **kwargs)
self.profile_user = get_object_or_404(User, username=self.kwargs["username"])

def get_queryset(self):
query = self.request.GET.get("query", "")
if len(query) < 3:
query = None
self.query = query

trips = (
Trip.objects.filter(user=self.profile_user)
.select_related("user")
.prefetch_related("photos")
.order_by(*self.get_ordering())
).annotate(
photo_count=Count(
"photos",
filter=Q(photos__is_valid=True, photos__deleted_at=None),
distinct=True,
),
comment_count=Count("comments", distinct=True),
)

if query:
distinct_query = self.get_ordering()
distinct_query = [x.replace("-", "") for x in distinct_query]

trips = (
trips.filter(
Q(cave_name__unaccent__icontains=query)
| Q(cave_entrance__unaccent__icontains=query)
| Q(cave_exit__unaccent__icontains=query)
| Q(cavers__name__unaccent__icontains=query)
| Q(clubs__unaccent__icontains=query)
| Q(expedition__unaccent__icontains=query)
)
.distinct(*distinct_query)
.order_by(*self.get_ordering())
)
self.allow_sort = False
else:
trips = trips.order_by(*self.get_ordering())

friends = self.profile_user.friends.all()

# Sanitise trips to be privacy aware
Expand All @@ -66,7 +128,7 @@ def get_queryset(self):
return trips

def get_ordering(self):
ordering = self.request.GET.get("sort", "")
ordering = self.request.GET.get("sort", "").lower()
if ordering.replace("-", "") in self.allowed_ordering:
return ordering, "pk"

Expand All @@ -75,20 +137,13 @@ def get_ordering(self):
def get_context_data(self, **kwargs):
context = super().get_context_data()
context["profile_user"] = self.profile_user
context["page_title"] = self.get_page_title()
context["mutual_friends"] = self.profile_user.mutual_friends(self.request.user)
context["show_cavers"] = self.profile_user.show_cavers_on_trip_list
if self.request.user not in self.profile_user.friends.all():
if self.profile_user.allow_friend_username:
context["can_add_friend"] = True

if self.profile_user.public_statistics:
context["stats"] = statistics.yearly(
self.profile_user.trips.exclude(type=Trip.SURFACE)
)

if not self.profile_user.is_viewable_by(self.request.user):
context["private_profile"] = True
context["htmx_url"] = reverse(
"log:profile_trips_table", args=[self.profile_user.username]
)
context["ordering"] = self.get_ordering()[0]
context["allow_sort"] = self.allow_sort
context["query"] = self.query

# This code provides the current GET parameters as a context variable
# so that when a pagination link is clicked, the GET parameters are
Expand All @@ -104,60 +159,3 @@ def get_page_title(self):
return self.profile_user.page_title
else:
return f"{self.profile_user.name}'s trips"


@method_decorator(
ratelimit(key="user_or_ip", rate="1000/h", method=ratelimit.UNSAFE), name="dispatch"
)
class HTMXTripListSearchView(View):
def __init__(self):
super().__init__()
self.profile_user = None

def setup(self, *args, **kwargs):
"""Assign self.profile_user and perform permissions checks"""
super().setup(*args, **kwargs)
self.profile_user = get_object_or_404(User, username=self.kwargs["username"])
if not self.profile_user.is_viewable_by(self.request.user):
raise PermissionDenied

def post(self, request, *args, **kwargs):
"""Return a list of trips matching the search query"""
query = request.POST.get("query", "")
if len(query) < 3:
return render(
request,
"logger/_htmx_trip_list_search.html",
{"trips": None, "query": query},
)

trips = (
Trip.objects.filter(
Q(user=self.profile_user)
& Q(
Q(cave_name__unaccent__icontains=query)
| Q(cave_entrance__unaccent__icontains=query)
| Q(cave_exit__unaccent__icontains=query)
| Q(cavers__name__unaccent__icontains=query)
| Q(clubs__unaccent__icontains=query)
| Q(expedition__unaccent__icontains=query)
)
)
.distinct("start", "pk")
.order_by("-start", "pk")[:20]
)

friends = self.profile_user.friends.all()

# Sanitise trips to be privacy aware
if not self.profile_user == self.request.user:
sanitised_trips = [
x for x in trips if x.is_viewable_by(self.request.user, friends)
]
trips = sanitised_trips

return render(
request,
"logger/_htmx_trip_list_search.html",
{"trips": trips, "query": query},
)
Loading

0 comments on commit 782c264

Please sign in to comment.