Skip to content
Open
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
12 changes: 12 additions & 0 deletions siteapps/socialmedia/test_web_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,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
Expand All @@ -28,6 +29,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",
Expand Down Expand Up @@ -76,6 +80,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):
Expand Down Expand Up @@ -548,6 +557,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):
Expand Down
8 changes: 6 additions & 2 deletions siteapps/socialmedia/web_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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

Expand Down Expand Up @@ -48,7 +49,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")
Expand Down Expand Up @@ -677,7 +681,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),
Expand Down
28 changes: 24 additions & 4 deletions siteapps/templates/socialmedia/feed.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "_base.html" %}
{% load static %}
{% load humanize %}
{% block title %}
Wildlife Feed - WildeBackyard
{% endblock title %}
Expand Down Expand Up @@ -340,7 +341,7 @@ <h5 class="card-title">{{ post.title }}</h5>
<p class="card-text text-muted small">
<i class="bi bi-person"></i> {{ post.user_name|default:"Anonymous" }}
<span class="mx-2">•</span>
<i class="bi bi-calendar"></i> {{ post.created|date:"M d, Y" }}
<i class="bi bi-calendar"></i> {{ post.created|naturaltime }}
</p>
{% if post.body %}<p class="card-text">{{ post.body|truncatewords:30 }}</p>{% endif %}
{% if post.geocoded_location %}
Expand Down Expand Up @@ -410,9 +411,28 @@ <h5 class="card-title">{{ post.title }}</h5>

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) {
Expand Down
Loading