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

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

22 Apr 2025 08:56PM UTC coverage: 92.93% (-0.4%) from 93.377%
613

Pull #295

travis-pro

web-flow
Merge d72022fb6 into 3cf36afac
Pull Request #295: tickets completed

45 of 55 new or added lines in 4 files covered. (81.82%)

1 existing line in 1 file now uncovered.

1459 of 1570 relevant lines covered (92.93%)

0.93 hits per line

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

79.7
/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 import login
1✔
25
from django.contrib.auth.decorators import login_required
1✔
26
from .forms import RegisterForm
1✔
27

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

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

41

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

59

60
@login_required
1✔
61
def all_messages_view(request):
1✔
62
    user = request.user
1✔
63
    # Get all messages involving the user, either sent or received
64
    messages = (
1✔
65
        Message.objects.filter(Q(sender=user) | Q(recipient=user))
66
        .select_related("sender", "recipient")
67
        .order_by("-timestamp")
68
    )
69

70
    # Group by the *other* user
71
    grouped = defaultdict(list)
1✔
72
    for msg in messages:
1✔
73
        other_user = msg.recipient if msg.sender == user else msg.sender
1✔
74
        grouped[other_user.username].append(msg)
1✔
75

76
    return render(
1✔
77
        request, "parks/all_messages.html", {"grouped_messages": dict(grouped)}
78
    )
79

80

81
@login_required
1✔
82
def delete_conversation(request, sender_username):
1✔
83
    # Get the recipient user object (the sender of the conversation)
84
    recipient = get_object_or_404(User, username=sender_username)
1✔
85

86
    # Delete messages where the user is either the sender or recipient
87
    Message.objects.filter(
1✔
88
        sender__in=[request.user, recipient], recipient__in=[request.user, recipient]
89
    ).delete()
90

91
    # Redirect to the all messages view after deleting the conversation
92
    return redirect("all_messages")
1✔
93

94

95
@login_required
1✔
96
@require_POST
1✔
97
def checkin_view(request):
1✔
98
    data = json.loads(request.body)
×
99
    park_id = data.get("park_id")
×
100
    park = get_object_or_404(DogRunNew, id=park_id)
×
101

102
    # Remove existing 'current' check-ins from other parks
103
    ParkPresence.objects.filter(user=request.user, status="current").exclude(
×
104
        park=park
105
    ).delete()
106

107
    # Check in to this park
108
    presence, created = ParkPresence.objects.update_or_create(
×
109
        user=request.user,
110
        park=park,
111
        defaults={"status": "current", "time": timezone.now()},
112
    )
113

114
    return JsonResponse({"status": "checked in", "new": created})
×
115

116

117
@login_required
1✔
118
@require_POST
1✔
119
def bethere_view(request):
1✔
120
    try:
×
121
        data = json.loads(request.body)
×
122
        park_id = data.get("park_id")
×
123
        time_str = data.get("time")  # e.g. "17:30"
×
124

125
        if not park_id or not time_str:
×
126
            return JsonResponse({"error": "Missing park_id or time"}, status=400)
×
127

128
        # Parse and validate time
129
        try:
×
130
            arrival_time = datetime.datetime.strptime(time_str, "%H:%M").time()
×
131
        except ValueError:
×
132
            return JsonResponse({"error": "Invalid time format"}, status=400)
×
133

134
        current_datetime = now()
×
135
        today = current_datetime.date()
×
136
        arrival_datetime = timezone.make_aware(
×
137
            datetime.datetime.combine(today, arrival_time)
138
        )
139

140
        if arrival_datetime < current_datetime:
×
141
            return JsonResponse({"error": "Cannot select a past time"}, status=400)
×
142

143
        park = get_object_or_404(DogRunNew, id=park_id)
×
144

145
        # ✅ Save the full datetime, not just the time
146
        presence, created = ParkPresence.objects.update_or_create(
×
147
            user=request.user,
148
            park=park,
149
            defaults={"status": "On their way", "time": arrival_datetime},
150
        )
151

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

155
    except Exception as e:
156
        import traceback
157

158
        print(traceback.format_exc())
159
        return JsonResponse({"error": str(e)}, status=500)
160

161

162
def expire_old_checkins():
1✔
163
    expiration_time = timezone.now() - timedelta(hours=1)
1✔
164
    ParkPresence.objects.filter(status="current", time__lt=expiration_time).delete()
1✔
165

166

167
def register_view(request):
1✔
168
    if request.method == "POST":
1✔
169
        form = RegisterForm(request.POST)
1✔
170
        if form.is_valid():
1✔
171
            # Save but don't commit yet
172
            user = form.save(commit=False)
1✔
173
            # If they chose Admin, mark them as staff
174
            if form.cleaned_data["role"] == "admin":
1✔
175
                user.is_staff = True
1✔
176
            user.save()
1✔
177

178
            # Log the user in immediately
179
            login(request, user)
1✔
180
            request.session.save()
1✔
181
            return redirect("home")
1✔
182
    else:
183
        form = RegisterForm()
1✔
184

185
    return render(request, "parks/register.html", {"form": form})
1✔
186

187

188
def home_view(request):
1✔
189
    return render(request, "parks/home.html")
×
190

191

192
@never_cache
1✔
193
def park_and_map(request):
1✔
194
    # Get filter values from GET request
195
    query = request.GET.get("query", "").strip()
1✔
196
    filter_value = request.GET.get("filter", "").strip()
1✔
197
    accessible_value = request.GET.get("accessible", "").strip()
1✔
198
    borough_value = request.GET.get("borough", "").strip().upper()
1✔
199

200
    thumbnail = ParkImage.objects.filter(
1✔
201
        park_id=OuterRef("pk"), is_removed=False, review__is_removed=False
202
    ).values("image")[:1]
203

204
    # Fetch all dog runs from the database
205
    parks = (
1✔
206
        DogRunNew.objects.all()
207
        .order_by("id")
208
        .prefetch_related("images")
209
        .annotate(
210
            thumbnail_url=Cast(Subquery(thumbnail), output_field=CharField()),
211
            average_rating=Avg("reviews__rating", filter=Q(reviews__is_removed=False)),
212
            review_count=Count("reviews", filter=Q(reviews__is_removed=False)),
213
        )
214
    )
215

216
    # Search by ZIP, name, or Google name
217
    if query:
1✔
218
        parks = parks.filter(
×
219
            Q(name__icontains=query)
220
            | Q(google_name__icontains=query)
221
            | Q(zip_code__icontains=query)
222
        )
223

224
    # Filter by park type (e.g., "Off-Leash")
225
    if filter_value:
1✔
226
        parks = parks.filter(dogruns_type__iexact=filter_value)
×
227

228
    # Filter by accessibility only if explicitly set to "True" or "False"
229
    if accessible_value == "True":
1✔
230
        parks = parks.filter(accessible=True)
×
231
    elif accessible_value == "False":
1✔
232
        parks = parks.filter(accessible=False)
×
233

234
    if borough_value:
1✔
235
        parks = parks.filter(borough=borough_value)
1✔
236

237
    # Convert parks to JSON (for JS use)
238
    # parks_json = json.dumps(list(parks.values()))
239

240
    parks_json = json.dumps(
1✔
241
        [
242
            {
243
                **model_to_dict(park),
244
                "thumbnail_url": park.thumbnail_url,
245
                "average_rating": park.average_rating,
246
                "review_count": park.review_count,
247
                "url": park.detail_page_url(),
248
            }
249
            for park in parks
250
        ]
251
    )
252

253
    # Render the template
254
    return render(
1✔
255
        request,
256
        "parks/combined_view.html",
257
        {
258
            "parks": parks,
259
            "parks_json": parks_json,
260
            "query": query,
261
            "selected_type": filter_value,
262
            "selected_accessible": accessible_value,
263
            "selected_borough": borough_value,
264
        },
265
    )
266

267

268
@never_cache
1✔
269
def park_detail(request, slug, id):
1✔
270
    park = get_object_or_404(DogRunNew, id=id)
1✔
271
    if slug != park.slug:
1✔
272
        return HttpResponsePermanentRedirect(park.detail_page_url())
1✔
273

274
    images = ParkImage.objects.filter(
1✔
275
        park=park, is_removed=False, review__is_removed=False
276
    )
277

278
    # Prefetch only non-removed images for each review
279
    visible_images = Prefetch(
1✔
280
        "images",
281
        queryset=ParkImage.objects.filter(is_removed=False),
282
        to_attr="visible_images",
283
    )
284
    reviews = park.reviews.filter(is_removed=False).prefetch_related(
1✔
285
        visible_images, "replies__user__userprofile", "user__userprofile"  # load avatar
286
    )
287
    reviews = park.reviews.filter(is_removed=False).prefetch_related(visible_images)
1✔
288

289
    average_rating = reviews.aggregate(Avg("rating"))["rating__avg"]
1✔
290

291
    # Clean up expired "On their way" entries
292
    now = localtime()
1✔
293
    # Call the function to expire old check-ins
294
    expire_old_checkins()
1✔
295

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

298
    # Updated counts after cleanup
299
    current_count = ParkPresence.objects.filter(park=park, status="current").count()
1✔
300
    on_the_way_count = ParkPresence.objects.filter(
1✔
301
        park=park, status="On their way", time__isnull=False, time__gte=now
302
    ).count()
303

304
    if request.user.is_authenticated and request.method == "POST":
1✔
305
        form_type = request.POST.get("form_type")
1✔
306

307
        if form_type == "submit_review":
1✔
308
            review_text = request.POST.get("text", "").strip()
1✔
309
            rating_value = request.POST.get("rating", "").strip()
1✔
310

311
            if not rating_value.isdigit():
1✔
312
                messages.error(request, "Please select a rating before submitting.")
×
313
                return redirect(park.detail_page_url())
×
314

315
            rating = int(rating_value)
1✔
316
            if rating < 1 or rating > 5:
1✔
317
                return render(
×
318
                    request,
319
                    "parks/park_detail.html",
320
                    {
321
                        "park": park,
322
                        "images": images,
323
                        "reviews": reviews,
324
                        "error_message": "Rating must be between 1 and 5 stars!",
325
                        "average_rating": average_rating,
326
                        "current_count": current_count,
327
                        "on_the_way_count": on_the_way_count,
328
                    },
329
                )
330

331
            review = Review.objects.create(
1✔
332
                park=park,
333
                text=review_text if review_text else "",
334
                rating=rating,
335
                user=request.user,
336
            )
337
            images = request.FILES.getlist("images")
1✔
338
            ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]
1✔
339

340
            invalid_type = any(
1✔
341
                img.content_type not in ALLOWED_IMAGE_TYPES for img in images
342
            )
343

344
            if invalid_type:
1✔
345
                messages.error(request, "Only JPEG, PNG, or WebP images are allowed.")
×
346
                review.delete()
×
347
                return redirect("park_detail", slug=park.slug, id=park.id)
×
348

349
            MAX_IMAGE_SIZE = 5 * 1024 * 1024  # 5 MB
1✔
350

351
            invalid_images = [img for img in images if img.size > MAX_IMAGE_SIZE]
1✔
352

353
            if invalid_images:
1✔
354
                messages.error(request, "Each image must be under 5 MB.")
×
355
                review.delete()
×
356
                return redirect("park_detail", slug=park.slug, id=park.id)
×
357

358
            # Save valid images
359
            for image in images:
1✔
360
                ParkImage.objects.create(
1✔
361
                    park=park, image=image, review=review, user=request.user
362
                )
363

364
            messages.success(request, "Your review was submitted successfully!")
1✔
365
            return redirect(park.detail_page_url())
1✔
366

367
        elif form_type == "check_in":
1✔
368
            ParkPresence.objects.create(
1✔
369
                user=request.user,
370
                park=park,
371
                status="current",
372
                time=now,
373
            )
374

375
        elif form_type == "be_there_at":
1✔
376
            time_str = request.POST.get("time")
1✔
377
            try:
1✔
378
                arrival_time = timezone.datetime.combine(
1✔
379
                    now.date(), timezone.datetime.strptime(time_str, "%H:%M").time()
380
                )
381
                arrival_time = timezone.make_aware(
1✔
382
                    arrival_time
383
                )  # Make it timezone aware
384
            except (ValueError, TypeError):
×
385
                arrival_time = None
×
386

387
            if arrival_time and arrival_time >= now:
1✔
388
                ParkPresence.objects.create(
1✔
389
                    user=request.user,
390
                    park=park,
391
                    status="on_the_way",
392
                    time=arrival_time,
393
                )
394
        # report reviews
395
        elif form_type == "report_review":
1✔
396
            if request.user.is_authenticated:
1✔
397
                review_id = request.POST.get("review_id")
1✔
398
                reason = request.POST.get("reason", "").strip()
1✔
399

400
                if review_id and reason:
1✔
401
                    review = get_object_or_404(Review, id=review_id)
1✔
402

403
                    # prevent duplicate reports by the same user
404
                    exists = ReviewReport.objects.filter(
1✔
405
                        review=review, reported_by=request.user
406
                    ).exists()
407

408
                    if exists:
1✔
409
                        messages.error(
1✔
410
                            request, "You have already reported this review before."
411
                        )
412
                    else:
413
                        ReviewReport.objects.create(
1✔
414
                            review=review, reported_by=request.user, reason=reason
415
                        )
416
                        messages.success(
1✔
417
                            request, "Your review report was submitted successfully."
418
                        )
419
            else:
420
                messages.error(request, "You must be logged in to report a review.")
×
421

422
            return redirect(park.detail_page_url())
1✔
423

424
        elif form_type == "submit_reply":
1✔
425
            if request.user.is_authenticated:
1✔
426
                parent_review_id = request.POST.get("parent_review_id")
1✔
427
                reply_text = request.POST.get("reply_text", "").strip()
1✔
428
                parent_reply_id = request.POST.get("parent_reply_id")
1✔
429

430
                if parent_review_id and reply_text:
1✔
431
                    parent_review = get_object_or_404(Review, id=parent_review_id)
1✔
432

433
                    parent_reply = None
1✔
434
                    if parent_reply_id:
1✔
435
                        try:
1✔
436
                            parent_reply = Reply.objects.get(id=parent_reply_id)
1✔
437
                        except Reply.DoesNotExist:
1✔
438
                            parent_reply = None  # fallback: just attach to review
1✔
439

440
                    Reply.objects.create(
1✔
441
                        review=parent_review,
442
                        user=request.user,
443
                        text=reply_text,
444
                        parent_reply=parent_reply,
445
                    )
446

447
                    messages.success(request, "Reply submitted successfully!")
1✔
448
        return redirect(park.detail_page_url())
1✔
449

450
    park_json = json.dumps(model_to_dict(park))
1✔
451

452
    return render(
1✔
453
        request,
454
        "parks/park_detail.html",
455
        {
456
            "park": park,
457
            "images": images,
458
            "reviews": reviews,
459
            "park_json": park_json,
460
            "average_rating": average_rating,
461
            "current_count": current_count,
462
            "on_the_way_count": on_the_way_count,
463
        },
464
    )
465

466

467
def try_hard_delete_review_if_all_replies_deleted(review):
1✔
468
    if review.is_deleted and not review.replies.filter(is_deleted=False).exists():
1✔
NEW
469
        ParkImage.objects.filter(review=review).delete()
×
NEW
470
        review.delete()
×
471

472

473
@login_required
1✔
474
def delete_review(request, review_id):
1✔
475
    review = get_object_or_404(Review, id=review_id)
1✔
476

477
    # Ensure the current user owns the review
478
    if request.user != review.user:
1✔
UNCOV
479
        return HttpResponseForbidden("You are not allowed to delete this review.")
×
480

481
    # If there are any non-deleted replies, perform soft-delete
482
    if review.replies.filter(is_deleted=False).exists():
1✔
NEW
483
        review.is_deleted = True
×
NEW
484
        review.text = ""
×
NEW
485
        review.save()
×
486
    else:
487
        # Delete associated images (if any), then delete the review
488
        review.images.all().delete()
1✔
489
        review.delete()
1✔
490

491
    messages.success(request, "You have successfully deleted the review!")
1✔
492
    return redirect(review.park.detail_page_url())
1✔
493

494

495
@login_required
1✔
496
def delete_image(request, image_id):
1✔
497
    image = get_object_or_404(ParkImage, id=image_id)
1✔
498
    if image.user == request.user:
1✔
499
        image.delete()
1✔
500
        messages.success(request, "You have successfully deleted the image!")
1✔
501
        return redirect(image.park.detail_page_url())
1✔
502
    return HttpResponseForbidden("You are not allowed to delete this image.")
×
503

504

505
def contact_view(request):
1✔
506
    return render(request, "parks/contact.html")
×
507

508

509
@login_required
1✔
510
def report_image(request, image_id):
1✔
511
    image = get_object_or_404(ParkImage, id=image_id)
1✔
512

513
    if request.method == "POST":
1✔
514
        reason = request.POST.get("reason", "").strip()
1✔
515
        if reason:
1✔
516
            # Check if this user already reported this image
517
            already_reported = ImageReport.objects.filter(
1✔
518
                user=request.user, image=image
519
            ).exists()
520
            if already_reported:
1✔
521
                messages.error(request, "You have already reported this image before.")
1✔
522
            else:
523
                ImageReport.objects.create(
1✔
524
                    user=request.user, image=image, reason=reason
525
                )
526
                messages.success(request, "You have successfully reported the image!")
1✔
527
        return redirect(image.park.detail_page_url())
1✔
528

529
    return redirect(image.park.detail_page_url())
×
530

531

532
@login_required
1✔
533
def delete_reply(request, reply_id):
1✔
534
    reply = get_object_or_404(Reply, id=reply_id)
1✔
535

536
    if reply.user != request.user:
1✔
537
        messages.error(request, "You can only delete your own replies.")
1✔
538
        return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
539

540
    if reply.children.filter(is_deleted=False).exists():
1✔
541
        # Soft-delete: mark deleted, clear text
NEW
542
        reply.is_deleted = True
×
NEW
543
        reply.text = ""
×
NEW
544
        reply.save()
×
545
    else:
546
        reply.delete()
1✔
547

548
    messages.success(request, "Reply deleted successfully.")
1✔
549
    # check & hard delete
550
    try_hard_delete_review_if_all_replies_deleted(reply.review)
1✔
551
    return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
552

553

554
@login_required
1✔
555
def report_reply(request, reply_id):
1✔
556
    if request.method == "POST":
1✔
557
        reason = request.POST.get("reason", "").strip()
1✔
558
        reply = get_object_or_404(Reply, id=reply_id)
1✔
559
        if reply.user != request.user and reason:
1✔
560
            ReplyReport.objects.create(reply=reply, user=request.user, reason=reason)
1✔
561
            messages.success(request, "Reply reported successfully.")
1✔
562
        else:
563
            messages.error(request, "You cannot report your own reply.")
1✔
564
    return redirect(request.META.get("HTTP_REFERER", "/"))
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