diff --git a/siteapps/sightings/tests.py b/siteapps/sightings/tests.py
index ecc568b..56f16ef 100644
--- a/siteapps/sightings/tests.py
+++ b/siteapps/sightings/tests.py
@@ -96,6 +96,7 @@ def test_post_missing_title_shows_error(self, mock_geocode, mock_client_class):
"encounter_date": "2024-01-01",
"location_latitude": "45.0",
"location_longitude": "-93.0",
+ "privacy_accepted": "1",
},
)
msgs = [str(m) for m in get_messages(response.wsgi_request)]
@@ -116,6 +117,7 @@ def test_post_missing_datetime_shows_error(self, mock_geocode, mock_client_class
"post_title": "Bird sighting",
"location_latitude": "45.0",
"location_longitude": "-93.0",
+ "privacy_accepted": "1",
},
)
msgs = [str(m) for m in get_messages(response.wsgi_request)]
@@ -135,6 +137,7 @@ def test_post_missing_location_shows_error(self, mock_geocode, mock_client_class
{
"post_title": "Bird",
"encounter_date": "2024-01-01",
+ "privacy_accepted": "1",
},
)
msgs = [str(m) for m in get_messages(response.wsgi_request)]
@@ -166,6 +169,7 @@ def test_successful_submission_redirects_to_feed(self, mock_geocode, mock_client
"privacy_setting": "public",
"location_accuracy_meters": "5",
"species_list": ["Robin"],
+ "privacy_accepted": "1",
},
)
self.assertRedirects(response, reverse("socialmedia:feed"), fetch_redirect_response=False)
@@ -187,6 +191,7 @@ def test_api_submission_failure_shows_error(self, mock_geocode, mock_client_clas
"encounter_date": "2024-01-01",
"location_latitude": "45.5",
"location_longitude": "-122.7",
+ "privacy_accepted": "1",
},
)
msgs = [str(m) for m in get_messages(response.wsgi_request)]
@@ -208,6 +213,7 @@ def test_invalid_coordinates_shows_error(self, mock_geocode, mock_client_class):
"encounter_date": "2024-01-01",
"location_latitude": "not_a_number",
"location_longitude": "-93.0",
+ "privacy_accepted": "1",
},
)
msgs = [str(m) for m in get_messages(response.wsgi_request)]
@@ -215,6 +221,43 @@ def test_invalid_coordinates_shows_error(self, mock_geocode, mock_client_class):
any("latitude" in m.lower() or "invalid" in m.lower() or "longitude" in m.lower() for m in msgs)
)
+ @patch("siteapps.sightings.views.BackendAPIClient")
+ @patch("siteapps.sightings.views.reverse_geocode_with_nominatim")
+ def test_post_without_privacy_consent_is_rejected(self, mock_geocode, mock_client_class):
+ self._login_with_token()
+ mock_geocode.return_value = None
+ mock_api = MagicMock()
+ mock_api.get.return_value = {"species_names": []}
+ mock_client_class.return_value = mock_api
+
+ response = self.client.post(
+ self.url,
+ {
+ "post_title": "Bird sighting",
+ "encounter_date": "2024-01-01",
+ "encounter_time": "10:00",
+ "location_latitude": "45.5",
+ "location_longitude": "-122.7",
+ "privacy_setting": "public",
+ },
+ )
+
+ self.assertEqual(response.status_code, 200)
+ mock_api.post.assert_not_called()
+ msgs = [str(m) for m in get_messages(response.wsgi_request)]
+ self.assertTrue(any("privacy policy" in m.lower() for m in msgs))
+
+ def test_form_renders_privacy_consent_checkbox(self):
+ self._login_with_token()
+ response = self.client.get(self.url)
+ self.assertContains(response, 'name="privacy_accepted"')
+ self.assertContains(response, "privacyPolicyModal")
+ # The policy text itself is inlined in the modal, not just linked.
+ self.assertContains(response, "1. Information We Collect")
+ self.assertContains(response, "Last Updated:")
+ # A template placeholder must never reach a consent-gated document.
+ self.assertNotContains(response, "[Insert Contact Email]")
+
class MySightingsViewTests(TestCase):
def setUp(self):
diff --git a/siteapps/sightings/views.py b/siteapps/sightings/views.py
index 8ae0bdd..90e978f 100644
--- a/siteapps/sightings/views.py
+++ b/siteapps/sightings/views.py
@@ -70,6 +70,12 @@ def post(self, request):
messages.error(request, "Authentication required.")
return redirect("users:login")
+ # Consent is enforced here as well as in the form, so a direct POST
+ # cannot skip it.
+ if not request.POST.get("privacy_accepted"):
+ messages.error(request, "You must accept the Privacy Policy before publishing a sighting.")
+ return self.get(request)
+
# Extract form data and transform to camelCase format for backend API
encounter_date = request.POST.get("encounter_date")
encounter_time = request.POST.get("encounter_time", "12:00")
@@ -142,6 +148,8 @@ def post(self, request):
data["obfuscationKilometers"] = obfuscation_km
elif data.get("privacySetting") == "obscured":
data["obfuscationKilometers"] = 2 # Default 2km when obscured and not explicitly set
+ if request.POST.get("device_type"):
+ data["deviceType"] = request.POST.get("device_type")
if request.POST.get("camera_model"):
data["cameraModel"] = request.POST.get("camera_model")
if request.POST.get("camera_deployment_date"):
diff --git a/siteapps/templates/components/privacy_policy_content.html b/siteapps/templates/components/privacy_policy_content.html
new file mode 100644
index 0000000..e3373bb
--- /dev/null
+++ b/siteapps/templates/components/privacy_policy_content.html
@@ -0,0 +1,207 @@
+{% comment %}
+WildeBackyard privacy policy body — text only, no page chrome and no colour
+overrides, so it renders correctly inside a modal or a standalone page.
+
+Source: "Privacy Policy for WildeBackyard", effective 06/29/2026, last updated
+07/27/2026. Note that wildepod.org (backyard/privacy.html) still serves the
+superseded 07/04/2024 policy and needs the same update.
+{% endcomment %}
+
+ Effective Date: 06/29/2026
+
+ Last Updated: 07/27/2026
+
+
+ At WildeBackyard (“we,” “us,” or “our”), a community science and social sharing initiative operated
+ in association with the Felidae Conservation Fund, we are committed to respecting your privacy while advancing
+ wildlife research and conservation.
+
+
+ This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use the
+ WildeBackyard website, mobile applications, and associated services (collectively, the “Platform”).
+
+
+ Please read this Privacy Policy carefully. By accessing or using the Platform, you acknowledge that you have read,
+ understood, and agree to the practices described in this policy.
+
+
+1. Information We Collect
+
+ We collect information directly from you, automatically through your use of the Platform, and from third-party
+ sources.
+
+A. Information You Provide Directly
+
+ -
+ Account Information: Name, username, email address, password, profile picture, and optional bio
+ when you register for an account.
+
+ -
+ Community Science & Social Uploads: Wildlife photos, video recordings, audio files,
+ observation notes, species tags, comments, likes, and community forum posts you contribute to the Platform.
+
+ -
+ Donations & Financial Transactions: If you make a donation or purchase through the Platform,
+ we collect billing details, donation amounts, and contact information. Payment processing details (such as
+ credit card numbers) are handled directly by secure third-party payment processors and are not stored on our
+ servers.
+
+ -
+ Communications: Information you provide when contacting us for support, participating in
+ surveys, or subscribing to updates.
+
+
+B. Location & Spatial Data (Community Science Observations)
+Because WildeBackyard is a community science platform dedicated to ecological and wildlife research:
+
+ -
+ Geolocation & EXIF Metadata: When you upload wildlife photos or log sightings, we collect
+ precise or approximate geographic coordinates (latitude and longitude), elevation, date, and timestamp from your
+ device or media files.
+
+ -
+ Privacy Controls for Sensitive Locations: You may have the option to obscure or "blur" precise
+ observation locations (e.g., sightings on private property or involving sensitive/threatened species) according
+ to your platform preferences.
+
+
+C. Information Collected Automatically
+
+ -
+ Device & Usage Data: IP address, browser type, operating system, unique device identifiers,
+ pages viewed, time spent on pages, and navigation paths.
+
+ -
+ Cookies and Tracking Technologies: We use cookies, pixels, and similar tools to maintain session
+ state, remember user preferences, and analyze platform usage.
+
+
+2. How We Use Your Information
+We use the information we collect for the following purposes:
+
+ -
+ Platform Operation & Social Sharing: To create and manage your account, display your public
+ profile and shared observations, facilitate social interaction (comments, likes, shares), and deliver core
+ features.
+
+ -
+ Scientific Research & Wildlife Conservation: To aggregate, analyze, and map wildlife
+ sighting data. This data helps researchers, conservation scientists, and ecological partners monitor
+ biodiversity, study wildlife corridors, and advance non-profit conservation efforts.
+
+ -
+ Community Engagement: To notify you about platform activity, updates on research projects you
+ contribute to, community challenges, and conservation news.
+
+ -
+ Processing Donations: To process contributions, issue tax receipts, and communicate regarding
+ fundraising initiatives.
+
+ -
+ Platform Security & Improvement: To maintain network security, troubleshoot technical
+ issues, prevent fraudulent activity, and enhance user experience.
+
+ -
+ Legal Compliance: To fulfill legal obligations and enforce our Terms of Service.
+
+
+3. How We Share Your Information
+
+ We do not sell your personal identification information to third parties. We share information only in the following
+ contexts:
+
+A. Public & Community Sharing
+
+ -
+ Public Sighting Data: Observations, species tags, non-obscured location data, photos, usernames,
+ and profile details you choose to share publicly on the Platform will be visible to other users and visitors.
+
+ -
+ Obscured / Private Observations: If you mark a location as private or if a species is flagged as
+ sensitive, location data displayed publicly may be generalized or obscured.
+
+
+B. Scientific & Conservation Research Partners
+
+ -
+ Community science data (including observation locations, timestamps, and media) may be shared with scientific
+ partners, universities, wildlife agencies, and affiliated conservation organizations (including the Felidae
+ Conservation Fund) for non-commercial research, environmental modeling, and habitat protection.
+
+
+C. Service Providers
+
+ -
+ We share data with trusted third-party vendors who assist us in operating the Platform (e.g., cloud hosting, data
+ analytics, email delivery, customer support, and payment gateways). These providers are bound by strict
+ confidentiality obligations.
+
+
+D. Legal & Safety Requirements
+
+ -
+ We may disclose information if required by law, court order, or government regulation, or if we believe in good
+ faith that disclosure is necessary to protect the rights, property, or safety of WildeBackyard, our users,
+ wildlife, or the public.
+
+
+4. Your Choices & Data Rights
+
+ -
+ Account & Profile Settings: You can review, update, or edit your account information and
+ profile visibility settings at any time through your account settings.
+
+ -
+ Location Controls: You can control location permissions via your device settings or choose to
+ blur/obscure specific observation locations prior to publishing.
+
+ -
+ Email Communications: You can opt out of promotional emails or newsletters by clicking the
+ "Unsubscribe" link in any promotional message. Essential transactional and account updates will still be sent.
+
+ -
+ Data Deletion: You may request the deletion of your account and personal data by contacting us
+ at privacy@felidaefund.org. Please note that anonymized or
+ aggregated community science research data previously contributed to research databases may be retained for
+ scientific integrity.
+
+
+5. Data Security
+
+ We implement appropriate technical and organizational security measures to safeguard your personal information
+ against unauthorized access, loss, alteration, or misuse. However, no internet transmission or electronic storage
+ method is 100% secure, and we cannot guarantee absolute security.
+
+6. Children’s Privacy
+
+ WildeBackyard is not intended for children under the age of 13 (or 16 in certain jurisdictions) without parental
+ consent. We do not knowingly collect personal information directly from children under these ages. If you believe a
+ child has provided us with personal information without parental consent, please contact us so we can take
+ appropriate steps to remove the information.
+
+7. Third-Party Links & Services
+
+ The Platform may contain links to third-party websites or services not operated by WildeBackyard (including partner
+ conservation sites). We are not responsible for the privacy practices or content of third-party websites. We
+ encourage you to review the privacy policies of any site you visit.
+
+8. Updates to This Privacy Policy
+
+ We may update this Privacy Policy from time to time to reflect changes in our practices, platform features, or legal
+ requirements. When we post updates, we will revise the "Last Updated" date at the top of this page. For material
+ changes, we will notify you through the Platform or via email.
+
+9. Contact Us
+
+ If you have any questions, concerns, or requests regarding this Privacy Policy or our data practices, please contact
+ us at:
+
+
+ WildeBackyard / Felidae Conservation Fund
+
+ Email: privacy@felidaefund.org / support@felidaefund.org
+
+ Mailing Address: 655 Redwood Hwy, Suite 150, Mill Valley, CA 94941
+
+ Website: https://felidaefund.org / https://wildebackyard.com
+
diff --git a/siteapps/templates/sightings/create_sighting.html b/siteapps/templates/sightings/create_sighting.html
index fe89931..de8d8be 100644
--- a/siteapps/templates/sightings/create_sighting.html
+++ b/siteapps/templates/sightings/create_sighting.html
@@ -36,6 +36,191 @@
.required-field::after {
content: " *";
color: red;
+ }
+ .date-field-locked {
+ background-color: #e9ecef !important;
+ color: #495057;
+ }
+ .media-remove-btn {
+ width: 1.75rem;
+ height: 1.75rem;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.35);
+ border: none;
+ color: rgba(255, 255, 255, 0.75);
+ opacity: 0.55;
+ transition: opacity 0.15s ease, background-color 0.15s ease;
+ }
+ .media-remove-btn i {
+ font-size: 0.8rem;
+ }
+ .media-remove-btn:hover,
+ .media-remove-btn:focus {
+ opacity: 1;
+ background: rgba(220, 53, 69, 0.85);
+ color: #fff;
+ }
+
+ /* ---------------- Wizard progress bar ---------------- */
+ .wizard-progress {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ padding: 0 0.5rem;
+ }
+ .wizard-progress .wp-step {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+ position: relative;
+ cursor: default;
+ }
+ .wizard-progress .wp-step.completed {
+ cursor: pointer;
+ }
+ .wizard-progress .wp-step .wp-circle {
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 50%;
+ background: rgba(255,255,255,0.15);
+ border: 2px solid rgba(255,255,255,0.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ color: #fff;
+ transition: all 0.2s ease;
+ position: relative;
+ z-index: 2;
+ }
+ .wizard-progress .wp-step .wp-label {
+ margin-top: 0.5rem;
+ font-size: 0.8rem;
+ text-align: center;
+ color: #fff;
+ font-weight: 500;
+ max-width: 6rem;
+ }
+ .wizard-progress .wp-step:not(:last-child)::after {
+ content: '';
+ position: absolute;
+ top: 1.25rem;
+ left: 50%;
+ width: 100%;
+ height: 2px;
+ background: rgba(255,255,255,0.3);
+ z-index: 1;
+ }
+ .wizard-progress .wp-step.active .wp-circle {
+ background: var(--primary-light, #69a68a);
+ border-color: var(--primary-light, #69a68a);
+ box-shadow: 0 0 0 4px rgba(105,166,138,0.3);
+ }
+ .wizard-progress .wp-step.completed .wp-circle {
+ background: var(--primary-dark, #34483f);
+ border-color: var(--primary-dark, #34483f);
+ }
+ .wizard-progress .wp-step.completed:not(:last-child)::after {
+ background: var(--primary-dark, #34483f);
+ }
+ .wizard-progress .wp-step .wp-check { display: none; }
+ .wizard-progress .wp-step.completed .wp-num { display: none; }
+ .wizard-progress .wp-step.completed .wp-check { display: inline; }
+ .wizard-step { animation: wizardFadeIn 0.25s ease; }
+ @keyframes wizardFadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ .preview-card {
+ background: #f3f8f5;
+ border: 1px solid var(--primary-light, #69a68a);
+ border-radius: 0.5rem;
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+ }
+ /* Feed-style preview card — mirrors the sighting feed card layout so
+ Preview & Publish reads like the real post: media on top, then title,
+ byline, description and location, all editable in place. */
+ .feed-preview-card {
+ padding: 0;
+ overflow: hidden;
+ background: #fff;
+ }
+ .feed-card-media {
+ position: relative;
+ width: 100%;
+ height: 220px;
+ background: #dcdfe1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #8a9096;
+ overflow: hidden;
+ }
+ .feed-card-media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ .feed-video-badge {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ background: rgba(0, 0, 0, 0.65);
+ color: #fff;
+ border-radius: 999px;
+ padding: 0.2rem 0.65rem;
+ font-size: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ }
+ .feed-play-btn {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 3.25rem;
+ height: 3.25rem;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.4);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.4rem;
+ }
+ .feed-card-body {
+ padding: 1.25rem;
+ }
+ .feed-title-input {
+ border: none;
+ padding: 0.15rem 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #1a1a1a;
+ }
+ .feed-title-input:focus {
+ box-shadow: none;
+ outline: 1px dashed var(--primary-light, #69a68a);
+ outline-offset: 2px;
+ }
+ .feed-description-input {
+ border: none;
+ padding: 0.15rem 0;
+ resize: vertical;
+ color: #333;
+ }
+ .feed-description-input:focus {
+ box-shadow: none;
+ outline: 1px dashed var(--primary-light, #69a68a);
+ outline-offset: 2px;
}
{% endblock header_includes %}
@@ -53,312 +238,491 @@ Report a Wildlife Sighting
{% endfor %}
{% endif %}
+
+
+
+ Step 1 of 5 — Media Upload
+