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

gcivil-nyu-org / fall24-monday-team4 / 412

11 Dec 2024 09:32PM UTC coverage: 97.367% (+0.3%) from 97.063%
412

Pull #167

travis-pro

jeffwong97
Fixed Sign Up UI bug
Pull Request #167: Fixed Sign Up UI bug

2995 of 3076 relevant lines covered (97.37%)

0.97 hits per line

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

89.56
/locations/views.py
1
import h3
1✔
2
import uuid
1✔
3
import json
1✔
4
from django.http import JsonResponse
1✔
5
from django.shortcuts import render, redirect
1✔
6
from locations.templatetags import trip_filters
1✔
7
from user_profile.models import FamilyMembers
1✔
8
from utils.email_utils import FamilyMemberEmails
1✔
9
from django.template.loader import render_to_string
1✔
10
from utils.location_utils import reverse_geocode
1✔
11
from .models import Trip, Match, UserLocation
1✔
12
from chat.models import ChatRoom, Message
1✔
13
from datetime import timedelta, datetime
1✔
14
from django.utils.timezone import make_aware
1✔
15
from django.contrib.auth.models import User
1✔
16
from django.db.models import Q, F
1✔
17
from django.core.paginator import Paginator
1✔
18
from utils.pusher_client import pusher_client
1✔
19
from django.conf import settings
1✔
20
from django.utils import timezone
1✔
21
from django.contrib.auth.decorators import login_required
1✔
22
from user_profile.decorators import emergency_support_required, verification_required
1✔
23
from .decorators import active_trip_required
1✔
24
from django.views.decorators.http import require_http_methods
1✔
25
from django.contrib import messages
1✔
26

27

28
def broadcast_trip_update(trip_id, status, message):
1✔
29
    pusher_client.trigger(
1✔
30
        f"trip-{trip_id}", "status-update", {"status": status, "message": message}
31
    )
32

33

34
@login_required
1✔
35
@verification_required
1✔
36
@active_trip_required
1✔
37
@require_http_methods(["POST"])
1✔
38
def update_location(request):
1✔
39
    try:
1✔
40
        lat = request.POST.get("latitude")
1✔
41
        lng = request.POST.get("longitude")
1✔
42
        UserLocation.objects.update_or_create(
1✔
43
            user=request.user, defaults={"latitude": lat, "longitude": lng}
44
        )
45

46
        # # Add Pusher broadcast for location update
47
        # pusher_client.trigger(
48
        #     f'location-updates',
49
        #     'location-update',
50
        #     {
51
        #         'username': request.user.username,
52
        #         'latitude': lat,
53
        #         'longitude': lng,
54
        #         'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
55
        #     }
56
        # )
57

58
        return JsonResponse({"success": True})
1✔
59
    except Exception as e:
1✔
60
        return JsonResponse({"success": False, "error": str(e)})
1✔
61

62

63
@login_required
1✔
64
@verification_required
1✔
65
@active_trip_required
1✔
66
@require_http_methods(["GET"])
1✔
67
def get_trip_locations(request):
1✔
68
    try:
1✔
69
        trip = Trip.objects.get(user=request.user, status="IN_PROGRESS")
1✔
70

71
        # Get matched users' locations in one query
72
        matched_users = User.objects.filter(
1✔
73
            trip__in=Trip.objects.filter(
74
                (
75
                    Q(matches__trip2=trip, matches__status="ACCEPTED")
76
                    | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
77
                    | Q(matches__trip1=trip, matches__status="ACCEPTED")
78
                    | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
79
                )
80
            ).distinct()
81
        ).exclude(id=request.user.id)
82

83
        locations = UserLocation.objects.filter(user__in=matched_users).values(
1✔
84
            "user__username", "latitude", "longitude", "last_updated"
85
        )
86

87
        return JsonResponse({"success": True, "locations": list(locations)})
1✔
88
    except Exception as e:
1✔
89
        return JsonResponse({"success": False, "error": str(e)})
1✔
90

91

92
@login_required
1✔
93
@verification_required
1✔
94
@require_http_methods(["POST"])
1✔
95
def create_trip(request):
1✔
96
    planned_departure = request.POST.get("planned_departure")
1✔
97
    try:
1✔
98
        # Convert to timezone-aware datetime
99
        planned_departure = make_aware(
1✔
100
            datetime.strptime(planned_departure, "%Y-%m-%dT%H:%M")
101
        )
102

103
        # Validate datetime
104
        now = timezone.now()
1✔
105
        max_date = now + timedelta(days=365)  # 1 year from now
1✔
106

107
        if planned_departure < now:
1✔
108
            return JsonResponse(
×
109
                {
110
                    "success": False,
111
                    "error": "Selected date and time cannot be in the past",
112
                }
113
            )
114
        if planned_departure > max_date:
1✔
115
            return JsonResponse(
×
116
                {
117
                    "success": False,
118
                    "error": "Selected date cannot be more than 1 year in the future",
119
                }
120
            )
121

122
        # Create trip if validation passes
123
        Trip.objects.update_or_create(
1✔
124
            user=request.user,
125
            status="SEARCHING",
126
            defaults={
127
                "start_latitude": request.POST.get("start_latitude"),
128
                "start_longitude": request.POST.get("start_longitude"),
129
                "dest_latitude": request.POST.get("dest_latitude"),
130
                "dest_longitude": request.POST.get("dest_longitude"),
131
                "planned_departure": planned_departure,
132
                "desired_companions": int(request.POST.get("desired_companions")),
133
                "search_radius": int(request.POST.get("search_radius")),
134
                "start_address": request.POST.get("start_address"),
135
                "end_address": request.POST.get("end_address"),
136
            },
137
        )
138

139
        family_members = FamilyMembers.objects.filter(user=request.user)
1✔
140
        if family_members.exists():
1✔
141
            html_message = render_to_string(
×
142
                "emails/trip_create_fam_email.html",
143
                {
144
                    "username": request.user.username,
145
                    "start": request.POST.get("start_address"),
146
                    "end": request.POST.get("end_address"),
147
                    "departure": planned_departure,
148
                    "start_lat": request.POST.get("start_latitude"),
149
                    "start_lng": request.POST.get("start_longitude"),
150
                    "dest_lat": request.POST.get("dest_latitude"),
151
                    "dest_lng": request.POST.get("dest_longitude"),
152
                },
153
            )
154
            FamilyMemberEmails(
×
155
                [member.email for member in family_members],
156
                html_message,
157
                f"{request.user.username}'s Trip Created",
158
            )
159
        return JsonResponse({"success": True})
1✔
160
    except Exception as e:
×
161
        return JsonResponse({"success": False, "error": str(e)})
×
162

163

164
@login_required
1✔
165
@verification_required
1✔
166
def current_trip(request):
1✔
167
    try:
1✔
168
        user_trip = Trip.objects.get(
1✔
169
            user=request.user,
170
            status__in=["SEARCHING", "MATCHED", "READY", "IN_PROGRESS"],
171
        )
172

173
        # Check if trip needs attention
174
        current_time = timezone.now()
1✔
175
        needs_attention = False
1✔
176
        attention_message = ""
1✔
177

178
        if user_trip.status == "SEARCHING":
1✔
179
            time_difference = (
1✔
180
                current_time - user_trip.planned_departure
181
            ).total_seconds() / 60
182
            if time_difference > 30:
1✔
183
                needs_attention = True
×
184
                attention_message = (
×
185
                    "Your planned departure time has passed more than 30 minutes. "
186
                    "Please reschedule or cancel this trip."
187
                )
188

189
        potential_matches = []
1✔
190
        received_matches = []
1✔
191
        filtered_matches = []
1✔
192

193
        if user_trip.status == "SEARCHING" and not needs_attention:
1✔
194
            # Define time window
195
            time_min = user_trip.planned_departure - timedelta(minutes=30)
1✔
196
            time_max = user_trip.planned_departure + timedelta(minutes=30)
1✔
197

198
            # Find potential matches in one query without search_radius filter first
199
            potential_matches = (
1✔
200
                Trip.objects.filter(
201
                    status="SEARCHING",
202
                    planned_departure__range=(time_min, time_max),
203
                    desired_companions=user_trip.desired_companions,
204
                    accepted_companions_count__lt=F("desired_companions"),
205
                )
206
                .exclude(
207
                    Q(user=request.user)
208
                    | Q(
209
                        matches__trip2=user_trip, matches__status="DECLINED"
210
                    )  # They declined us
211
                    | Q(
212
                        matched_with__trip1=user_trip, matched_with__status="DECLINED"
213
                    )  # We declined them
214
                )
215
                .exclude(
216
                    matches__trip2=user_trip,  # No existing match attempts
217
                )
218
            )
219

220
            # Filter by location using the minimum search radius of each pair
221
            for potential_trip in potential_matches:
1✔
222
                # Use the minimum search radius of both trips
223
                min_radius = min(user_trip.search_radius, potential_trip.search_radius)
1✔
224

225
                # Get resolution and ring size based on minimum radius
226
                resolution, ring_size = get_h3_resolution_and_ring_size(min_radius)
1✔
227

228
                # Convert locations to hexagons with calculated resolution
229
                user_start_hex = h3.latlng_to_cell(
1✔
230
                    float(user_trip.start_latitude),
231
                    float(user_trip.start_longitude),
232
                    resolution,
233
                )
234
                user_dest_hex = h3.latlng_to_cell(
1✔
235
                    float(user_trip.dest_latitude),
236
                    float(user_trip.dest_longitude),
237
                    resolution,
238
                )
239

240
                potential_start_hex = h3.latlng_to_cell(
1✔
241
                    float(potential_trip.start_latitude),
242
                    float(potential_trip.start_longitude),
243
                    resolution,
244
                )
245
                potential_dest_hex = h3.latlng_to_cell(
1✔
246
                    float(potential_trip.dest_latitude),
247
                    float(potential_trip.dest_longitude),
248
                    resolution,
249
                )
250

251
                # Check if locations are within the minimum search radius
252
                if potential_start_hex in h3.grid_disk(
1✔
253
                    user_start_hex, ring_size
254
                ) and potential_dest_hex in h3.grid_disk(user_dest_hex, ring_size):
255
                    filtered_matches.append(potential_trip)
1✔
256

257
            received_matches = Match.objects.filter(
1✔
258
                trip2=user_trip, status="PENDING"
259
            ).select_related("trip1__user")
260

261
            for newTripMatch in filtered_matches:
1✔
262
                broadcast_trip_update(
1✔
263
                    newTripMatch.id, "SEARCHING", "New potential companion available"
264
                )
265

266
        return render(
1✔
267
            request,
268
            "locations/current_trip.html",
269
            {
270
                "user_trip": user_trip,
271
                "potential_matches": filtered_matches,
272
                "received_matches": received_matches,
273
                "pusher_key": settings.PUSHER_KEY,
274
                "pusher_cluster": settings.PUSHER_CLUSTER,
275
                "needs_attention": needs_attention,
276
                "attention_message": attention_message,
277
            },
278
        )
279

280
    except Exception:
1✔
281
        return render(
1✔
282
            request,
283
            "locations/current_trip.html",
284
            {"error": "No active trip found. Create a trip first."},
285
        )
286

287

288
def get_h3_resolution_and_ring_size(radius_meters):
1✔
289
    """
290
    Convert a radius in meters to appropriate H3 resolution and ring size.
291
    Returns (resolution, ring_size) tuple.
292
    """
293
    # H3 resolutions and their approximate edge lengths in meters
294
    # These are approximate values - adjust based on your needs
295
    resolution_map = [
1✔
296
        (8, 461.354684),  # ~460m
297
        (9, 174.375668),  # ~174m
298
        (10, 65.907807),  # ~66m
299
        (11, 24.910561),  # ~25m
300
        (12, 9.415526),  # ~9.4m
301
    ]
302

303
    # Find the appropriate resolution
304
    chosen_res = 10  # default
1✔
305
    for res, edge_length in resolution_map:
1✔
306
        if edge_length < radius_meters / 2:
1✔
307
            chosen_res = res
1✔
308
            break
1✔
309

310
    # Calculate ring size based on radius and edge length
311
    chosen_edge_length = [edge for res, edge in resolution_map if res == chosen_res][0]
1✔
312
    ring_size = max(1, int(radius_meters / (chosen_edge_length * 2)))
1✔
313

314
    return chosen_res, ring_size
1✔
315

316

317
@login_required
1✔
318
@verification_required
1✔
319
def reschedule_trip(request):
1✔
320
    try:
1✔
321
        trip = Trip.objects.get(user=request.user, status="SEARCHING")
1✔
322

323
        if request.method == "POST":
×
324
            new_departure = make_aware(
×
325
                datetime.strptime(
326
                    request.POST.get("planned_departure"), "%Y-%m-%dT%H:%M"
327
                )
328
            )
329
            current_time = timezone.now()
×
330
            if new_departure <= current_time:
×
331
                messages.error(request, "Please select a future date and time.")
×
332
                return render(request, "locations/reschedule_trip.html", {"trip": trip})
×
333

334
            trip.planned_departure = new_departure
×
335
            trip.save()
×
336

337
            family_members = FamilyMembers.objects.filter(user=request.user)
×
338
            if family_members.exists():
×
339
                html_message = render_to_string(
×
340
                    "emails/trip_reschedule_fam_email.html",
341
                    {
342
                        "username": request.user.username,
343
                        "start": trip.start_address,
344
                        "end": trip.end_address,
345
                        "departure": new_departure,
346
                        "start_lat": trip.start_latitude,
347
                        "start_lng": trip.start_longitude,
348
                        "dest_lat": trip.dest_latitude,
349
                        "dest_lng": trip.dest_longitude,
350
                    },
351
                )
352
                FamilyMemberEmails(
×
353
                    [member.email for member in family_members],
354
                    html_message,
355
                    f"{request.user.username}'s Trip Rescheduled",
356
                )
357
            return redirect("current_trip")
×
358
        return render(request, "locations/reschedule_trip.html", {"trip": trip})
×
359
    except Trip.DoesNotExist:
1✔
360
        return redirect("home")
1✔
361

362

363
@login_required
1✔
364
@verification_required
1✔
365
@active_trip_required
1✔
366
@require_http_methods(["POST"])
1✔
367
def send_match_request(request):
1✔
368
    trip_id = request.POST.get("trip_id")
1✔
369
    action = request.POST.get("action")
1✔
370

371
    try:
1✔
372
        user_trip = Trip.objects.get(user=request.user, status="SEARCHING")
1✔
373

374
        if action == "cancel":
1✔
375
            Match.objects.filter(
1✔
376
                trip1=user_trip, trip2_id=trip_id, status="PENDING"
377
            ).delete()
378

379
            broadcast_trip_update(trip_id, "UNREQUESTED", "New match request recinded")
1✔
380
        else:
381
            Match.objects.get_or_create(
1✔
382
                trip1=user_trip, trip2_id=trip_id, defaults={"status": "PENDING"}
383
            )
384

385
            broadcast_trip_update(trip_id, "REQUESTED", "New match request received")
1✔
386

387
        return JsonResponse({"success": True})
1✔
388
    except Exception as e:
1✔
389
        return JsonResponse({"success": False, "error": str(e)})
1✔
390

391

392
@login_required
1✔
393
@verification_required
1✔
394
@active_trip_required
1✔
395
@require_http_methods(["POST"])
1✔
396
def handle_match_request(request):
1✔
397
    match_id = request.POST.get("match_id")
1✔
398
    action = request.POST.get("action")
1✔
399

400
    try:
1✔
401
        match = Match.objects.get(
1✔
402
            id=match_id,
403
            trip2__user=request.user,  # Ensure the user owns the receiving trip
404
            status="PENDING",
405
        )
406

407
        if action == "accept":
1✔
408
            match.status = "ACCEPTED"
1✔
409
            match.save()
1✔
410

411
            # Update both trips atomically to MATCHED status only
412
            for trip in [match.trip1, match.trip2]:
1✔
413
                if trip.status != "SEARCHING":
1✔
414
                    raise ValueError(f"Trip {trip.id} is no longer accepting matches")
1✔
415

416
                Trip.objects.filter(id=trip.id).update(
1✔
417
                    accepted_companions_count=F("accepted_companions_count") + 1,
418
                    status="MATCHED",  # Both trips should be MATCHED first
419
                )
420

421
                # Check and update status if matched
422
                trip.refresh_from_db()
1✔
423
                if trip.accepted_companions_count >= trip.desired_companions:
1✔
424
                    trip.status = "MATCHED"
1✔
425
                    trip.save()
1✔
426

427
            # Create chatroom if needed
428
            if not match.chatroom:
1✔
429
                room_name = f"Trip_Group_{uuid.uuid4()}"
1✔
430
                chat_room = ChatRoom.objects.create(
1✔
431
                    name=room_name, description="Trip Group Chat"
432
                )
433
                # Add users to chatroom
434
                chat_room.users.add(match.trip1.user, match.trip2.user)
1✔
435

436
                match.chatroom = chat_room
1✔
437
                match.save()
1✔
438

439
                Trip.objects.filter(id__in=[match.trip1.id, match.trip2.id]).update(
1✔
440
                    chatroom=chat_room
441
                )
442

443
            # Now clean up other pending requests from matched users
444
            other_pending = Match.objects.filter(
1✔
445
                (Q(trip1=match.trip1) | Q(trip1=match.trip2)), status="PENDING"
446
            )
447

448
            # Get affected trips before deletion
449
            affected_trip_ids = set(other_pending.values_list("trip2_id", flat=True))
1✔
450
            other_pending.delete()
1✔
451

452
            # Notify affected users who received requests
453
            for trip_id in affected_trip_ids:
1✔
454
                broadcast_trip_update(
1✔
455
                    trip_id,
456
                    "SEARCHING",
457
                    f"{match.trip1.user.username} is no longer available",
458
                )
459

460
            for trip in [match.trip1, match.trip2]:
1✔
461
                broadcast_trip_update(
1✔
462
                    trip.id,
463
                    "MATCHED",
464
                    (
465
                        f"Match accepted between {match.trip1.user.username} and "
466
                        f"{match.trip2.user.username}"
467
                    ),
468
                )
469
        else:
470
            match.status = "DECLINED"
1✔
471
            match.save()
1✔
472

473
            # Notify the requester their request was declined
474
            broadcast_trip_update(
1✔
475
                match.trip1.id,
476
                "SEARCHING",
477
                f"{request.user.username} declined your request",
478
            )
479
        return redirect("current_trip")
1✔
480
    except Match.DoesNotExist:
1✔
481
        return JsonResponse(
1✔
482
            {"success": False, "error": "Match request not found or already handled"},
483
            status=404,
484
        )
485
    except ValueError as e:
1✔
486
        return JsonResponse({"success": False, "error": str(e)}, status=400)
1✔
487
    except Exception as e:
1✔
488
        return JsonResponse({"success": False, "error": str(e)}, status=500)
1✔
489

490

491
def send_system_message(chat_room, message):
1✔
492
    Message.objects.create(chat_room=chat_room, message=message, message_type="SYSTEM")
1✔
493

494
    pusher_client.trigger(
1✔
495
        f"chat-{chat_room.id}", "message-event", {"message": message, "type": "system"}
496
    )
497

498

499
def send_ems_message(chat_room, sytem_message, chat_message, user):
1✔
500
    Message.objects.create(
1✔
501
        chat_room=chat_room, message=sytem_message, message_type="EMS_SYSTEM"
502
    )
503
    Message.objects.create(
1✔
504
        chat_room=chat_room,
505
        user=user,
506
        message=chat_message,
507
        message_type="EMS_PANIC_MESSAGE",
508
    )
509

510
    # Send messages separately
511
    pusher_client.trigger(
1✔
512
        f"chat-{chat_room.id}",
513
        "message-event",
514
        {"message": sytem_message, "type": "ems_system"},
515
    )
516

517
    pusher_client.trigger(
1✔
518
        f"chat-{chat_room.id}",
519
        "message-event",
520
        {
521
            "message": chat_message,
522
            "username": user.username,
523
            "type": "ems_panic_message",
524
        },
525
    )
526

527

528
@login_required
1✔
529
@verification_required
1✔
530
@active_trip_required
1✔
531
@require_http_methods(["POST"])
1✔
532
def start_trip(request):
1✔
533
    trip = Trip.objects.get(user=request.user, status__in=["MATCHED", "READY"])
1✔
534

535
    # Set to READY if not already
536
    if trip.status == "MATCHED":
1✔
537
        trip.status = "READY"
1✔
538
        trip.save()
1✔
539

540
        # Broadcast update
541
        broadcast_trip_update(
1✔
542
            trip.id, "READY", f"{request.user.username} is ready to start"
543
        )
544

545
        if trip.chatroom:
1✔
546
            send_system_message(
1✔
547
                trip.chatroom,
548
                f"{request.user.username} is ready to start the trip.",
549
            )
550

551
    # Get all matched trips in one query, including both directions of matches
552
    matched_trips = (
1✔
553
        Trip.objects.filter(
554
            (
555
                Q(matches__trip2=trip, matches__status="ACCEPTED")
556
                | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
557
                | Q(matches__trip1=trip, matches__status="ACCEPTED")
558
                | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
559
            )
560
        )
561
        .exclude(id=trip.id)
562
        .distinct()
563
    )
564

565
    # If all trips (including current one) are READY, update all to IN_PROGRESS
566
    if matched_trips.exists() and all(t.status == "READY" for t in matched_trips):
1✔
567
        # Update all trips to IN_PROGRESS atomically
568
        matched_trips.update(status="IN_PROGRESS")
1✔
569
        trip.status = "IN_PROGRESS"  # Update the current trip too
1✔
570
        trip.save()
1✔
571

572
        family_members = FamilyMembers.objects.filter(user=request.user)
1✔
573
        if family_members.exists():
1✔
574
            companions = [trip.user.username for trip in matched_trips]
×
575
            html_message = render_to_string(
×
576
                "emails/trip_start_fam_email.html",
577
                {
578
                    "username": request.user.username,
579
                    "start": trip.start_address,
580
                    "end": trip.end_address,
581
                    "departure": trip.planned_departure,
582
                    "companions": list(companions),
583
                },
584
            )
585

586
            FamilyMemberEmails(
×
587
                [member.email for member in family_members],
588
                html_message,
589
                f"{request.user.username}'s Trip Started",
590
            )
591

592
        # Broadcast update to all participants
593
        broadcast_trip_update(trip.id, "IN_PROGRESS", "Trip is now in progress")
1✔
594
        for matched_trip in matched_trips:
1✔
595
            broadcast_trip_update(
1✔
596
                matched_trip.id, "IN_PROGRESS", "Trip is now in progress"
597
            )
598

599
        if trip.chatroom:
1✔
600
            send_system_message(
1✔
601
                trip.chatroom, "All members are ready. Trip is now in progress!"
602
            )
603

604
    return redirect("current_trip")
1✔
605

606

607
@login_required
1✔
608
@verification_required
1✔
609
@active_trip_required
1✔
610
@require_http_methods(["POST"])
1✔
611
def cancel_trip(request):
1✔
612
    try:
1✔
613
        trip = Trip.objects.get(
1✔
614
            user=request.user, status__in=["SEARCHING", "MATCHED", "READY"]
615
        )
616

617
        # Update all matched trips' companion counts
618
        affected_trips = list(
1✔
619
            Trip.objects.filter(
620
                (
621
                    Q(matches__trip2=trip, matches__status="ACCEPTED")
622
                    | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
623
                    | Q(matches__trip1=trip, matches__status="ACCEPTED")
624
                    | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
625
                ),
626
                status__in=["MATCHED", "READY"],  # Add this back
627
            )
628
            .exclude(id=trip.id)
629
            .distinct()
630
        )
631

632
        # Cancel all associated matches at once
633
        Match.objects.filter(Q(trip1=trip) | Q(trip2=trip)).update(status="DECLINED")
1✔
634

635
        for affected_trip in affected_trips:
1✔
636
            affected_trip.accepted_companions_count = affected_trip.matches.filter(
1✔
637
                status="ACCEPTED"
638
            ).count()
639
            affected_trip.status = "SEARCHING"
1✔
640
            affected_trip.save()
1✔
641

642
            broadcast_trip_update(
1✔
643
                affected_trip.id,
644
                "SEARCHING",
645
                f"{request.user.username} has cancelled their trip",
646
            )
647

648
        # Update the cancelled trip
649
        trip.status = "CANCELLED"
1✔
650
        trip.accepted_companions_count = 0
1✔
651
        trip.save()
1✔
652

653
        # Also broadcast to potential matches who might now be compatible
654
        time_min = trip.planned_departure - timedelta(minutes=30)
1✔
655
        time_max = trip.planned_departure + timedelta(minutes=30)
1✔
656

657
        potential_matches = Trip.objects.filter(
1✔
658
            status="SEARCHING",
659
            planned_departure__range=(time_min, time_max),
660
            desired_companions=trip.desired_companions,
661
        ).exclude(user=request.user)
662

663
        for tripMatch in potential_matches:
1✔
664
            broadcast_trip_update(
1✔
665
                tripMatch.id, "SEARCHING", "New potential companion available"
666
            )
667

668
        return redirect("home")
1✔
669
    except Exception as e:
1✔
670
        return JsonResponse({"success": False, "error": str(e)})
1✔
671

672

673
@login_required
1✔
674
@verification_required
1✔
675
def previous_trips(request):
1✔
676
    trip_list = (
1✔
677
        Trip.objects.filter(user=request.user, status__in=["COMPLETED", "CANCELLED"])
678
        .prefetch_related("matches__trip1__user", "matches__trip2__user", "chatroom")
679
        .order_by("-created_at")
680
    )
681

682
    paginator = Paginator(trip_list, 3)
1✔
683
    page = request.GET.get("page")
1✔
684
    trips = paginator.get_page(page)
1✔
685

686
    trips_data = [
1✔
687
        {
688
            "id": trip.id,
689
            "start_latitude": float(trip.start_latitude),
690
            "start_longitude": float(trip.start_longitude),
691
            "dest_latitude": float(trip.dest_latitude),
692
            "dest_longitude": float(trip.dest_longitude),
693
            "companions": [
694
                (
695
                    match.trip2.user.username
696
                    if match.trip1 == trip
697
                    else match.trip1.user.username
698
                )
699
                for match in trip.matches.filter(status="ACCEPTED")
700
            ],
701
        }
702
        for trip in trips
703
    ]
704

705
    return render(
1✔
706
        request,
707
        "locations/previous_trips.html",
708
        {"trips": trips, "trips_json": json.dumps(trips_data)},
709
    )
710

711

712
@login_required
1✔
713
@verification_required
1✔
714
@active_trip_required
1✔
715
@require_http_methods(["GET"])
1✔
716
def check_panic_users(request):
1✔
717
    try:
×
718
        trip = Trip.objects.get(user=request.user, status="IN_PROGRESS")
×
719
        has_panic = trip_filters.has_panic_users(trip)
×
720
        return JsonResponse({"success": True, "has_panic": has_panic})
×
721
    except Exception as e:
×
722
        return JsonResponse({"success": False, "error": str(e)})
×
723

724

725
@login_required
1✔
726
@verification_required
1✔
727
@active_trip_required
1✔
728
@require_http_methods(["POST"])
1✔
729
def trigger_panic(request):
1✔
730
    try:
1✔
731
        user_location = UserLocation.objects.get(user=request.user)
1✔
732
        panic_locations = UserLocation.objects.filter(panic=True).select_related("user")
1✔
733

734
        active_trip = Trip.objects.get(user=request.user, status="IN_PROGRESS")
1✔
735

736
        user_location.panic = True
1✔
737
        user_location.panic_message = request.POST.get("initial_message")
1✔
738
        user_location.save()
1✔
739

740
        family_members = FamilyMembers.objects.filter(user=request.user)
1✔
741
        if family_members.exists():
1✔
742
            current_address = reverse_geocode(
×
743
                float(user_location.latitude), float(user_location.longitude)
744
            )
745

746
            html_message = render_to_string(
×
747
                "emails/panic_trigger_fam_email.html",
748
                {
749
                    "username": request.user.username,
750
                    "message": user_location.panic_message,
751
                    "location": current_address or "Location Unavailable",
752
                    "latitude": user_location.latitude,
753
                    "longitude": user_location.longitude,
754
                },
755
            )
756
            FamilyMemberEmails(
×
757
                [member.email for member in family_members],
758
                html_message,
759
                f"PANIC ALERT: {request.user.username} Requested Emergency Support",
760
            )
761

762
        # Get all active panic locations for emergency support view
763
        locations_data = [
1✔
764
            {
765
                "id": loc.id,
766
                "username": loc.user.username,
767
                "panic_message": loc.panic_message,
768
                "latitude": float(loc.latitude),
769
                "longitude": float(loc.longitude),
770
            }
771
            for loc in panic_locations
772
        ]
773

774
        pusher_client.trigger(
1✔
775
            "emergency-channel",
776
            "panic-create",
777
            {
778
                "locations": locations_data,
779
                "active_users": UserLocation.objects.count(),
780
                "panic_users": panic_locations.count(),
781
            },
782
        )
783

784
        # Send ems message to their trip's chatroom
785
        if active_trip.chatroom:
1✔
786
            send_ems_message(
1✔
787
                active_trip.chatroom,
788
                f"{request.user.username} has triggered panic mode.",
789
                user_location.panic_message,
790
                request.user,
791
            )
792

793
        return JsonResponse({"success": True, "message": "Panic mode activated."})
1✔
794
    except Exception as e:
1✔
795
        return JsonResponse({"success": False, "error": str(e)})
1✔
796

797

798
@login_required
1✔
799
@verification_required
1✔
800
@emergency_support_required
1✔
801
@require_http_methods(["POST"])
1✔
802
def resolve_panic(request, panic_username):
1✔
803
    try:
1✔
804
        user = User.objects.get(username=panic_username)
1✔
805
        user_location = UserLocation.objects.get(user=user)
1✔
806
        active_trip = Trip.objects.get(user=user, status="IN_PROGRESS")
1✔
807

808
        user_location.panic = False
1✔
809
        user_location.panic_message = None
1✔
810
        user_location.save()
1✔
811

812
        family_members = FamilyMembers.objects.filter(user=user)
1✔
813
        if family_members.exists():
1✔
814
            html_message = render_to_string(
×
815
                "emails/panic_resolve_fam_email.html", {"username": user.username}
816
            )
817
            FamilyMemberEmails(
×
818
                [member.email for member in family_members],
819
                html_message,
820
                f"PANIC ALERT: {user.username}'s Emergency Support Request Resolved",
821
            )
822

823
        # Trigger a Pusher event to update the panic button
824
        pusher_client.trigger(
1✔
825
            "emergency-channel", "panic-resolve", {"username": panic_username}
826
        )
827

828
        # Send ems message to their trip's chatroom
829
        if active_trip.chatroom:
1✔
830
            send_system_message(
1✔
831
                active_trip.chatroom,
832
                f"Emergency support has resolved {user.username}'s panic request.",
833
            )
834

835
        return JsonResponse({"success": True, "message": "Panic mode deactivated."})
1✔
836
    except Exception as e:
1✔
837
        return JsonResponse({"success": False, "error": str(e)})
1✔
838

839

840
@login_required
1✔
841
@verification_required
1✔
842
@emergency_support_required
1✔
843
def emergency_support(request):
1✔
844
    # Fetch only the users who have panic mode activated
845
    user_locations = UserLocation.objects.filter(panic=True).select_related("user")
1✔
846

847
    active_user_count = UserLocation.objects.count()
1✔
848
    panic_user_count = user_locations.count()
1✔
849

850
    user_locations_data = [
1✔
851
        {
852
            "id": location.id,
853
            "latitude": float(location.latitude),
854
            "longitude": float(location.longitude),
855
            "username": location.user.username,
856
            "panic": location.panic,
857
            "panic_message": location.panic_message,
858
        }
859
        for location in user_locations
860
    ]
861

862
    return render(
1✔
863
        request,
864
        "locations/emergency_support.html",
865
        {
866
            "active_users": active_user_count,
867
            "panic_users": panic_user_count,
868
            "locations": user_locations,
869
            "locations_json": json.dumps(user_locations_data),
870
            "pusher_key": settings.PUSHER_KEY,
871
            "pusher_cluster": settings.PUSHER_CLUSTER,
872
        },
873
    )
874

875

876
@login_required
1✔
877
@verification_required
1✔
878
@active_trip_required
1✔
879
@require_http_methods(["POST"])
1✔
880
def complete_trip(request):
1✔
881
    trip = Trip.objects.get(user=request.user, status="IN_PROGRESS")
1✔
882

883
    if not trip.completion_requested:
1✔
884
        trip.completion_requested = True
1✔
885
        trip.save()
1✔
886

887
        if trip.chatroom:
1✔
888
            send_system_message(
1✔
889
                trip.chatroom,
890
                f"{request.user.username} has voted to complete the trip.",
891
            )
892

893
        # Get all connected trips in one query
894
        matched_trips = Trip.objects.filter(
1✔
895
            (
896
                Q(matches__trip2=trip, matches__status="ACCEPTED")
897
                | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
898
                | Q(matches__trip1=trip, matches__status="ACCEPTED")
899
                | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
900
            )
901
        ).distinct()
902

903
        # Count completion votes and total members
904
        completion_votes = matched_trips.filter(completion_requested=True).count()
1✔
905
        total_members = matched_trips.count()
1✔
906

907
        # If majority votes for completion
908
        if completion_votes >= total_members / 2:
1✔
909
            current_time = timezone.now()
1✔
910
            if trip.chatroom:
1✔
911
                send_system_message(
1✔
912
                    trip.chatroom,
913
                    "Majority has voted to complete the trip. Trip is now archived.",
914
                )
915

916
            # Complete all trips in one query
917
            matched_trips.update(
1✔
918
                status="COMPLETED",
919
                completion_requested=True,
920
                completed_at=current_time,
921
            )
922

923
            family_members = FamilyMembers.objects.filter(user=request.user)
1✔
924
            if family_members.exists():
1✔
925
                html_message = render_to_string(
×
926
                    "emails/trip_complete_fam_email.html",
927
                    {"username": request.user.username, "toa": current_time},
928
                )
929
                FamilyMemberEmails(
×
930
                    [member.email for member in family_members],
931
                    html_message,
932
                    f"{request.user.username}'s Trip Completed",
933
                )
934

935
            # Delete UserLocations for all users involved
936
            user_ids = matched_trips.values_list("user", flat=True)
1✔
937
            UserLocation.objects.filter(user__in=user_ids).delete()
1✔
938

939
            # Broadcast completion to all participants
940
            for matched_trip in matched_trips:
1✔
941
                broadcast_trip_update(
1✔
942
                    matched_trip.id, "COMPLETED", "Trip has been completed"
943
                )
944
        return redirect("previous_trips")
1✔
945
    return redirect("current_trip")
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

© 2025 Coveralls, Inc