Skip to content

Commit

Permalink
Add dynamic JavaScript trip list (#220)
Browse files Browse the repository at this point in the history
This brings caves.app to version 2.0.1. See CHANGELOG.md for details.
  • Loading branch information
anorthall authored Dec 19, 2023
1 parent dc1b071 commit ffd8e8d
Show file tree
Hide file tree
Showing 25 changed files with 1,943 additions and 346 deletions.
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
# Changes to caves.app
A summary of changes made to caves.app, organised by version, can be found below.

## Version 2.0
## Version 2.0.1
Version 2.0.1 of caves.app was released on Tuesday, 19th December 2023.

### Changes
#### User profiles
- The trip list has been redesigned to allow easier viewing and management of trips.
- Column sorting has been improved and a bug with distance field sorting fixed.
- Users who disable distance or survey statistics will now see location related fields
in their trip list instead.
- Clicking a trip now opens a modal with details of the trip, removing the need to visit
the trip page to review details.
- An advanced search feature, accessed by clicking the caret on the right of the quick search
bar, has been added. Trips can be filtered and searched by specific fields.
- The ability to hide statistics from your profile has been removed. This feature originally existed
for aesthetic reasons, and since statistics are now in their own tab, the ability to hide them is no
longer required. Statistics will only be shown to people who can view your profile, in accordance with
your privacy settings.
- A profile view counter has been added, accessible via the quick stats in the right hand sidebar, or
via the info tab. Users can only view their own profile view count.
- A bug where no 'Add friend' link was shown on smaller screens has been fixed.
- A bug preventing the photos tab being shown when the user had less than 40 photos has been fixed.

#### Trip detail page
- A trip view counter has been added. Users can only view the view count for their own trips. The
view count will increment if trips are viewed in the trip feed, or by accessing the trip page. Trips
viewed via a modal from the user profile page do not increment the view count.
- The featured photo cropping tool will now show a loading spinner until such time the image to crop
has loaded.

#### Trip photos page
- The uploader will now redirect you to the trip detail page when all uploads have finished successfully.

#### Trip feed page
- A bug preventing photos showing on some trips has been fixed.

#### Cave map page
- A bug where some locations were not accepted for geocoding has been fixed.

#### Staff dashboard
- Additional statistics on recent user activity, anonymised for privacy purposes, have been added.

#### Account page
- A bug where the account page stated the profile was private, when it was not, has been fixed.

## Version 2.0.0
Version 2.0 of caves.app is the result of around a month of my free time and
consists of a complete stylistic and functional redesign of the application as
well as many new features and improvements to the user experience.
Expand Down
8 changes: 5 additions & 3 deletions app/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Union

from django.contrib.auth.models import AnonymousUser
from django.http import HttpRequest
from users.models import CavingUser


def get_user(request: HttpRequest) -> CavingUser:
assert request.user.is_authenticated
assert isinstance(request.user, CavingUser)
def get_user(request: HttpRequest) -> Union[AnonymousUser, CavingUser]:
assert isinstance(request.user, (CavingUser, AnonymousUser))
return request.user
19 changes: 0 additions & 19 deletions app/logger/tests/test_user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,25 +156,6 @@ def test_user_profile_page_with_various_privacy_settings(self):
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.user.name)

@tag("privacy")
def test_public_statistics_privacy_setting(self):
"""Test that the public statistics privacy setting works"""
for i in range(0, 50):
TripFactory(user=self.user)
self.user.public_statistics = False
self.user.save()

response = self.client.get(self.user.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, '<div class="profile-stats')

self.user.public_statistics = True
self.user.save()

response = self.client.get(self.user.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<div class="profile-stats')

def test_friends_appear_on_the_user_profile_page(self):
"""Test that friends appear on the user profile page when viewed by the user"""
self.client.force_login(self.user)
Expand Down
5 changes: 0 additions & 5 deletions app/logger/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
urlpatterns = [
path("", views.Index.as_view(), name="index"),
path("u/<slug:username>/", views.UserProfile.as_view(), name="user"),
path(
"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"),
path("trip/delete/<uuid:uuid>/", views.TripDelete.as_view(), name="trip_delete"),
Expand Down
152 changes: 37 additions & 115 deletions app/logger/views/userprofile.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from typing import Union
from typing import Optional

from django.db.models import Q
from django.contrib import messages
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.generic import ListView, TemplateView
from django.views.generic import TemplateView
from django_ratelimit.decorators import ratelimit
from stats import statistics
from users.models import CavingUser as User
Expand All @@ -22,137 +21,60 @@ class UserProfile(TemplateView):

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.profile_user: Union[User, None] = None
self.profile_user: Optional[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(self, request, *args, **kwargs):
self.profile_user.add_profile_view(self.request)
return super().get(request, *args, **kwargs)

def get_context_data(self, **kwargs):
context = super().get_context_data()
context["profile_user"] = self.profile_user
context["mutual_friends"] = self.profile_user.mutual_friends(self.request.user)
context["user_has_trips"] = self.profile_user.trips.exists()
context["photos"] = self.profile_user.get_photos(for_user=self.request.user)
context["quick_stats"] = self.profile_user.quick_stats
context["show_stats_link"] = self.profile_user == self.request.user
context["private_stats"] = self.profile_user == self.request.user

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


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 = 100
ordering = ("-start", "pk")
allowed_ordering = [
"start",
"cave_name",
"duration",
"type",
"vert_dist_up",
"vert_dist_down",
]

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

def get_trips(self, for_user: User) -> list[Trip]:
trips = (
Trip.objects.filter(user=self.profile_user)
.select_related("user")
.prefetch_related("photos", "cavers")
.order_by("-start")
)

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()
for trip in trips:
trip.total_surveyed_dist = trip.surveyed_dist + trip.resurveyed_dist

# 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)
]
return sanitised_trips
else:
# Remove trips that the user cannot view
if (self.profile_user == for_user) or for_user.is_superuser:
return trips
else:
friends = self.profile_user.friends.all()
return [x for x in trips if x.is_viewable_by(for_user, friends)]

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

return self.ordering

# noinspection PyTypeChecker
def get_context_data(self, **kwargs):
user: User = self.request.user
context = super().get_context_data()
context["profile_user"] = self.profile_user
context["show_cavers"] = self.profile_user.show_cavers_on_trip_list
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
# preserved (for sorting).
parameters = self.request.GET.copy()
parameters = parameters.pop("page", True) and parameters.urlencode()
context["get_parameters"] = parameters
context["mutual_friends"] = self.profile_user.mutual_friends(user)

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

if user.is_superuser or self.profile_user.is_viewable_by(user):
if user.is_superuser and not self.profile_user.is_viewable_by(user):
messages.warning(self.request, "Viewing profile in administrator mode.")

context["trips"] = self.get_trips(user)
context["trip_types"] = [x[1] for x in Trip.TRIP_TYPES]
context["photos"] = self.profile_user.get_photos(for_user=user)
context["quick_stats"] = self.profile_user.quick_stats
context["stats"] = statistics.yearly(
self.profile_user.trips.exclude(type=Trip.SURFACE)
)
context["enable_private_stats"] = (
self.profile_user == user
) or user.is_superuser
else:
context["private_profile"] = True

return context
29 changes: 6 additions & 23 deletions app/static/css/caves.app.css
Original file line number Diff line number Diff line change
Expand Up @@ -716,26 +716,8 @@ main {
margin-bottom: 0.75rem;
}

#tripsTable {
font-size: 0.9rem;
}

#tripsTable tbody:hover td {
background-color: var(--bs-table-hover-bg);
color: var(--bs-table-hover-color);
}

#tripsTable tbody tr:first-child td {
border-bottom: none;
}

#tripsTable tbody tr:last-child td, #tripsTable tbody tr:first-of-type td:first-of-type {
border-bottom: 1px solid var(--bs-border-color);
}

#searchInput.htmx-request {
opacity: 100% !important;
transition: unset !important;
#userTripsTable {
max-width: 100%;
}

.friend-card {
Expand Down Expand Up @@ -970,17 +952,18 @@ main.home {
/* Headers */
h1 {
font-size: 1.5rem !important;
margin-bottom: 1rem !important;
}

.modal-title {
margin-bottom: 0;
}

h2 {
font-size: 1.3rem !important;
margin-bottom: 0.75rem !important;
}

h3 {
font-size: 1.2rem !important;
margin-bottom: 0.75rem !important;
}

#changelog h1, #changelog h2, #changelog h3, .title-underline {
Expand Down
Loading

0 comments on commit ffd8e8d

Please sign in to comment.