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
+ +
B. Location & Spatial Data (Community Science Observations)
+

Because WildeBackyard is a community science platform dedicated to ecological and wildlife research:

+ +
C. Information Collected Automatically
+ +

2. How We Use Your Information

+

We use the information we collect for the following purposes:

+ +

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
+ +
B. Scientific & Conservation Research Partners
+ +
C. Service Providers
+ +
D. Legal & Safety Requirements
+ +

4. Your Choices & Data Rights

+ +

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 %} + + +
+
+
1
+
Media
+
+
+
2
+
Time & Place
+
+
+
3
+
Species
+
+
+
4
+
Device
+
+
+
5
+
Preview
+
+
+

Step 1 of 5 — Media Upload

+
{% csrf_token %} - -
-

- Media Upload -

-
- - - Upload up to 5 images or videos (max 500MB each). Supported formats: JPEG, PNG, MP4, WebM, MOV, AVI -
-
-
- -
-

- Species Identification -

-

- Add up to 5 species observed in this sighting. The first entry is the primary identification. -

-
- -
-
- - 1 of 5 max + + +
+
+

+ Media Upload +

+
+ + + Upload up to 5 images or videos (max 500MB each). Supported formats: JPEG, PNG, MP4, WebM, MOV, AVI +
+
+
- -
-

- Location -

-
- - - Select a previously saved location or enter coordinates manually below. Manage saved locations -
-
-
- - + + +
+
+

+ Time & Place +

+
+ +
+ + + + +
-
- - +
+ + + Select a previously saved location, or switch to "New Location" to enter one manually. Manage saved locations
-
-
- +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ + Auto-filled from your media. Camera clock off? Adjust it below. + +
+ + +
+
+

If your camera time was incorrect, record the error here:

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
-
-
-
- - + + How did you observe this wildlife?
-
- +
+ + max="9999" + value="1"> + How many individual animals were visible? +
+

+ Add up to 5 species observed in this sighting. The first entry is the primary identification. +

+
+ +
+
+ + 1 of 5 max +
+
+ +
-
-
- -
- -
-

- When Did You See It? -

-
-
- - + + +
+
+

+ Device & Credit +

+
+ +
+ + + + + + + + +
+
+
+ +
+ + + + +
+
+ +
-
- - + + + id="camera_model" + name="camera_model" + placeholder="e.g., Canon EOS 5D Mark IV"> + Detected automatically from photo EXIF data when available.
-
- -
- -
-

- Description -

-
- - - 0/200 characters -
-
- - - 0/2000 characters -
-
- -
-

- Additional Information -

-
- - - How did you observe this wildlife? -
-
- - - How many individual animals were visible? -
-
- - -
-
- - -
-
- -

If your camera time was incorrect, record the error here:

-
-
- +
+
+ + + License applied to this sighting's data and media. Your default is set on your profile page. +
+
+
+ + id="new_device_name" + placeholder="e.g., Backyard Trail Cam" + maxlength="100">
-
- - +
+ +
-
-
- +
+
+ + +
+
+
+
+ + +
+
+

+ Preview & Publish +

+
+
+ +
+
+ + class="form-control feed-title-input" + id="post_title" + name="post_title" + maxlength="200" + required + placeholder="Give your sighting a descriptive title"> + 0/200 characters + + + + 0/2000 characters +
+ Not specified +
+
+ Species not specified + · + Camera not specified + · + — +
-
- +
+
+
+ + +
+
+ + class="form-control" + id="attribution_override" + name="attribution_override" + placeholder="Leave blank to use your display name">
-
-
- - - License applied to this sighting's data and media. Your default is set on your profile page. -
-
- - - Custom name to use for attribution (e.g. your organization). Leave blank to use your display name. +
+
+
+
+ +
+ + + + +
+
+ + + Used only when Public Setting is Approximate. +
+
+ + + +
+
+
+
+ + +
+ {% if is_expert %} - + +

+ Optional expert tool below — works independently of the steps above. +

Bulk Upload Expert @@ -510,20 +874,57 @@

{% endif %} - -
- + Cancel + + - - Cancel -
+ +