• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gcivil-nyu-org / team4-wed-spring25 / 627

23 Apr 2025 05:47PM UTC coverage: 92.893% (-0.5%) from 93.39%
627

push

travis-pro

mr2447
merge with develop

1477 of 1590 relevant lines covered (92.89%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.86
/parks/tests.py
1
from django.test import TestCase, Client
1✔
2
from django.urls import reverse
1✔
3
from django.contrib.auth.models import User
1✔
4
from .models import DogRunNew, Review, ParkImage, ReviewReport, ImageReport, Reply
1✔
5
from parks.templatetags.display_rating import render_stars
1✔
6
from parks.templatetags import image_filters
1✔
7
from django.utils.text import slugify
1✔
8
from django.contrib.messages import get_messages
1✔
9

10
from django.utils import timezone
1✔
11
from datetime import timedelta
1✔
12
from parks.models import ParkPresence
1✔
13

14
from unittest.mock import patch
1✔
15
from django.core.files.uploadedfile import SimpleUploadedFile
1✔
16

17
from .models import Message
1✔
18

19
from cloudinary import config as cloudinary_config
1✔
20

21

22
@patch(
1✔
23
    "cloudinary.uploader.upload",
24
    return_value={
25
        "asset_id": "dummy_asset_id",
26
        "public_id": "dummy_id",
27
        "version": "1234567890",
28
        "signature": "dummy_signature",
29
        "width": 800,
30
        "height": 600,
31
        "format": "jpg",
32
        "resource_type": "image",
33
        "type": "upload",
34
        "secure_url": "https://res.cloudinary.com/demo/image/upload/sample.jpg",
35
        "url": "https://res.cloudinary.com/demo/image/upload/sample.jpg",
36
    },
37
)
38
class ErrorPageTests(TestCase):
1✔
39
    def test_trigger_400(self, _mock=None):
1✔
40
        response = self.client.get("/test400/")
1✔
41
        self.assertEqual(response.status_code, 400)
1✔
42
        self.assertTemplateUsed(response, "400.html")
1✔
43

44
    def test_trigger_403(self, _mock=None):
1✔
45
        response = self.client.get("/test403/")
1✔
46
        self.assertEqual(response.status_code, 403)
1✔
47
        self.assertTemplateUsed(response, "403.html")
1✔
48

49
    def test_trigger_404(self, _mock=None):
1✔
50
        response = self.client.get("/test404/")
1✔
51
        self.assertEqual(response.status_code, 404)
1✔
52
        self.assertTemplateUsed(response, "404.html")
1✔
53

54
    def test_trigger_500(self, _mock=None):
1✔
55
        response = self.client.get("/test500/")
1✔
56
        self.assertEqual(response.status_code, 500)
1✔
57
        self.assertTemplateUsed(response, "500.html")
1✔
58

59

60
class ParkModelTest(TestCase):
1✔
61
    def setUp(self):
1✔
62
        self.client = Client()
1✔
63
        self.park = DogRunNew.objects.create(
1✔
64
            id="1",
65
            prop_id="1234",
66
            name="Central Park",
67
            address="New York, NY",
68
            dogruns_type="Small",
69
            accessible="Yes",
70
            notes="Test park notes",
71
            google_name="Central Park",
72
            borough="M",
73
            zip_code="United States",
74
            formatted_address="Central Pk N, New York, NY, USA",
75
            latitude=40.7987768,
76
            longitude=-73.9537196,
77
            additional={
78
                "geometry": {
79
                    "bounds": {
80
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
81
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
82
                    },
83
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
84
                    "location_type": "GEOMETRIC_CENTER",
85
                    "viewport": {
86
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
87
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
88
                    },
89
                }
90
            },
91
            display_name="Central Park",
92
            slug=slugify(f"{'Central Park'}-{'1234'}"),
93
        )
94

95
    def test_park_creation(self):
1✔
96
        self.assertEqual(self.park.name, "Central Park")
1✔
97
        self.assertEqual(self.park.address, "New York, NY")
1✔
98
        self.assertEqual(self.park.notes, "Test park notes")
1✔
99
        self.assertEqual(self.park.slug, "central-park-1234")
1✔
100

101

102
class ReviewModelTest(TestCase):
1✔
103
    def setUp(self):
1✔
104
        self.user = User.objects.create_user(username="testuser", password="123456abc")
1✔
105
        self.park = DogRunNew.objects.create(
1✔
106
            id="2",
107
            prop_id="5678",
108
            name="Brooklyn Park",
109
            address="Brooklyn, NY",
110
            dogruns_type="Large",
111
            accessible="No",
112
            notes="Another test park",
113
            display_name="Brooklyn Park",
114
            slug=slugify(f"{'Brooklyn Park'}-{'5678'}"),
115
        )
116
        self.review = Review.objects.create(
1✔
117
            park=self.park, text="Great park!", rating=5, user=self.user
118
        )
119

120
    def test_review_creation(self):
1✔
121
        self.assertEqual(self.review.text, "Great park!")
1✔
122
        self.assertEqual(self.review.rating, 5)
1✔
123
        self.assertEqual(self.review.park.name, "Brooklyn Park")
1✔
124

125
    def test_review_str_method(self):
1✔
126
        self.assertEqual(str(self.review), "Review for Brooklyn Park (5 stars)")
1✔
127

128

129
class CombinedViewTest(TestCase):
1✔
130
    def test_combined_view(self):
1✔
131
        response = self.client.get(reverse("park_and_map"))
1✔
132
        self.assertEqual(response.status_code, 200)
1✔
133

134
    def setUp(self):
1✔
135
        self.client = Client()
1✔
136
        # One park in Manhattan
137
        self.park_manhattan = DogRunNew.objects.create(
1✔
138
            id="1",
139
            prop_id="1234",
140
            name="Central Park",
141
            address="New York, NY",
142
            dogruns_type="Small",
143
            accessible="Yes",
144
            notes="Manhattan park",
145
            google_name="Central Park",
146
            borough="M",
147
            zip_code="10024",
148
            latitude=40.7987768,
149
            longitude=-73.9537196,
150
            display_name="Central Park",
151
        )
152
        # One park in Brooklyn
153
        self.park_brooklyn = DogRunNew.objects.create(
1✔
154
            id="2",
155
            prop_id="5678",
156
            name="Brooklyn Bridge Park",
157
            address="Brooklyn, NY",
158
            dogruns_type="Large",
159
            accessible="Yes",
160
            notes="Brooklyn park",
161
            google_name="Brooklyn Bridge Park",
162
            borough="B",
163
            zip_code="11201",
164
            latitude=40.700292,
165
            longitude=-73.996123,
166
            display_name="Brooklyn Bridge Park",
167
        )
168

169
    def test_combined_view_filters_by_borough(self):
1✔
170
        response = self.client.get(reverse("park_and_map"), {"borough": "M"})
1✔
171
        self.assertEqual(response.status_code, 200)
1✔
172
        self.assertContains(response, "Central Park")
1✔
173
        self.assertNotContains(response, "Brooklyn Bridge Park")
1✔
174

175

176
class ParkDetailViewTest(TestCase):
1✔
177
    def setUp(self):
1✔
178
        """Set up the test client and create a test park."""
179
        self.client = Client()
1✔
180

181
        self.park = DogRunNew.objects.create(
1✔
182
            id="1",
183
            prop_id="1234",
184
            name="Central Park",
185
            address="New York, NY",
186
            dogruns_type="Small",
187
            accessible="Yes",
188
            notes="Test park notes",
189
            google_name="Central Park",
190
            borough="M",
191
            zip_code="United States",
192
            formatted_address="Central Pk N, New York, NY, USA",
193
            latitude=40.7987768,
194
            longitude=-73.9537196,
195
            additional={
196
                "geometry": {
197
                    "bounds": {
198
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
199
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
200
                    },
201
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
202
                    "location_type": "GEOMETRIC_CENTER",
203
                    "viewport": {
204
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
205
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
206
                    },
207
                }
208
            },
209
            display_name="Central Park",
210
            slug="central-park-1234",
211
        )
212

213
        self.park2 = DogRunNew.objects.create(
1✔
214
            id="2",
215
            prop_id="4321",
216
            name="Allison Pond Park",
217
            address="Staten Island",
218
            dogruns_type="Small",
219
            accessible="Yes",
220
            notes="Test park notes",
221
            google_name="Allison Pond Park",
222
            borough="Q",
223
            zip_code="United States",
224
            formatted_address="Allison Pond Park, Staten Island, NY 10301, USA",
225
            latitude=40.7987768,
226
            longitude=-73.9537196,
227
            display_name="Allison Pond Park",
228
            slug="allison-pond-park-4321",
229
        )
230

231
    def test_park_detail_page_loads(self):
1✔
232
        url = self.park.detail_page_url()
1✔
233
        response = self.client.get(url)
1✔
234
        self.assertEqual(response.status_code, 200)
1✔
235
        self.assertTemplateUsed(response, "parks/park_detail.html")
1✔
236
        self.assertContains(response, "Central Park")
1✔
237

238
    def test_redirect_on_wrong_slug(self):
1✔
239
        url = reverse("park_detail", kwargs={"slug": "wrong-slug", "id": self.park.id})
1✔
240
        response = self.client.get(url)
1✔
241
        self.assertEqual(response.status_code, 301)
1✔
242

243
        correct_response = url = reverse(
1✔
244
            "park_detail", kwargs={"slug": self.park.slug, "id": self.park.id}
245
        )
246

247
        self.assertRedirects(response, correct_response, status_code=301)
1✔
248

249
    def test_404_on_nonexistent_id(self):
1✔
250
        url = reverse("park_detail", kwargs={"slug": "central-park", "id": "-4"})
1✔
251
        response = self.client.get(url)
1✔
252
        self.assertEqual(response.status_code, 404)
1✔
253

254
    def test_redirect_on_wrong_id_right_slug(self):
1✔
255
        url = reverse(
1✔
256
            "park_detail", kwargs={"slug": "central-park", "id": self.park2.id}
257
        )
258
        response = self.client.get(url)
1✔
259

260
        self.assertEqual(response.status_code, 301)
1✔
261

262
        expected_url = reverse(
1✔
263
            "park_detail", kwargs={"slug": self.park2.slug, "id": self.park2.id}
264
        )
265
        self.assertRedirects(
1✔
266
            response, expected_url, status_code=301, target_status_code=200
267
        )
268

269
    from django.contrib.messages import get_messages
1✔
270

271

272
def test_submit_review_non_integer_rating(self):
1✔
273
    self.client.login(username="testuser", password="testpass")
×
274

275
    response = self.client.post(
×
276
        self.park.detail_page_url(),
277
        {
278
            "form_type": "submit_review",
279
            "text": "This should not go through.",
280
            "rating": "abc",
281
        },
282
        follow=True,
283
    )
284

285
    self.assertEqual(response.status_code, 200)
×
286

287
    messages = list(get_messages(response.wsgi_request))
×
288
    self.assertTrue(
×
289
        any("Please select a rating before submitting." in str(m) for m in messages),
290
        "Expected error message not found in messages.",
291
    )
292

293

294
class ReportFunctionalityTests(TestCase):
1✔
295
    def setUp(self):
1✔
296
        self.client = Client()
1✔
297
        self.user = User.objects.create_user(
1✔
298
            username="reporter", password="testpass123"
299
        )
300
        self.other_user = User.objects.create_user(
1✔
301
            username="uploader", password="testpass123"
302
        )
303

304
        self.park = DogRunNew.objects.create(
1✔
305
            id="10",
306
            prop_id="9999",
307
            name="Test Park",
308
            address="Test Address",
309
            dogruns_type="All",
310
            accessible="Yes",
311
            formatted_address="Test Address",
312
            latitude=40.0,
313
            longitude=-73.0,
314
        )
315

316
        self.image = ParkImage.objects.create(
1✔
317
            park=self.park,
318
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
319
            user=self.other_user,
320
        )
321

322
        self.review = Review.objects.create(
1✔
323
            park=self.park, text="Nice place!", rating=4, user=self.other_user
324
        )
325

326
        self.client.login(username="reporter", password="testpass123")
1✔
327

328
    def test_report_image_creates_record(self):
1✔
329
        response = self.client.post(
1✔
330
            reverse("report_image", args=[self.image.id]),
331
            {"reason": "Inappropriate image"},
332
        )
333
        self.assertEqual(response.status_code, 302)
1✔
334
        self.assertEqual(self.image.reports.count(), 1)
1✔
335
        report = self.image.reports.first()
1✔
336
        self.assertEqual(report.reason, "Inappropriate image")
1✔
337
        self.assertEqual(report.user, self.user)
1✔
338

339
    def test_report_review_creates_record(self):
1✔
340
        response = self.client.post(
1✔
341
            reverse("park_detail", args=[self.park.slug, self.park.id]),
342
            {
343
                "form_type": "report_review",
344
                "review_id": self.review.id,
345
                "reason": "Offensive content",
346
            },
347
        )
348
        self.assertEqual(response.status_code, 302)
1✔
349
        self.assertEqual(self.review.reports.count(), 1)
1✔
350
        report = self.review.reports.first()
1✔
351
        self.assertEqual(report.reason, "Offensive content")
1✔
352
        self.assertEqual(report.reported_by, self.user)
1✔
353

354
    def test_submit_review(self):
1✔
355
        response = self.client.post(
1✔
356
            reverse("park_detail", args=[self.park.slug, self.park.id]),
357
            {"form_type": "submit_review", "text": "Another review!", "rating": "5"},
358
        )
359
        self.assertEqual(response.status_code, 302)
1✔
360
        self.assertEqual(Review.objects.filter(park=self.park).count(), 2)
1✔
361

362
    def test_review_report_str(self):
1✔
363
        report = ReviewReport.objects.create(
1✔
364
            review=self.review, reported_by=self.user, reason="Inappropriate content"
365
        )
366
        self.assertIn("Reported by", str(report))
1✔
367
        self.assertIn(str(self.review.id), str(report))
1✔
368

369
    def test_image_report_str(self):
1✔
370
        report = ImageReport.objects.create(
1✔
371
            image=self.image, user=self.user, reason="Offensive image"
372
        )
373
        self.assertIn("Report by", str(report))
1✔
374
        self.assertIn(str(self.image.id), str(report))
1✔
375

376
    def test_missing_reason_does_not_create_report(self):
1✔
377
        self.client.login(username="user2", password="testpass")
1✔
378
        response = self.client.post(
1✔
379
            reverse("report_image", args=[self.image.id]),
380
            {"reason": ""},
381
        )
382
        self.assertEqual(ImageReport.objects.count(), 0)
1✔
383
        self.assertEqual(response.status_code, 302)
1✔
384

385
    def test_duplicate_review_report(self):
1✔
386
        # First report
387
        response1 = self.client.post(
1✔
388
            reverse("park_detail", args=[self.park.slug, self.park.id]),
389
            {
390
                "form_type": "report_review",
391
                "review_id": self.review.id,
392
                "reason": "Spam",
393
            },
394
        )
395
        self.assertEqual(response1.status_code, 302)
1✔
396
        self.assertEqual(self.review.reports.count(), 1)
1✔
397

398
        # Second report by same user
399
        response2 = self.client.post(
1✔
400
            reverse("park_detail", args=[self.park.slug, self.park.id]),
401
            {
402
                "form_type": "report_review",
403
                "review_id": self.review.id,
404
                "reason": "Still spam",
405
            },
406
        )
407
        self.assertEqual(response2.status_code, 302)
1✔
408
        self.assertEqual(self.review.reports.count(), 1)  # should still be 1
1✔
409

410
    def test_duplicate_image_report(self):
1✔
411
        # First report
412
        response1 = self.client.post(
1✔
413
            reverse("report_image", args=[self.image.id]),
414
            {"reason": "Bad image"},
415
        )
416
        self.assertEqual(response1.status_code, 302)
1✔
417
        self.assertEqual(self.image.reports.count(), 1)
1✔
418

419
        # Second report by same user
420
        response2 = self.client.post(
1✔
421
            reverse("report_image", args=[self.image.id]),
422
            {"reason": "Still bad"},
423
        )
424
        self.assertEqual(response2.status_code, 302)
1✔
425
        self.assertEqual(self.image.reports.count(), 1)  # should still be 1
1✔
426

427

428
class DeleteTests(TestCase):
1✔
429
    def setUp(self):
1✔
430
        self.client = Client()
1✔
431
        self.user = User.objects.create_user(username="deleter", password="123pass")
1✔
432
        self.client.login(username="deleter", password="123pass")
1✔
433

434
        self.park = DogRunNew.objects.create(
1✔
435
            id="22",
436
            prop_id="9988",
437
            name="Del Park",
438
            address="Somewhere",
439
            dogruns_type="All",
440
            accessible="Yes",
441
            formatted_address="Addr",
442
            latitude=40.0,
443
            longitude=-73.0,
444
        )
445
        self.review = Review.objects.create(
1✔
446
            park=self.park, text="Review", rating=4, user=self.user
447
        )
448
        self.image = ParkImage.objects.create(
1✔
449
            park=self.park,
450
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
451
            user=self.user,
452
        )
453

454
    def test_delete_review(self):
1✔
455
        response = self.client.post(reverse("delete_review", args=[self.review.id]))
1✔
456
        self.assertEqual(response.status_code, 302)
1✔
457
        self.assertFalse(Review.objects.filter(id=self.review.id).exists())
1✔
458

459
    def test_delete_image(self):
1✔
460
        response = self.client.post(reverse("delete_image", args=[self.image.id]))
1✔
461
        self.assertEqual(response.status_code, 302)
1✔
462
        self.assertFalse(ParkImage.objects.filter(id=self.image.id).exists())
1✔
463

464

465
class ParkImageModelTest(TestCase):
1✔
466
    def setUp(self):
1✔
467
        """Set up a test park and associated images."""
468
        self.park = DogRunNew.objects.create(
1✔
469
            id="1",
470
            prop_id="1234",
471
            name="Central Park",
472
            address="New York, NY",
473
            dogruns_type="Small",
474
            accessible="Yes",
475
            notes="Test park notes",
476
            google_name="Central Park",
477
            borough="M",
478
            zip_code="United States",
479
            formatted_address="Central Pk N, New York, NY, USA",
480
            latitude=40.7987768,
481
            longitude=-73.9537196,
482
            additional={
483
                "geometry": {
484
                    "bounds": {
485
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
486
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
487
                    },
488
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
489
                    "location_type": "GEOMETRIC_CENTER",
490
                    "viewport": {
491
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
492
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
493
                    },
494
                }
495
            },
496
        )
497
        self.image = ParkImage.objects.create(
1✔
498
            park=self.park,
499
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
500
        )
501

502
    def test_park_image_creation(self):
1✔
503
        """Test that a ParkImage object is created successfully."""
504
        self.assertEqual(self.image.park, self.park)
1✔
505
        self.assertEqual(
1✔
506
            self.image.image, "https://res.cloudinary.com/demo/image/upload/sample.jpg"
507
        )
508

509
    def test_park_image_str(self):
1✔
510
        """Test the string representation of a ParkImage object."""
511
        self.assertEqual(str(self.image), f"Image for {self.park.name}")
1✔
512

513

514
class ParkDetailViewImageTest(TestCase):
1✔
515
    def setUp(self):
1✔
516
        """Set up a test park and associated images."""
517
        self.client = Client()
1✔
518
        self.park = DogRunNew.objects.create(
1✔
519
            id="1",
520
            prop_id="1234",
521
            name="Central Park",
522
            address="New York, NY",
523
            dogruns_type="Small",
524
            accessible="Yes",
525
            notes="Test park notes",
526
            google_name="Central Park",
527
            borough="M",
528
            zip_code="United States",
529
            formatted_address="Central Pk N, New York, NY, USA",
530
            latitude=40.7987768,
531
            longitude=-73.9537196,
532
            additional={
533
                "geometry": {
534
                    "bounds": {
535
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
536
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
537
                    },
538
                    "location": {"lat": 40.7987768, "lng": -73.9537196},
539
                    "location_type": "GEOMETRIC_CENTER",
540
                    "viewport": {
541
                        "northeast": {"lat": 40.8009264, "lng": -73.9495752},
542
                        "southwest": {"lat": 40.796948, "lng": -73.9580246},
543
                    },
544
                }
545
            },
546
        )
547
        self.image = ParkImage.objects.create(
1✔
548
            park=self.park,
549
            image="https://res.cloudinary.com/demo/image/upload/sample.jpg",
550
        )
551

552
    def test_park_detail_view_with_images(self):
1✔
553
        """Test that the park detail view displays associated images."""
554
        response = self.client.get(
1✔
555
            reverse("park_detail", args=[self.park.slug, self.park.id])
556
        )
557
        self.assertEqual(response.status_code, 200)
1✔
558
        self.assertContains(response, self.park.name)
1✔
559
        # self.assertIn(self.image.image, response.content.decode())
560

561

562
class ParkDetailDisplayedReviewsTests(TestCase):
1✔
563
    def setUp(self):
1✔
564
        self.client = Client()
1✔
565
        self.user = User.objects.create_user(username="reviewer", password="pass123")
1✔
566

567
        self.park = DogRunNew.objects.create(
1✔
568
            id="10",
569
            prop_id="PARK100",
570
            name="Test Park",
571
            address="100 Test St",
572
            dogruns_type="Run",
573
            accessible="Yes",
574
            notes="Some notes",
575
            google_name="Test Park",
576
            borough="M",
577
            zip_code="10001",
578
            formatted_address="100 Test St, New York, NY",
579
            latitude=40.7128,
580
            longitude=-74.0060,
581
            display_name="Test Park",
582
            slug=slugify("Test Park-PARK100"),
583
        )
584

585
        # One visible review
586
        self.review_visible = Review.objects.create(
1✔
587
            park=self.park,
588
            user=self.user,
589
            text="This park is great!",
590
            rating=5,
591
            is_removed=False,
592
        )
593

594
        # One soft-deleted review
595
        self.review_removed = Review.objects.create(
1✔
596
            park=self.park,
597
            user=self.user,
598
            text="This review should be hidden",
599
            rating=1,
600
            is_removed=True,
601
        )
602

603
    def test_only_visible_reviews_displayed(self):
1✔
604
        url = reverse("park_detail", args=[self.park.slug, self.park.id])
1✔
605
        response = self.client.get(url)
1✔
606

607
        self.assertEqual(response.status_code, 200)
1✔
608
        self.assertContains(response, self.review_visible.text)
1✔
609
        self.assertNotContains(response, self.review_removed.text)
1✔
610

611
    def test_average_rating_excludes_removed_reviews(self):
1✔
612
        url = reverse("park_detail", args=[self.park.slug, self.park.id])
1✔
613
        response = self.client.get(url)
1✔
614

615
        # Ensure the average is based only on the 5-star review
616
        self.assertContains(response, "5.0")
1✔
617

618

619
class RenderStarsTests(TestCase):
1✔
620
    def test_int_stars(self):
1✔
621
        size = 20
1✔
622
        result = render_stars(4, size)
1✔
623
        self.assertEqual(result["filled_stars"], 4)
1✔
624
        self.assertEqual(result["half_stars"], 0)
1✔
625
        self.assertEqual(result["empty_stars"], 1)
1✔
626
        self.assertEqual(result["size"], size)
1✔
627

628
    def test_full_stars(self):
1✔
629
        size = 15
1✔
630
        result = render_stars(5, size)
1✔
631
        self.assertEqual(result["filled_stars"], 5)
1✔
632
        self.assertEqual(result["half_stars"], 0)
1✔
633
        self.assertEqual(result["empty_stars"], 0)
1✔
634
        self.assertEqual(result["size"], size)
1✔
635

636
    def test_no_stars(self):
1✔
637
        size = 10
1✔
638
        result = render_stars(0, size)
1✔
639
        self.assertEqual(result["filled_stars"], 0)
1✔
640
        self.assertEqual(result["half_stars"], 0)
1✔
641
        self.assertEqual(result["empty_stars"], 5)
1✔
642
        self.assertEqual(result["size"], size)
1✔
643

644
    def test_half_stars(self):
1✔
645
        size = 20
1✔
646
        result = render_stars(2.5, size)
1✔
647
        self.assertEqual(result["filled_stars"], 2)
1✔
648
        self.assertEqual(result["half_stars"], 1)
1✔
649
        self.assertEqual(result["empty_stars"], 2)
1✔
650
        self.assertEqual(result["size"], size)
1✔
651

652
    # >= X.25 -> one half star
653
    def test_round_up_to_half(self):
1✔
654
        size = 20
1✔
655
        result = render_stars(4.25, size)
1✔
656
        self.assertEqual(result["filled_stars"], 4)
1✔
657
        self.assertEqual(result["half_stars"], 1)
1✔
658
        self.assertEqual(result["empty_stars"], 0)
1✔
659
        self.assertEqual(result["size"], size)
1✔
660

661
    # < X.25 -> round down to whole
662
    def test_round_down_to_whole(self):
1✔
663
        size = 20
1✔
664
        result = render_stars(3.24, size)
1✔
665
        self.assertEqual(result["filled_stars"], 3)
1✔
666
        self.assertEqual(result["half_stars"], 0)
1✔
667
        self.assertEqual(result["empty_stars"], 2)
1✔
668
        self.assertEqual(result["size"], size)
1✔
669

670
    # < X.75 -> one half star
671
    def test_round_down_to_half(self):
1✔
672
        size = 20
1✔
673
        result = render_stars(2.74, size)
1✔
674
        self.assertEqual(result["filled_stars"], 2)
1✔
675
        self.assertEqual(result["half_stars"], 1)
1✔
676
        self.assertEqual(result["empty_stars"], 2)
1✔
677
        self.assertEqual(result["size"], size)
1✔
678

679
    # >= X.75 -> round up to next whole
680
    def test_round_up_to_whole(self):
1✔
681
        size = 20
1✔
682
        result = render_stars(4.75, size)
1✔
683
        self.assertEqual(result["filled_stars"], 5)
1✔
684
        self.assertEqual(result["half_stars"], 0)
1✔
685
        self.assertEqual(result["empty_stars"], 0)
1✔
686
        self.assertEqual(result["size"], size)
1✔
687

688

689
class ParkPresenceTests(TestCase):
1✔
690
    def setUp(self):
1✔
691
        self.client = Client()
1✔
692
        self.user = User.objects.create_user(username="tester", password="testpass")
1✔
693
        self.park = DogRunNew.objects.create(
1✔
694
            id="5",
695
            prop_id="5566",
696
            name="Test Dog Park",
697
            address="Test Location",
698
            dogruns_type="All",
699
            accessible="Yes",
700
            formatted_address="Test Address",
701
            latitude=40.0,
702
            longitude=-73.0,
703
            display_name="Test Dog Park",
704
            slug="test-dog-park-5566",
705
        )
706
        self.client.login(username="tester", password="testpass")
1✔
707

708
    def test_user_check_in_creates_presence(self):
1✔
709
        self.client.post(
1✔
710
            reverse("park_detail", args=[self.park.slug, self.park.id]),
711
            {"form_type": "check_in"},
712
        )
713
        presences = ParkPresence.objects.filter(user=self.user, park=self.park)
1✔
714
        self.assertEqual(presences.count(), 1)
1✔
715
        self.assertEqual(presences.first().status, "current")
1✔
716

717
    @patch("django.utils.timezone.now")
1✔
718
    def test_user_be_there_at_creates_presence(self, mock_now):
1✔
719
        fixed_now = timezone.datetime(2025, 4, 20, 12, 0, 0, tzinfo=timezone.utc)
1✔
720
        mock_now.return_value = fixed_now
1✔
721

722
        future_time = (fixed_now + timedelta(minutes=20)).strftime("%H:%M")
1✔
723

724
        self.client.post(
1✔
725
            reverse("park_detail", args=[self.park.slug, self.park.id]),
726
            {"form_type": "be_there_at", "time": future_time},
727
        )
728
        presences = ParkPresence.objects.filter(user=self.user, park=self.park)
1✔
729
        self.assertEqual(presences.count(), 1)
1✔
730
        self.assertEqual(presences.first().status, "on_the_way")
1✔
731

732

733
@patch(
1✔
734
    "cloudinary.uploader.upload",
735
    return_value={
736
        "asset_id": "dummy_asset_id",
737
        "public_id": "dummy_id",
738
        "version": "1234567890",
739
        "signature": "dummy_signature",
740
        "width": 800,
741
        "height": 600,
742
        "format": "jpg",
743
        "resource_type": "image",
744
        "type": "upload",
745
        "secure_url": "https://dummy.cloudinary.com/image.jpg",
746
        "url": "http://dummy.cloudinary.com/image.jpg",
747
    },
748
)
749
class ImageUploadTests(TestCase):
1✔
750
    def setUp(self):
1✔
751
        self.client = Client()
1✔
752
        self.user = User.objects.create_user(username="uploader", password="pass123")
1✔
753
        self.client.login(username="uploader", password="pass123")
1✔
754
        self.park = DogRunNew.objects.create(
1✔
755
            id="20",
756
            prop_id="8888",
757
            name="Mock Park",
758
            address="123",
759
            dogruns_type="All",
760
            accessible="Yes",
761
            formatted_address="123",
762
            latitude=40.0,
763
            longitude=-73.0,
764
            slug="mock-park-8888",
765
            display_name="Mock Park",
766
        )
767

768
    def test_upload_image_with_review(self, mock_upload):
1✔
769
        cloudinary_config(
1✔
770
            cloud_name="demo",
771
            api_key="fake_api_key",
772
            api_secret="fake_api_secret",
773
        )
774

775
        image = SimpleUploadedFile(
1✔
776
            "test.jpg", b"file_content", content_type="image/jpeg"
777
        )
778

779
        response = self.client.post(
1✔
780
            reverse("park_detail", args=[self.park.slug, self.park.id]),
781
            {
782
                "form_type": "submit_review",
783
                "text": "Nice park!",
784
                "rating": "5",
785
                "images": image,
786
            },
787
            follow=True,
788
        )
789

790
        self.assertEqual(response.status_code, 200)
1✔
791
        self.assertEqual(ParkImage.objects.count(), 1)
1✔
792

793

794
class ModalInteractionTests(TestCase):
1✔
795
    def test_modal_js_is_present(self):
1✔
796
        client = Client()
1✔
797
        park = DogRunNew.objects.create(
1✔
798
            id="7",
799
            prop_id="3344",
800
            name="Modal Park",
801
            address="JSville",
802
            dogruns_type="Small",
803
            accessible="Yes",
804
            formatted_address="JS Road",
805
            latitude=42.0,
806
            longitude=-75.0,
807
            display_name="Modal Park",
808
            slug="modal-park-3344",
809
        )
810
        response = client.get(reverse("park_detail", args=[park.slug, park.id]))
1✔
811
        self.assertContains(response, "function openCarouselImageModal")
1✔
812
        self.assertContains(response, "imagePreviewModal")
1✔
813
        self.assertContains(response, "modalImage")
1✔
814

815

816
class ReplaceFilterTests(TestCase):
1✔
817
    def test_replace_basic(self):
1✔
818
        result = image_filters.replace("hello world", "world,there")
1✔
819
        self.assertEqual(result, "hello there")
1✔
820

821
    def test_replace_partial_match(self):
1✔
822
        result = image_filters.replace("abcabcabc", "a,x")
1✔
823
        self.assertEqual(result, "xbcxbcxbc")
1✔
824

825
    def test_replace_only_first_comma_splits(self):
1✔
826
        result = image_filters.replace("one,two,three", "two,2")
1✔
827
        self.assertEqual(result, "one,2,three")
1✔
828

829
    def test_replace_with_comma_in_replacement(self):
1✔
830
        result = image_filters.replace("item1,item2", "item1,x,y")
1✔
831
        self.assertEqual(result, "x,y,item2")  # Splits only on first comma
1✔
832

833
    def test_replace_no_match(self):
1✔
834
        result = image_filters.replace("hello", "z,x")
1✔
835
        self.assertEqual(result, "hello")
1✔
836

837

838
class ReplyViewTests(TestCase):
1✔
839
    def setUp(self):
1✔
840
        self.client = Client()
1✔
841
        self.user = User.objects.create_user(username="testuser", password="testpass")
1✔
842
        self.park = DogRunNew.objects.create(
1✔
843
            id="99",
844
            prop_id="9999",
845
            name="Reply Park",
846
            address="Somewhere",
847
            dogruns_type="All",
848
            accessible="Yes",
849
            formatted_address="Reply Address",
850
            latitude=40.0,
851
            longitude=-73.0,
852
            slug="reply-park-9999",
853
        )
854
        self.review = Review.objects.create(
1✔
855
            park=self.park, text="Original Review", rating=4, user=self.user
856
        )
857
        self.park_detail_url = reverse(
1✔
858
            "park_detail", args=[self.park.slug, self.park.id]
859
        )
860

861
    def test_submit_reply_to_review(self):
1✔
862
        self.client.login(username="testuser", password="testpass")
1✔
863
        response = self.client.post(
1✔
864
            self.park_detail_url,
865
            {
866
                "form_type": "submit_reply",
867
                "parent_review_id": self.review.id,
868
                "reply_text": "This is a reply to a review.",
869
            },
870
        )
871
        self.assertEqual(response.status_code, 302)
1✔
872
        self.assertTrue(
1✔
873
            Reply.objects.filter(
874
                review=self.review, text="This is a reply to a review."
875
            ).exists()
876
        )
877

878
    def test_submit_nested_reply_to_reply(self):
1✔
879
        parent_reply = Reply.objects.create(
1✔
880
            review=self.review, user=self.user, text="Parent reply"
881
        )
882
        self.client.login(username="testuser", password="testpass")
1✔
883
        response = self.client.post(
1✔
884
            self.park_detail_url,
885
            {
886
                "form_type": "submit_reply",
887
                "parent_review_id": self.review.id,
888
                "parent_reply_id": parent_reply.id,
889
                "reply_text": "Child reply",
890
            },
891
        )
892
        self.assertEqual(response.status_code, 302)
1✔
893
        self.assertTrue(
1✔
894
            Reply.objects.filter(parent_reply=parent_reply, text="Child reply").exists()
895
        )
896

897
    def test_submit_reply_with_invalid_parent_reply_id(self):
1✔
898
        self.client.login(username="testuser", password="testpass")
1✔
899
        response = self.client.post(
1✔
900
            self.park_detail_url,
901
            {
902
                "form_type": "submit_reply",
903
                "parent_review_id": self.review.id,
904
                "parent_reply_id": 9999,
905
                "reply_text": "Fallback to review",
906
            },
907
        )
908
        self.assertEqual(response.status_code, 302)
1✔
909
        reply = Reply.objects.get(text="Fallback to review")
1✔
910
        self.assertIsNone(reply.parent_reply)
1✔
911

912
    def test_submit_reply_without_text(self):
1✔
913
        self.client.login(username="testuser", password="testpass")
1✔
914
        response = self.client.post(
1✔
915
            self.park_detail_url,
916
            {
917
                "form_type": "submit_reply",
918
                "parent_review_id": self.review.id,
919
                "reply_text": "   ",
920
            },
921
        )
922
        self.assertEqual(response.status_code, 302)
1✔
923
        self.assertEqual(Reply.objects.filter(review=self.review).count(), 0)
1✔
924

925
    def test_submit_reply_unauthenticated(self):
1✔
926
        response = self.client.post(
1✔
927
            self.park_detail_url,
928
            {
929
                "form_type": "submit_reply",
930
                "parent_review_id": self.review.id,
931
                "reply_text": "Unauthorized reply",
932
            },
933
        )
934
        self.assertEqual(response.status_code, 200)
1✔
935
        self.assertNotIn("Reply submitted successfully", response.content.decode())
1✔
936

937
    def test_delete_own_reply(self):
1✔
938
        self.client.login(username="testuser", password="testpass")
1✔
939
        reply = Reply.objects.create(
1✔
940
            review=self.review, user=self.user, text="To be deleted"
941
        )
942
        response = self.client.post(reverse("delete_reply", args=[reply.id]))
1✔
943
        self.assertEqual(response.status_code, 302)
1✔
944
        self.assertFalse(Reply.objects.filter(id=reply.id).exists())
1✔
945

946
    def test_delete_others_reply_forbidden(self):
1✔
947
        other = User.objects.create_user(username="other", password="pass")
1✔
948
        reply = Reply.objects.create(review=self.review, user=other, text="Not yours")
1✔
949
        self.client.login(username="testuser", password="testpass")
1✔
950
        response = self.client.post(reverse("delete_reply", args=[reply.id]))
1✔
951
        self.assertEqual(response.status_code, 302)
1✔
952
        self.assertTrue(Reply.objects.filter(id=reply.id).exists())
1✔
953

954
    def test_report_reply_success(self):
1✔
955
        other = User.objects.create_user(username="other", password="pass")
1✔
956
        reply = Reply.objects.create(review=self.review, user=other, text="Report me")
1✔
957
        self.client.login(username="testuser", password="testpass")
1✔
958
        response = self.client.post(
1✔
959
            reverse("report_reply", args=[reply.id]), {"reason": "Spam"}
960
        )
961
        self.assertEqual(response.status_code, 302)
1✔
962
        self.assertTrue(reply.reports.exists())
1✔
963

964
    def test_report_own_reply_fails(self):
1✔
965
        reply = Reply.objects.create(
1✔
966
            review=self.review, user=self.user, text="Self report"
967
        )
968
        self.client.login(username="testuser", password="testpass")
1✔
969
        response = self.client.post(
1✔
970
            reverse("report_reply", args=[reply.id]), {"reason": "Oops"}
971
        )
972
        self.assertEqual(response.status_code, 302)
1✔
973
        self.assertEqual(reply.reports.count(), 0)
1✔
974

975

976
class ChatViewTests(TestCase):
1✔
977
    def setUp(self):
1✔
978
        self.user1 = User.objects.create_user(username="user1", password="pass1234")
1✔
979
        self.user2 = User.objects.create_user(username="user2", password="pass1234")
1✔
980

981
    def test_chat_view_get(self):
1✔
982
        self.client.login(username="user1", password="pass1234")
1✔
983
        response = self.client.get(reverse("chat_view", args=[self.user2.username]))
1✔
984
        self.assertEqual(response.status_code, 200)
1✔
985
        self.assertContains(response, self.user2.username)
1✔
986

987
    def test_chat_view_post(self):
1✔
988
        self.client.login(username="user1", password="pass1234")
1✔
989
        response = self.client.post(
1✔
990
            reverse("chat_view", args=[self.user2.username]),
991
            {"content": "Hello user2!"},
992
        )
993
        self.assertEqual(response.status_code, 302)  # Redirect after POST
1✔
994

995
        # Verify the message was saved
996
        messages = Message.objects.filter(sender=self.user1, recipient=self.user2)
1✔
997
        self.assertEqual(messages.count(), 1)
1✔
998
        self.assertEqual(messages.first().content, "Hello user2!")
1✔
999

1000

1001
class AllMessagesViewTests(TestCase):
1✔
1002
    def setUp(self):
1✔
1003
        self.user1 = User.objects.create_user(username="user1", password="pass1234")
1✔
1004
        self.user2 = User.objects.create_user(username="user2", password="pass1234")
1✔
1005
        self.user3 = User.objects.create_user(username="user3", password="pass1234")
1✔
1006

1007
        # Create messages between user1 and user2
1008
        Message.objects.create(sender=self.user1, recipient=self.user2, content="Hi 2")
1✔
1009
        Message.objects.create(sender=self.user2, recipient=self.user1, content="Hey 1")
1✔
1010

1011
        # Create messages between user1 and user3
1012
        Message.objects.create(
1✔
1013
            sender=self.user3, recipient=self.user1, content="Hello from 3"
1014
        )
1015

1016
    def test_all_messages_view(self):
1✔
1017
        self.client.login(username="user1", password="pass1234")
1✔
1018
        response = self.client.get(reverse("all_messages"))
1✔
1019
        self.assertEqual(response.status_code, 200)
1✔
1020
        self.assertContains(response, "user2")
1✔
1021
        self.assertContains(response, "user3")
1✔
1022

1023
    def test_grouping_of_messages(self):
1✔
1024
        self.client.login(username="user1", password="pass1234")
1✔
1025
        response = self.client.get(reverse("all_messages"))
1✔
1026
        self.assertEqual(response.status_code, 200)
1✔
1027
        context = response.context["grouped_messages"]
1✔
1028
        self.assertIn("user2", context)
1✔
1029
        self.assertIn("user3", context)
1✔
1030
        self.assertEqual(len(context["user2"]), 2)
1✔
1031
        self.assertEqual(len(context["user3"]), 1)
1✔
1032

1033

1034
class DeleteConversationTests(TestCase):
1✔
1035
    def setUp(self):
1✔
1036
        self.user1 = User.objects.create_user(username="user1", password="pass1234")
1✔
1037
        self.user2 = User.objects.create_user(username="user2", password="pass1234")
1✔
1038

1039
        # Create some messages between user1 and user2
1040
        Message.objects.create(
1✔
1041
            sender=self.user1,
1042
            recipient=self.user2,
1043
            content="Hello",
1044
            timestamp=timezone.now(),
1045
        )
1046
        Message.objects.create(
1✔
1047
            sender=self.user2,
1048
            recipient=self.user1,
1049
            content="Hi",
1050
            timestamp=timezone.now() + timedelta(minutes=1),
1051
        )
1052
        Message.objects.create(
1✔
1053
            sender=self.user1,
1054
            recipient=self.user2,
1055
            content="How are you?",
1056
            timestamp=timezone.now() + timedelta(minutes=2),
1057
        )
1058

1059
    def test_delete_conversation_deletes_messages(self):
1✔
1060
        self.client.login(username="user1", password="pass1234")
1✔
1061

1062
        # Ensure messages exist
1063
        self.assertEqual(Message.objects.count(), 3)
1✔
1064

1065
        url = reverse("delete_conversation", args=[self.user2.username])
1✔
1066
        response = self.client.post(url)
1✔
1067

1068
        # Should redirect to all_messages
1069
        self.assertEqual(response.status_code, 302)
1✔
1070
        self.assertRedirects(response, reverse("all_messages"))
1✔
1071

1072
        # All messages between user1 and user2 should be gone
1073
        self.assertEqual(Message.objects.count(), 0)
1✔
1074

1075
    def test_delete_conversation_requires_login(self):
1✔
1076
        url = reverse("delete_conversation", args=[self.user2.username])
1✔
1077
        response = self.client.post(url)
1✔
1078

1079
        # Should redirect to login page
1080
        self.assertEqual(response.status_code, 302)
1✔
1081
        self.assertIn("/login", response.url)  # Adjust if you have a custom login URL
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc