From e5bfbad0cc96caef24893c79bd5310500070e4c1 Mon Sep 17 00:00:00 2001 From: irenecancode <196759302+irenecancode@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:37:44 -0700 Subject: [PATCH] fix: resolve frontend timestamp overwrite, and fix test fixtures --- siteapps/socialmedia/test_web_views.py | 12 ++++++++++ siteapps/socialmedia/web_views.py | 8 +++++-- siteapps/templates/socialmedia/feed.html | 28 ++++++++++++++++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/siteapps/socialmedia/test_web_views.py b/siteapps/socialmedia/test_web_views.py index 4405454..305cb0a 100644 --- a/siteapps/socialmedia/test_web_views.py +++ b/siteapps/socialmedia/test_web_views.py @@ -1,6 +1,7 @@ """Tests for siteapps/socialmedia/web_views.py""" import uuid +from datetime import datetime, timezone from unittest.mock import MagicMock, patch from django.contrib.messages import get_messages @@ -25,6 +26,9 @@ def make_user(email="viewer@example.com", is_staff=False, is_superuser=False): "species": [{"name": "Robin"}], "media": {"url": "https://example.com/img.jpg", "is_video": False}, "created_by": "testuser", + # Deliberately distinct from encounter_datetime so tests can catch _normalize_post + # falling back to the wrong field (see WildeBackyardBackend serialize_post). + "created": "2024-01-01T09:15:00Z", "encounter_datetime": "2024-01-01T12:00:00Z", "additional_info": {"camera_model": "Canon", "habitat_type": "Forest"}, "geocoded_location": "Portland, OR", @@ -73,6 +77,11 @@ def test_authenticated_shows_posts(self, mock_client_class): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["posts"]), 1) + # Must reflect the post's actual creation time, not encounter_datetime. + self.assertEqual( + response.context["posts"][0]["created"], + datetime(2024, 1, 1, 9, 15, tzinfo=timezone.utc), + ) @patch("siteapps.socialmedia.web_views.BackendAPIClient") def test_species_filter_passed_to_api(self, mock_client_class): @@ -545,6 +554,9 @@ def test_success_returns_posts(self, mock_client_class): data = response.json() self.assertIn("posts", data) self.assertFalse(data["has_more"]) + # JSON endpoint passes the raw ISO string through (JS parses it), and it + # must be the post's actual creation time, not encounter_datetime. + self.assertEqual(data["posts"][0]["created"], "2024-01-01T09:15:00Z") @patch("siteapps.socialmedia.web_views.BackendAPIClient") def test_api_failure_returns_500(self, mock_client_class): diff --git a/siteapps/socialmedia/web_views.py b/siteapps/socialmedia/web_views.py index 711a372..198a816 100644 --- a/siteapps/socialmedia/web_views.py +++ b/siteapps/socialmedia/web_views.py @@ -6,6 +6,7 @@ from django.http import JsonResponse from django.shortcuts import redirect, render from django.urls import reverse +from django.utils.dateparse import parse_datetime from siteapps.users.api_client import BackendAPIClient @@ -45,7 +46,10 @@ def _normalize_post(post): post["media_url"] = media.get("url") post["is_video"] = media.get("is_video", False) post["user_name"] = post.get("created_by") - post["created"] = post.get("encounter_datetime") + # API returns "created" as an ISO string; the |date: template filter silently + # renders "" for a raw string, so it must be parsed into a datetime here. + raw_created = post.get("created") + post["created"] = parse_datetime(raw_created) if isinstance(raw_created, str) else raw_created additional = post.get("additional_info") or {} post["camera_model"] = additional.get("camera_model") post["iucn_habitat_lvl1"] = additional.get("iucn_habitat_lvl1_name") @@ -674,7 +678,7 @@ def load_more_posts(request): "media_url": media_url, "is_video": is_video, "user_name": post.get("created_by"), - "created": post.get("encounter_datetime"), + "created": post.get("created"), "geocoded_location": post.get("geocoded_location"), "likes_count": post.get("likes_count", 0), "comments_count": post.get("comments_count", 0), diff --git a/siteapps/templates/socialmedia/feed.html b/siteapps/templates/socialmedia/feed.html index 0d46936..ecb1e96 100644 --- a/siteapps/templates/socialmedia/feed.html +++ b/siteapps/templates/socialmedia/feed.html @@ -1,5 +1,6 @@ {% extends "_base.html" %} {% load static %} +{% load humanize %} {% block title %} Wildlife Feed - WildeBackyard {% endblock title %} @@ -340,7 +341,7 @@
{{ post.title }}

{{ post.user_name|default:"Anonymous" }} • - {{ post.created|date:"M d, Y" }} + {{ post.created|naturaltime }}

{% if post.body %}

{{ post.body|truncatewords:30 }}

{% endif %} {% if post.geocoded_location %} @@ -410,9 +411,28 @@
{{ post.title }}
function formatDate(dateString) { if (!dateString) return ''; - const date = new Date(dateString); - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - return date.toLocaleDateString('en-US', options); + const diffSeconds = Math.floor((Date.now() - new Date(dateString).getTime()) / 1000); + if (diffSeconds < 1) return 'now'; + + // Mirrors Django's humanize naturaltime filter (used for the initial page load) + // so posts loaded via infinite scroll read the same way. + const DAY = 86400; + const steps = [ + [60, 1, 'a second ago', 'seconds'], + [3600, 60, 'a minute ago', 'minutes'], + [DAY, 3600, 'an hour ago', 'hours'], + [DAY * 7, DAY, '1 day ago', 'days'], + [DAY * 30, DAY * 7, '1 week ago', 'weeks'], + [DAY * 365, DAY * 30, '1 month ago', 'months'], + [Infinity, DAY * 365, '1 year ago', 'years'], + ]; + + for (const [limit, divisor, singular, unit] of steps) { + if (diffSeconds < limit) { + const n = Math.floor(diffSeconds / divisor); + return n <= 1 ? singular : `${n} ${unit} ago`; + } + } } function truncateWords(text, wordLimit) {