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

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

16 Apr 2025 10:57PM UTC coverage: 92.942% (-4.8%) from 97.744%
506

Pull #261

travis-pro

web-flow
Merge pull request #260 from gcivil-nyu-org/Han_develop

Han develop
Pull Request #261: Develop

392 of 470 new or added lines in 16 files covered. (83.4%)

2 existing lines in 2 files now uncovered.

1330 of 1431 relevant lines covered (92.94%)

0.93 hits per line

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

79.37
/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

38
@login_required
1✔
39
@require_POST
1✔
40
def checkin_view(request):
1✔
NEW
41
    data = json.loads(request.body)
×
NEW
42
    park_id = data.get("park_id")
×
NEW
43
    park = get_object_or_404(DogRunNew, id=park_id)
×
44

45
    # Remove existing 'current' check-ins from other parks
NEW
46
    ParkPresence.objects.filter(user=request.user, status="current").exclude(
×
47
        park=park
48
    ).delete()
49

50
    # Check in to this park
NEW
51
    presence, created = ParkPresence.objects.update_or_create(
×
52
        user=request.user,
53
        park=park,
54
        defaults={"status": "current", "time": timezone.now()},
55
    )
56

NEW
57
    return JsonResponse({"status": "checked in", "new": created})
×
58

59

60
@login_required
1✔
61
@require_POST
1✔
62
def bethere_view(request):
1✔
NEW
63
    try:
×
NEW
64
        data = json.loads(request.body)
×
NEW
65
        park_id = data.get("park_id")
×
NEW
66
        time_str = data.get("time")  # e.g. "17:30"
×
67

NEW
68
        if not park_id or not time_str:
×
NEW
69
            return JsonResponse({"error": "Missing park_id or time"}, status=400)
×
70

71
        # Parse and validate time
NEW
72
        try:
×
NEW
73
            arrival_time = datetime.datetime.strptime(time_str, "%H:%M").time()
×
NEW
74
        except ValueError:
×
NEW
75
            return JsonResponse({"error": "Invalid time format"}, status=400)
×
76

NEW
77
        current_datetime = now()
×
NEW
78
        today = current_datetime.date()
×
NEW
79
        arrival_datetime = timezone.make_aware(
×
80
            datetime.datetime.combine(today, arrival_time)
81
        )
82

NEW
83
        if arrival_datetime < current_datetime:
×
NEW
84
            return JsonResponse({"error": "Cannot select a past time"}, status=400)
×
85

NEW
86
        park = get_object_or_404(DogRunNew, id=park_id)
×
87

88
        # ✅ Save the full datetime, not just the time
NEW
89
        presence, created = ParkPresence.objects.update_or_create(
×
90
            user=request.user,
91
            park=park,
92
            defaults={"status": "On their way", "time": arrival_datetime},
93
        )
94

NEW
95
        formatted_time = arrival_datetime.strftime("%I:%M %p")
×
NEW
96
        return JsonResponse({"status": "on their way", "time": formatted_time})
×
97

98
    except Exception as e:
99
        import traceback
100

101
        print(traceback.format_exc())
102
        return JsonResponse({"error": str(e)}, status=500)
103

104

105
def expire_old_checkins():
1✔
106
    expiration_time = timezone.now() - timedelta(hours=1)
1✔
107
    ParkPresence.objects.filter(status="current", time__lt=expiration_time).delete()
1✔
108

109

110
def register_view(request):
1✔
111
    if request.method == "POST":
1✔
112
        form = RegisterForm(request.POST)
1✔
113
        if form.is_valid():
1✔
114
            # Save but don't commit yet
115
            user = form.save(commit=False)
1✔
116
            # If they chose Admin, mark them as staff
117
            if form.cleaned_data["role"] == "admin":
1✔
118
                user.is_staff = True
1✔
119
            user.save()
1✔
120

121
            # Log the user in immediately
122
            login(request, user)
1✔
123
            request.session.save()
1✔
124
            return redirect("home")
1✔
125
    else:
126
        form = RegisterForm()
1✔
127

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

130

131
def home_view(request):
1✔
132
    return render(request, "parks/home.html")
×
133

134

135
@never_cache
1✔
136
def park_and_map(request):
1✔
137
    # Get filter values from GET request
138
    query = request.GET.get("query", "").strip()
1✔
139
    filter_value = request.GET.get("filter", "").strip()
1✔
140
    accessible_value = request.GET.get("accessible", "").strip()
1✔
141
    borough_value = request.GET.get("borough", "").strip().upper()
1✔
142

143
    thumbnail = ParkImage.objects.filter(
1✔
144
        park_id=OuterRef("pk"), is_removed=False, review__is_removed=False
145
    ).values("image")[:1]
146

147
    # Fetch all dog runs from the database
148
    parks = (
1✔
149
        DogRunNew.objects.all()
150
        .order_by("id")
151
        .prefetch_related("images")
152
        .annotate(
153
            thumbnail_url=Cast(Subquery(thumbnail), output_field=CharField()),
154
            average_rating=Avg("reviews__rating", filter=Q(reviews__is_removed=False)),
155
            review_count=Count("reviews", filter=Q(reviews__is_removed=False)),
156
        )
157
    )
158

159
    # Search by ZIP, name, or Google name
160
    if query:
1✔
161
        parks = parks.filter(
×
162
            Q(name__icontains=query)
163
            | Q(google_name__icontains=query)
164
            | Q(zip_code__icontains=query)
165
        )
166

167
    # Filter by park type (e.g., "Off-Leash")
168
    if filter_value:
1✔
169
        parks = parks.filter(dogruns_type__iexact=filter_value)
×
170

171
    # Filter by accessibility only if explicitly set to "True" or "False"
172
    if accessible_value == "True":
1✔
173
        parks = parks.filter(accessible=True)
×
174
    elif accessible_value == "False":
1✔
175
        parks = parks.filter(accessible=False)
×
176

177
    if borough_value:
1✔
178
        parks = parks.filter(borough=borough_value)
1✔
179

180
    # Convert parks to JSON (for JS use)
181
    # parks_json = json.dumps(list(parks.values()))
182

183
    parks_json = json.dumps(
1✔
184
        [
185
            {
186
                **model_to_dict(park),
187
                "thumbnail_url": park.thumbnail_url,
188
                "average_rating": park.average_rating,
189
                "review_count": park.review_count,
190
                "url": park.detail_page_url(),
191
            }
192
            for park in parks
193
        ]
194
    )
195

196
    # Render the template
197
    return render(
1✔
198
        request,
199
        "parks/combined_view.html",
200
        {
201
            "parks": parks,
202
            "parks_json": parks_json,
203
            "query": query,
204
            "selected_type": filter_value,
205
            "selected_accessible": accessible_value,
206
            "selected_borough": borough_value,
207
        },
208
    )
209

210

211
@never_cache
1✔
212
def park_detail(request, slug, id):
1✔
213
    park = get_object_or_404(DogRunNew, id=id)
1✔
214
    if slug != park.slug:
1✔
215
        return HttpResponsePermanentRedirect(park.detail_page_url())
1✔
216

217
    images = ParkImage.objects.filter(
1✔
218
        park=park, is_removed=False, review__is_removed=False
219
    )
220

221
    # Prefetch only non-removed images for each review
222
    visible_images = Prefetch(
1✔
223
        "images",
224
        queryset=ParkImage.objects.filter(is_removed=False),
225
        to_attr="visible_images",
226
    )
227
    reviews = park.reviews.filter(is_removed=False).prefetch_related(visible_images)
1✔
228

229
    average_rating = reviews.aggregate(Avg("rating"))["rating__avg"]
1✔
230

231
    # Clean up expired "On their way" entries
232
    now = localtime()
1✔
233
    # Call the function to expire old check-ins
234
    expire_old_checkins()
1✔
235

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

238
    # Updated counts after cleanup
239
    current_count = ParkPresence.objects.filter(park=park, status="current").count()
1✔
240
    on_the_way_count = ParkPresence.objects.filter(
1✔
241
        park=park, status="On their way", time__isnull=False, time__gte=now
242
    ).count()
243

244
    if request.user.is_authenticated and request.method == "POST":
1✔
245
        form_type = request.POST.get("form_type")
1✔
246

247
        if form_type == "submit_review":
1✔
248
            review_text = request.POST.get("text", "").strip()
1✔
249
            rating_value = request.POST.get("rating", "").strip()
1✔
250

251
            if not rating_value.isdigit():
1✔
252
                messages.error(request, "Please select a rating before submitting.")
×
253
                return redirect(park.detail_page_url())
×
254

255
            rating = int(rating_value)
1✔
256
            if rating < 1 or rating > 5:
1✔
257
                return render(
×
258
                    request,
259
                    "parks/park_detail.html",
260
                    {
261
                        "park": park,
262
                        "images": images,
263
                        "reviews": reviews,
264
                        "error_message": "Rating must be between 1 and 5 stars!",
265
                        "average_rating": average_rating,
266
                        "current_count": current_count,
267
                        "on_the_way_count": on_the_way_count,
268
                    },
269
                )
270

271
            review = Review.objects.create(
1✔
272
                park=park,
273
                text=review_text if review_text else "",
274
                rating=rating,
275
                user=request.user,
276
            )
277
            images = request.FILES.getlist("images")
1✔
278
            ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]
1✔
279

280
            invalid_type = any(
1✔
281
                img.content_type not in ALLOWED_IMAGE_TYPES for img in images
282
            )
283

284
            if invalid_type:
1✔
NEW
285
                messages.error(request, "Only JPEG, PNG, or WebP images are allowed.")
×
NEW
286
                review.delete()
×
NEW
287
                return redirect("park_detail", slug=park.slug, id=park.id)
×
288

289
            MAX_IMAGE_SIZE = 5 * 1024 * 1024  # 5 MB
1✔
290

291
            invalid_images = [img for img in images if img.size > MAX_IMAGE_SIZE]
1✔
292

293
            if invalid_images:
1✔
NEW
294
                messages.error(request, "Each image must be under 5 MB.")
×
NEW
295
                review.delete()
×
NEW
296
                return redirect("park_detail", slug=park.slug, id=park.id)
×
297

298
            # Save valid images
299
            for image in images:
1✔
300
                ParkImage.objects.create(
1✔
301
                    park=park, image=image, review=review, user=request.user
302
                )
303

304
            messages.success(request, "Your review was submitted successfully!")
1✔
305
            return redirect(park.detail_page_url())
1✔
306

307
        elif form_type == "check_in":
1✔
308
            ParkPresence.objects.create(
1✔
309
                user=request.user,
310
                park=park,
311
                status="current",
312
                time=now,
313
            )
314

315
        elif form_type == "be_there_at":
1✔
316
            time_str = request.POST.get("time")
1✔
317
            try:
1✔
318
                arrival_time = timezone.datetime.combine(
1✔
319
                    now.date(), timezone.datetime.strptime(time_str, "%H:%M").time()
320
                )
321
                arrival_time = timezone.make_aware(
1✔
322
                    arrival_time
323
                )  # Make it timezone aware
NEW
324
            except (ValueError, TypeError):
×
NEW
325
                arrival_time = None
×
326

327
            if arrival_time and arrival_time >= now:
1✔
328
                ParkPresence.objects.create(
1✔
329
                    user=request.user,
330
                    park=park,
331
                    status="on_the_way",
332
                    time=arrival_time,
333
                )
334
        # report reviews
335
        elif form_type == "report_review":
1✔
336
            if request.user.is_authenticated:
1✔
337
                review_id = request.POST.get("review_id")
1✔
338
                reason = request.POST.get("reason", "").strip()
1✔
339

340
                if review_id and reason:
1✔
341
                    review = get_object_or_404(Review, id=review_id)
1✔
342

343
                    # prevent duplicate reports by the same user
344
                    exists = ReviewReport.objects.filter(
1✔
345
                        review=review, reported_by=request.user
346
                    ).exists()
347

348
                    if exists:
1✔
349
                        messages.error(
1✔
350
                            request, "You have already reported this review before."
351
                        )
352
                    else:
353
                        ReviewReport.objects.create(
1✔
354
                            review=review, reported_by=request.user, reason=reason
355
                        )
356
                        messages.success(
1✔
357
                            request, "Your review report was submitted successfully."
358
                        )
359
            else:
NEW
360
                messages.error(request, "You must be logged in to report a review.")
×
361

362
            return redirect(park.detail_page_url())
1✔
363

364
        elif form_type == "submit_reply":
1✔
365
            if request.user.is_authenticated:
1✔
366
                parent_review_id = request.POST.get("parent_review_id")
1✔
367
                reply_text = request.POST.get("reply_text", "").strip()
1✔
368
                parent_reply_id = request.POST.get("parent_reply_id")
1✔
369

370
                if parent_review_id and reply_text:
1✔
371
                    parent_review = get_object_or_404(Review, id=parent_review_id)
1✔
372

373
                    parent_reply = None
1✔
374
                    if parent_reply_id:
1✔
375
                        try:
1✔
376
                            parent_reply = Reply.objects.get(id=parent_reply_id)
1✔
377
                        except Reply.DoesNotExist:
1✔
378
                            parent_reply = None  # fallback: just attach to review
1✔
379

380
                    Reply.objects.create(
1✔
381
                        review=parent_review,
382
                        user=request.user,
383
                        text=reply_text,
384
                        parent_reply=parent_reply,
385
                    )
386

387
                    messages.success(request, "Reply submitted successfully!")
1✔
388
        return redirect(park.detail_page_url())
1✔
389

390
    park_json = json.dumps(model_to_dict(park))
1✔
391

392
    return render(
1✔
393
        request,
394
        "parks/park_detail.html",
395
        {
396
            "park": park,
397
            "images": images,
398
            "reviews": reviews,
399
            "park_json": park_json,
400
            "average_rating": average_rating,
401
            "current_count": current_count,
402
            "on_the_way_count": on_the_way_count,
403
        },
404
    )
405

406

407
@login_required
1✔
408
def delete_review(request, review_id):
1✔
409
    review = get_object_or_404(Review, id=review_id)
1✔
410
    if request.user == review.user:
1✔
411
        review.delete()
1✔
412
        messages.success(request, "You have successfully deleted the review!")
1✔
413
        return redirect(review.park.detail_page_url())
1✔
414
    else:
415
        return HttpResponseForbidden("You are not allowed to delete this review.")
×
416

417

418
@login_required
1✔
419
def delete_image(request, image_id):
1✔
420
    image = get_object_or_404(ParkImage, id=image_id)
1✔
421
    if image.user == request.user:
1✔
422
        image.delete()
1✔
423
        messages.success(request, "You have successfully deleted the image!")
1✔
424
        return redirect(image.park.detail_page_url())
1✔
425
    return HttpResponseForbidden("You are not allowed to delete this image.")
×
426

427

428
def contact_view(request):
1✔
UNCOV
429
    return render(request, "parks/contact.html")
×
430

431

432
@login_required
1✔
433
def report_image(request, image_id):
1✔
434
    image = get_object_or_404(ParkImage, id=image_id)
1✔
435

436
    if request.method == "POST":
1✔
437
        reason = request.POST.get("reason", "").strip()
1✔
438
        if reason:
1✔
439
            # Check if this user already reported this image
440
            already_reported = ImageReport.objects.filter(
1✔
441
                user=request.user, image=image
442
            ).exists()
443
            if already_reported:
1✔
444
                messages.error(request, "You have already reported this image before.")
1✔
445
            else:
446
                ImageReport.objects.create(
1✔
447
                    user=request.user, image=image, reason=reason
448
                )
449
                messages.success(request, "You have successfully reported the image!")
1✔
450
        return redirect(image.park.detail_page_url())
1✔
451

452
    return redirect(image.park.detail_page_url())
×
453

454

455
@login_required
1✔
456
def delete_reply(request, reply_id):
1✔
457
    reply = get_object_or_404(Reply, id=reply_id)
1✔
458
    if reply.user == request.user:
1✔
459
        reply.delete()
1✔
460
        messages.success(request, "Reply deleted successfully.")
1✔
461
    else:
462
        messages.error(request, "You can only delete your own replies.")
1✔
463
    return redirect(request.META.get("HTTP_REFERER", "/"))
1✔
464

465

466
@login_required
1✔
467
def report_reply(request, reply_id):
1✔
468
    if request.method == "POST":
1✔
469
        reason = request.POST.get("reason", "").strip()
1✔
470
        reply = get_object_or_404(Reply, id=reply_id)
1✔
471
        if reply.user != request.user and reason:
1✔
472
            ReplyReport.objects.create(reply=reply, user=request.user, reason=reason)
1✔
473
            messages.success(request, "Reply reported successfully.")
1✔
474
        else:
475
            messages.error(request, "You cannot report your own reply.")
1✔
476
    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