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

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

30 Apr 2025 05:34AM UTC coverage: 91.285% (-2.0%) from 93.242%
674

push

travis-pro

mr2447
user manage,emnt

0 of 2 new or added lines in 1 file covered. (0.0%)

85 existing lines in 2 files now uncovered.

1812 of 1985 relevant lines covered (91.28%)

0.91 hits per line

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

78.26
/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
)
23
from django.forms.models import model_to_dict
1✔
24
from django.contrib.auth.decorators import login_required
1✔
25

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

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

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

43

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

64

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

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

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

86

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

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

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

101

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

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

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

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

124

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

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

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

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

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

UNCOV
152
        park = get_object_or_404(DogRunNew, id=park_id)
×
153

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

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

164
    except Exception as e:
165
        import traceback
166

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

170

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

175

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

179

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

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

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

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

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

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

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

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

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

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

255

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

262
    images = ParkImage.objects.filter(
1✔
263
        park=park, is_removed=False, review__is_removed=False
264
    )
265

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

476

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

482

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

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

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

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

505

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

516

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

520

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

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

UNCOV
542
    return redirect(image.park.detail_page_url())
×
543

544

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

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

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

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

567

568
@ban_protected
1✔
569
@login_required
1✔
570
def report_reply(request, reply_id):
1✔
571
    if request.method == "POST":
1✔
572
        reason = request.POST.get("reason", "").strip()
1✔
573
        reply = get_object_or_404(Reply, id=reply_id)
1✔
574
        if reply.user != request.user and reason:
1✔
575
            ReplyReport.objects.create(reply=reply, user=request.user, reason=reason)
1✔
576
            messages.success(request, "Reply reported successfully.")
1✔
577
        else:
578
            messages.error(request, "You cannot report your own reply.")
1✔
579
    return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
580

581

582
def custom_500_view(request):
1✔
UNCOV
583
    return render(request, "500.html", status=500)
×
584

585

586
def trigger_500(request):
1✔
UNCOV
587
    raise Exception("Simulated 500 error")
×
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