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

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

05 May 2025 06:05PM UTC coverage: 89.964% (+0.02%) from 89.942%
722

push

travis-pro

web-flow
Merge pull request #333 from gcivil-nyu-org/marvin_dev

Marvin dev

1981 of 2202 relevant lines covered (89.96%)

0.9 hits per line

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

76.21
/parks/views.py
1
from django.shortcuts import render
1✔
2
from django.shortcuts import get_object_or_404
1✔
3
from django.shortcuts import redirect
1✔
4
from django.http import (  # noqa: F401  # Ignore "imported but unused"
1✔
5
    HttpResponseForbidden,
6
    HttpResponse,
7
    JsonResponse,
8
    HttpResponsePermanentRedirect,
9
)
10
from django.urls import reverse  # noqa: F401  # Ignore "imported but unused"
1✔
11
from django.db.models import OuterRef, Subquery, CharField, Q, Avg, Count, Prefetch
1✔
12
from django.db.models.functions import Cast
1✔
13
from .models import (
1✔
14
    DogRunNew,
15
    Review,
16
    ParkImage,
17
    ReviewReport,
18
    ImageReport,
19
    ParkPresence,
20
    Reply,
21
    ReplyReport,
22
    ParkInfoReport,
23
)
24
from django.forms.models import model_to_dict
1✔
25
from django.contrib.auth.decorators import login_required
1✔
26

27
import json
1✔
28
import datetime
1✔
29
from django.contrib import messages
1✔
30
from django.utils import timezone
1✔
31
from django.utils.timezone import now, localtime
1✔
32
from django.views.decorators.http import require_POST
1✔
33
from django.views.decorators.cache import never_cache
1✔
34
from datetime import timedelta
1✔
35

36
from django.contrib.auth.models import User
1✔
37
from .models import Message
1✔
38
from collections import defaultdict
1✔
39

40
from accounts.decorators import ban_protected
1✔
41
from accounts.utils import is_user_banned
1✔
42
from django.contrib.auth import logout
1✔
43

44

45
@ban_protected
1✔
46
@login_required
1✔
47
def chat_view(request, username):
1✔
48
    recipient = get_object_or_404(User, username=username)
1✔
49
    chat_messages = Message.objects.filter(
1✔
50
        sender__in=[request.user, recipient], recipient__in=[request.user, recipient]
51
    )
52
    if request.method == "POST":
1✔
53
        content = request.POST.get("content")
1✔
54
        if content:
1✔
55
            Message.objects.create(
1✔
56
                sender=request.user, recipient=recipient, content=content
57
            )
58
            return redirect("chat_view", username=username)
1✔
59
    return render(
1✔
60
        request,
61
        "parks/chat.html",
62
        {"recipient": recipient, "chat_messages": chat_messages},
63
    )
64

65

66
@ban_protected
1✔
67
@login_required
1✔
68
def all_messages_view(request):
1✔
69
    user = request.user
1✔
70
    # Get all messages involving the user, either sent or received
71
    messages = (
1✔
72
        Message.objects.filter(Q(sender=user) | Q(recipient=user))
73
        .select_related("sender", "recipient")
74
        .order_by("-timestamp")
75
    )
76

77
    # Group by the *other* user
78
    grouped = defaultdict(list)
1✔
79
    for msg in messages:
1✔
80
        other_user = msg.recipient if msg.sender == user else msg.sender
1✔
81
        grouped[other_user.username].append(msg)
1✔
82

83
    return render(
1✔
84
        request, "parks/all_messages.html", {"grouped_messages": dict(grouped)}
85
    )
86

87

88
@ban_protected
1✔
89
@login_required
1✔
90
def delete_conversation(request, sender_username):
1✔
91
    # Get the recipient user object (the sender of the conversation)
92
    recipient = get_object_or_404(User, username=sender_username)
1✔
93

94
    # Delete messages where the user is either the sender or recipient
95
    Message.objects.filter(
1✔
96
        sender__in=[request.user, recipient], recipient__in=[request.user, recipient]
97
    ).delete()
98

99
    # Redirect to the all messages view after deleting the conversation
100
    return redirect("all_messages")
1✔
101

102

103
@ban_protected
1✔
104
@login_required
1✔
105
@require_POST
1✔
106
def checkin_view(request):
1✔
107
    data = json.loads(request.body)
×
108
    park_id = data.get("park_id")
×
109
    park = get_object_or_404(DogRunNew, id=park_id)
×
110

111
    # Remove existing 'current' check-ins from other parks
112
    ParkPresence.objects.filter(user=request.user, status="current").exclude(
×
113
        park=park
114
    ).delete()
115

116
    # Check in to this park
117
    presence, created = ParkPresence.objects.update_or_create(
×
118
        user=request.user,
119
        park=park,
120
        defaults={"status": "current", "time": timezone.now()},
121
    )
122

123
    return JsonResponse({"status": "checked in", "new": created})
×
124

125

126
@ban_protected
1✔
127
@login_required
1✔
128
@require_POST
1✔
129
def bethere_view(request):
1✔
130
    try:
×
131
        data = json.loads(request.body)
×
132
        park_id = data.get("park_id")
×
133
        time_str = data.get("time")  # e.g. "17:30"
×
134

135
        if not park_id or not time_str:
×
136
            return JsonResponse({"error": "Missing park_id or time"}, status=400)
×
137

138
        # Parse and validate time
139
        try:
×
140
            arrival_time = datetime.datetime.strptime(time_str, "%H:%M").time()
×
141
        except ValueError:
×
142
            return JsonResponse({"error": "Invalid time format"}, status=400)
×
143

144
        current_datetime = now()
×
145
        today = current_datetime.date()
×
146
        arrival_datetime = timezone.make_aware(
×
147
            datetime.datetime.combine(today, arrival_time)
148
        )
149

150
        if arrival_datetime < current_datetime:
×
151
            return JsonResponse({"error": "Cannot select a past time"}, status=400)
×
152

153
        park = get_object_or_404(DogRunNew, id=park_id)
×
154

155
        # ✅ Save the full datetime, not just the time
156
        presence, created = ParkPresence.objects.update_or_create(
×
157
            user=request.user,
158
            park=park,
159
            defaults={"status": "On their way", "time": arrival_datetime},
160
        )
161

162
        formatted_time = arrival_datetime.strftime("%I:%M %p")
×
163
        return JsonResponse({"status": "on their way", "time": formatted_time})
×
164

165
    except Exception as e:
166
        import traceback
167

168
        print(traceback.format_exc())
169
        return JsonResponse({"error": str(e)}, status=500)
170

171

172
def expire_old_checkins():
1✔
173
    expiration_time = timezone.now() - timedelta(hours=1)
1✔
174
    ParkPresence.objects.filter(status="current", time__lt=expiration_time).delete()
1✔
175

176

177
def home_view(request):
1✔
178
    return render(request, "parks/home.html")
×
179

180

181
@never_cache
1✔
182
def park_and_map(request):
1✔
183
    # Get filter values from GET request
184
    query = request.GET.get("query", "").strip()
1✔
185
    filter_value = request.GET.get("filter", "").strip()
1✔
186
    accessible_value = request.GET.get("accessible", "").strip()
1✔
187
    borough_value = request.GET.get("borough", "").strip().upper()
1✔
188

189
    thumbnail = ParkImage.objects.filter(
1✔
190
        park_id=OuterRef("pk"), is_removed=False, review__is_removed=False
191
    ).values("image")[:1]
192

193
    # Fetch all dog runs from the database
194
    parks = (
1✔
195
        DogRunNew.objects.all()
196
        .order_by("id")
197
        .prefetch_related("images")
198
        .annotate(
199
            thumbnail_url=Cast(Subquery(thumbnail), output_field=CharField()),
200
            average_rating=Avg("reviews__rating", filter=Q(reviews__is_removed=False)),
201
            review_count=Count("reviews", filter=Q(reviews__is_removed=False)),
202
        )
203
    )
204

205
    # Search by ZIP, name, or Google name
206
    if query:
1✔
207
        parks = parks.filter(
×
208
            Q(name__icontains=query)
209
            | Q(google_name__icontains=query)
210
            | Q(zip_code__icontains=query)
211
        )
212

213
    # Filter by park type (e.g., "Off-Leash")
214
    if filter_value:
1✔
215
        parks = parks.filter(dogruns_type__iexact=filter_value)
×
216

217
    # Filter by accessibility only if explicitly set to "True" or "False"
218
    if accessible_value == "True":
1✔
219
        parks = parks.filter(accessible=True)
×
220
    elif accessible_value == "False":
1✔
221
        parks = parks.filter(accessible=False)
×
222

223
    if borough_value:
1✔
224
        parks = parks.filter(borough=borough_value)
1✔
225

226
    # Convert parks to JSON (for JS use)
227
    # parks_json = json.dumps(list(parks.values()))
228

229
    parks_json = json.dumps(
1✔
230
        [
231
            {
232
                **model_to_dict(park),
233
                "thumbnail_url": park.thumbnail_url,
234
                "average_rating": park.average_rating,
235
                "review_count": park.review_count,
236
                "url": park.detail_page_url(),
237
            }
238
            for park in parks
239
        ]
240
    )
241

242
    # Render the template
243
    return render(
1✔
244
        request,
245
        "parks/combined_view.html",
246
        {
247
            "parks": parks,
248
            "parks_json": parks_json,
249
            "query": query,
250
            "selected_type": filter_value,
251
            "selected_accessible": accessible_value,
252
            "selected_borough": borough_value,
253
        },
254
    )
255

256

257
@never_cache
1✔
258
def park_detail(request, slug, id):
1✔
259
    park = get_object_or_404(DogRunNew, id=id)
1✔
260
    if slug != park.slug:
1✔
261
        return HttpResponsePermanentRedirect(park.detail_page_url())
1✔
262

263
    images = ParkImage.objects.filter(
1✔
264
        park=park, is_removed=False, review__is_removed=False
265
    ).filter(Q(review__isnull=False) & Q(review__is_deleted=False))
266

267
    # Prefetch only non-removed images for each review
268
    visible_images = Prefetch(
1✔
269
        "images",
270
        queryset=ParkImage.objects.filter(is_removed=False),
271
        to_attr="visible_images",
272
    )
273
    reviews = park.reviews.filter(is_removed=False).prefetch_related(
1✔
274
        visible_images, "replies__user__userprofile", "user__userprofile"  # load avatar
275
    )
276
    reviews = park.reviews.filter(is_removed=False).prefetch_related(visible_images)
1✔
277

278
    average_rating = reviews.aggregate(Avg("rating"))["rating__avg"]
1✔
279

280
    # Clean up expired "On their way" entries
281
    now = localtime()
1✔
282
    # Call the function to expire old check-ins
283
    expire_old_checkins()
1✔
284

285
    ParkPresence.objects.filter(park=park, status="On their way", time__lt=now).delete()
1✔
286

287
    # Updated counts after cleanup
288
    current_count = ParkPresence.objects.filter(park=park, status="current").count()
1✔
289
    on_the_way_count = ParkPresence.objects.filter(
1✔
290
        park=park, status="On their way", time__isnull=False, time__gte=now
291
    ).count()
292

293
    query = request.GET.get("q", "")
1✔
294

295
    # Only users currently checked-in or on their way
296
    presences = ParkPresence.objects.filter(
1✔
297
        park=park, status__in=["current", "On their way"]
298
    )
299

300
    if query:
1✔
301
        presences = presences.filter(user__username__icontains=query)
×
302

303
    if request.user.is_authenticated and request.method == "POST":
1✔
304

305
        if is_user_banned(request.user):
1✔
306
            logout(request)
×
307
            messages.error(
×
308
                request,
309
                "Your account is banned. "
310
                "You cannot perform this action. You have been logged out.",
311
            )
312
            return redirect("login")
×
313

314
        form_type = request.POST.get("form_type")
1✔
315

316
        if form_type == "submit_review":
1✔
317
            review_text = request.POST.get("text", "").strip()
1✔
318
            rating_value = request.POST.get("rating", "").strip()
1✔
319

320
            if not rating_value.isdigit():
1✔
321
                messages.error(request, "Please select a rating before submitting.")
×
322
                return redirect(park.detail_page_url())
×
323

324
            rating = int(rating_value)
1✔
325
            if rating < 1 or rating > 5:
1✔
326
                return render(
×
327
                    request,
328
                    "parks/park_detail.html",
329
                    {
330
                        "park": park,
331
                        "images": images,
332
                        "reviews": reviews,
333
                        "error_message": "Rating must be between 1 and 5 stars!",
334
                        "average_rating": average_rating,
335
                        "current_count": current_count,
336
                        "on_the_way_count": on_the_way_count,
337
                    },
338
                )
339

340
            review = Review.objects.create(
1✔
341
                park=park,
342
                text=review_text if review_text else "",
343
                rating=rating,
344
                user=request.user,
345
            )
346
            images = request.FILES.getlist("images")
1✔
347
            ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]
1✔
348

349
            invalid_type = any(
1✔
350
                img.content_type not in ALLOWED_IMAGE_TYPES for img in images
351
            )
352

353
            if invalid_type:
1✔
354
                messages.error(request, "Only JPEG, PNG, or WebP images are allowed.")
×
355
                review.delete()
×
356
                return redirect("park_detail", slug=park.slug, id=park.id)
×
357

358
            MAX_IMAGE_SIZE = 5 * 1024 * 1024  # 5 MB
1✔
359

360
            invalid_images = [img for img in images if img.size > MAX_IMAGE_SIZE]
1✔
361

362
            if invalid_images:
1✔
363
                messages.error(request, "Each image must be under 5 MB.")
×
364
                review.delete()
×
365
                return redirect("park_detail", slug=park.slug, id=park.id)
×
366

367
            # Save valid images
368
            for image in images:
1✔
369
                ParkImage.objects.create(
1✔
370
                    park=park, image=image, review=review, user=request.user
371
                )
372

373
            messages.success(request, "Your review was submitted successfully!")
1✔
374
            return redirect(park.detail_page_url())
1✔
375

376
        elif form_type == "check_in":
1✔
377
            ParkPresence.objects.create(
1✔
378
                user=request.user,
379
                park=park,
380
                status="current",
381
                time=now,
382
            )
383

384
        elif form_type == "be_there_at":
1✔
385
            time_str = request.POST.get("time")
1✔
386
            try:
1✔
387
                arrival_time = timezone.datetime.combine(
1✔
388
                    now.date(), timezone.datetime.strptime(time_str, "%H:%M").time()
389
                )
390
                arrival_time = timezone.make_aware(
1✔
391
                    arrival_time
392
                )  # Make it timezone aware
393
            except (ValueError, TypeError):
×
394
                arrival_time = None
×
395

396
            if arrival_time and arrival_time >= now:
1✔
397
                ParkPresence.objects.create(
1✔
398
                    user=request.user,
399
                    park=park,
400
                    status="on_the_way",
401
                    time=arrival_time,
402
                )
403
        # report reviews
404
        elif form_type == "report_review":
1✔
405
            if request.user.is_authenticated:
1✔
406
                review_id = request.POST.get("review_id")
1✔
407
                reason = request.POST.get("reason", "").strip()
1✔
408

409
                if review_id and reason:
1✔
410
                    review = get_object_or_404(Review, id=review_id)
1✔
411

412
                    # prevent duplicate reports by the same user
413
                    exists = ReviewReport.objects.filter(
1✔
414
                        review=review, reported_by=request.user
415
                    ).exists()
416

417
                    if exists:
1✔
418
                        messages.error(
1✔
419
                            request, "You have already reported this review before."
420
                        )
421
                    else:
422
                        ReviewReport.objects.create(
1✔
423
                            review=review, reported_by=request.user, reason=reason
424
                        )
425
                        messages.success(
1✔
426
                            request, "Your review report was submitted successfully."
427
                        )
428
            else:
429
                messages.error(request, "You must be logged in to report a review.")
×
430

431
            return redirect(park.detail_page_url())
1✔
432

433
        elif form_type == "submit_reply":
1✔
434
            if request.user.is_authenticated:
1✔
435
                parent_review_id = request.POST.get("parent_review_id")
1✔
436
                reply_text = request.POST.get("reply_text", "").strip()
1✔
437
                parent_reply_id = request.POST.get("parent_reply_id")
1✔
438

439
                if parent_review_id and reply_text:
1✔
440
                    parent_review = get_object_or_404(Review, id=parent_review_id)
1✔
441

442
                    parent_reply = None
1✔
443
                    if parent_reply_id:
1✔
444
                        try:
1✔
445
                            parent_reply = Reply.objects.get(id=parent_reply_id)
1✔
446
                        except Reply.DoesNotExist:
1✔
447
                            parent_reply = None  # fallback: just attach to review
1✔
448

449
                    Reply.objects.create(
1✔
450
                        review=parent_review,
451
                        user=request.user,
452
                        text=reply_text,
453
                        parent_reply=parent_reply,
454
                    )
455

456
                    messages.success(request, "Reply submitted successfully!")
1✔
457
        return redirect(park.detail_page_url())
1✔
458

459
    park_json = json.dumps(model_to_dict(park))
1✔
460

461
    return render(
1✔
462
        request,
463
        "parks/park_detail.html",
464
        {
465
            "park": park,
466
            "images": images,
467
            "reviews": reviews,
468
            "park_json": park_json,
469
            "average_rating": average_rating,
470
            "current_count": current_count,
471
            "on_the_way_count": on_the_way_count,
472
            "presences": presences,
473
            "query": query,
474
        },
475
    )
476

477

478
def try_hard_delete_review_if_all_replies_deleted(review):
1✔
479
    if review.is_deleted and not review.replies.filter(is_deleted=False).exists():
1✔
480
        ParkImage.objects.filter(review=review).delete()
×
481
        review.delete()
×
482

483

484
@ban_protected
1✔
485
@login_required
1✔
486
def delete_review(request, review_id):
1✔
487
    review = get_object_or_404(Review, id=review_id)
1✔
488

489
    # Ensure the current user owns the review
490
    if request.user != review.user:
1✔
491
        return HttpResponseForbidden("You are not allowed to delete this review.")
×
492

493
    # If there are any non-deleted replies, perform soft-delete
494
    if review.replies.filter(is_deleted=False).exists():
1✔
495
        review.is_deleted = True
×
496
        review.text = ""
×
497
        review.save()
×
498
    else:
499
        # Delete associated images (if any), then delete the review
500
        review.images.all().delete()
1✔
501
        review.delete()
1✔
502

503
    messages.success(request, "You have successfully deleted the review!")
1✔
504
    return redirect(review.park.detail_page_url())
1✔
505

506

507
@ban_protected
1✔
508
@login_required
1✔
509
def delete_image(request, image_id):
1✔
510
    image = get_object_or_404(ParkImage, id=image_id)
1✔
511
    if image.user == request.user:
1✔
512
        image.delete()
1✔
513
        messages.success(request, "You have successfully deleted the image!")
1✔
514
        return redirect(image.park.detail_page_url())
1✔
515
    return HttpResponseForbidden("You are not allowed to delete this image.")
×
516

517

518
def contact_view(request):
1✔
519
    return render(request, "parks/contact.html")
×
520

521

522
@ban_protected
1✔
523
@login_required
1✔
524
def report_image(request, image_id):
1✔
525
    image = get_object_or_404(ParkImage, id=image_id)
1✔
526

527
    if request.method == "POST":
1✔
528
        reason = request.POST.get("reason", "").strip()
1✔
529
        if reason:
1✔
530
            # Check if this user already reported this image
531
            already_reported = ImageReport.objects.filter(
1✔
532
                user=request.user, image=image
533
            ).exists()
534
            if already_reported:
1✔
535
                messages.error(request, "You have already reported this image before.")
1✔
536
            else:
537
                ImageReport.objects.create(
1✔
538
                    user=request.user, image=image, reason=reason
539
                )
540
                messages.success(request, "You have successfully reported the image!")
1✔
541
        return redirect(image.park.detail_page_url())
1✔
542

543
    return redirect(image.park.detail_page_url())
×
544

545

546
@ban_protected
1✔
547
@login_required
1✔
548
def delete_reply(request, reply_id):
1✔
549
    reply = get_object_or_404(Reply, id=reply_id)
1✔
550

551
    if reply.user != request.user:
1✔
552
        messages.error(request, "You can only delete your own replies.")
1✔
553
        return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
554

555
    if reply.children.filter(is_deleted=False).exists():
1✔
556
        # Soft-delete: mark deleted, clear text
557
        reply.is_deleted = True
×
558
        reply.text = ""
×
559
        reply.save()
×
560
    else:
561
        reply.delete()
1✔
562

563
    messages.success(request, "Reply deleted successfully.")
1✔
564
    # check & hard delete
565
    try_hard_delete_review_if_all_replies_deleted(reply.review)
1✔
566
    return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
567

568

569
@ban_protected
1✔
570
@login_required
1✔
571
def report_reply(request, reply_id):
1✔
572

573
    if not request.user.is_authenticated:
1✔
574
        messages.error(request, "You must be logged in to report a review.")
×
575

576
    if request.method == "POST":
1✔
577
        reason = request.POST.get("reason", "").strip()
1✔
578
        reply = get_object_or_404(Reply, id=reply_id)
1✔
579

580
        if reply.user != request.user and reason:
1✔
581

582
            if ReplyReport.objects.filter(reply=reply, user=request.user).exists():
1✔
583
                messages.error(request, "You have already reported this reply before.")
×
584
            else:
585
                ReplyReport.objects.create(
1✔
586
                    reply=reply, user=request.user, reason=reason
587
                )
588
                messages.success(request, "Reply reported successfully.")
1✔
589
        else:
590
            messages.error(request, "You cannot report your own reply.")
1✔
591
    return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
592

593

594
def custom_500_view(request):
1✔
595
    return render(request, "500.html", status=500)
×
596

597

598
def trigger_500(request):
1✔
599
    raise Exception("Simulated 500 error")
×
600

601

602
@ban_protected
1✔
603
@login_required
1✔
604
def report_park_info(request, park_id):
1✔
605
    if request.method == "POST":
×
606
        park = get_object_or_404(DogRunNew, id=park_id)
×
607
        new_dogruns_type = request.POST.get("new_dogruns_type")
×
608
        new_accessible = request.POST.get("new_accessible") == "True"
×
609

610
        ParkInfoReport.objects.create(
×
611
            park=park,
612
            user=request.user,
613
            new_dogruns_type=new_dogruns_type,
614
            new_accessible=new_accessible,
615
        )
616
        messages.success(request, "Thank you! Your report was submitted.")
×
617
        return redirect(park.detail_page_url())
×
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