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

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

16 Dec 2024 08:19PM UTC coverage: 96.872% (-3.0%) from 99.892%
437

Pull #171

travis-pro

web-flow
Merge pull request #170 from gcivil-nyu-org/all_branch_merge

Develop V6.1.2
Pull Request #171: Production V2

153 of 230 new or added lines in 14 files covered. (66.52%)

17 existing lines in 4 files now uncovered.

3004 of 3101 relevant lines covered (96.87%)

0.97 hits per line

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

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

26

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

32

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

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

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

61

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

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

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

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

90

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

102
        # Validate datetime
103
        now = timezone.localtime(timezone.now().replace(second=0, microsecond=0))
1✔
104
        max_date = now + timedelta(days=365)  # 1 year from now
1✔
105

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

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

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

162

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

286

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

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

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

313
    return chosen_res, ring_size
1✔
314

315

316
@login_required
1✔
317
@verification_required
1✔
318
@active_trip_required
1✔
319
@require_http_methods(["POST", "GET"])
1✔
320
def reschedule_trip(request):
1✔
NEW
321
    if request.method == "GET":
×
NEW
322
        return render(request, "locations/reschedule_trip.html")
×
323

NEW
324
    try:
×
NEW
325
        trip = Trip.objects.get(user=request.user, status="SEARCHING")
×
326

NEW
327
        new_departure = make_aware(
×
328
            datetime.strptime(request.POST.get("planned_departure"), "%Y-%m-%dT%H:%M")
329
        )
330

NEW
331
        current_time = timezone.localtime(
×
332
            timezone.now().replace(second=0, microsecond=0)
333
        )
NEW
334
        max_date = current_time + timedelta(days=365)  # 1 year from now
×
335

NEW
336
        if new_departure < current_time:
×
NEW
337
            return JsonResponse(
×
338
                {
339
                    "success": False,
340
                    "error": "Selected date and time cannot be in the past",
341
                }
342
            )
NEW
343
        if new_departure > max_date:
×
NEW
344
            return JsonResponse(
×
345
                {
346
                    "success": False,
347
                    "error": "Selected date cannot be more than 1 year in the future",
348
                }
349
            )
350

NEW
351
        trip.planned_departure = new_departure
×
NEW
352
        trip.save()
×
353

NEW
354
        family_members = FamilyMembers.objects.filter(user=request.user)
×
NEW
355
        if family_members.exists():
×
NEW
356
            html_message = render_to_string(
×
357
                "emails/trip_reschedule_fam_email.html",
358
                {
359
                    "username": request.user.username,
360
                    "start": trip.start_address,
361
                    "end": trip.end_address,
362
                    "departure": new_departure,
363
                    "start_lat": trip.start_latitude,
364
                    "start_lng": trip.start_longitude,
365
                    "dest_lat": trip.dest_latitude,
366
                    "dest_lng": trip.dest_longitude,
367
                },
368
            )
NEW
369
            FamilyMemberEmails(
×
370
                [member.email for member in family_members],
371
                html_message,
372
                f"{request.user.username}'s Trip Rescheduled",
373
            )
NEW
374
        return JsonResponse({"success": True})
×
NEW
375
    except Exception as e:
×
NEW
376
        return JsonResponse({"success": False, "error": str(e)})
×
377

378

379
@login_required
1✔
380
@verification_required
1✔
381
@active_trip_required
1✔
382
@require_http_methods(["POST"])
1✔
383
def send_match_request(request):
1✔
384
    trip_id = request.POST.get("trip_id")
1✔
385
    action = request.POST.get("action")
1✔
386

387
    try:
1✔
388
        user_trip = Trip.objects.get(user=request.user, status="SEARCHING")
1✔
389

390
        if action == "cancel":
1✔
391
            Match.objects.filter(
1✔
392
                trip1=user_trip, trip2_id=trip_id, status="PENDING"
393
            ).delete()
394

395
            broadcast_trip_update(trip_id, "UNREQUESTED", "New match request recinded")
1✔
396
        else:
397
            Match.objects.get_or_create(
1✔
398
                trip1=user_trip, trip2_id=trip_id, defaults={"status": "PENDING"}
399
            )
400

401
            broadcast_trip_update(trip_id, "REQUESTED", "New match request received")
1✔
402

403
        return JsonResponse({"success": True})
1✔
404
    except Exception as e:
1✔
405
        return JsonResponse({"success": False, "error": str(e)})
1✔
406

407

408
@login_required
1✔
409
@verification_required
1✔
410
@active_trip_required
1✔
411
@require_http_methods(["POST"])
1✔
412
def handle_match_request(request):
1✔
413
    match_id = request.POST.get("match_id")
1✔
414
    action = request.POST.get("action")
1✔
415

416
    try:
1✔
417
        match = Match.objects.get(
1✔
418
            id=match_id,
419
            trip2__user=request.user,  # Ensure the user owns the receiving trip
420
            status="PENDING",
421
        )
422

423
        if action == "accept":
1✔
424
            match.status = "ACCEPTED"
1✔
425
            match.save()
1✔
426

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

432
                Trip.objects.filter(id=trip.id).update(
1✔
433
                    accepted_companions_count=F("accepted_companions_count") + 1,
434
                    status="MATCHED",  # Both trips should be MATCHED first
435
                )
436

437
                # Check and update status if matched
438
                trip.refresh_from_db()
1✔
439
                if trip.accepted_companions_count >= trip.desired_companions:
1✔
440
                    trip.status = "MATCHED"
1✔
441
                    trip.save()
1✔
442

443
            # Create chatroom if needed
444
            if not match.chatroom:
1✔
445
                room_name = f"Trip_Group_{uuid.uuid4()}"
1✔
446
                chat_room = ChatRoom.objects.create(
1✔
447
                    name=room_name, description="Trip Group Chat"
448
                )
449
                # Add users to chatroom
450
                chat_room.users.add(match.trip1.user, match.trip2.user)
1✔
451

452
                match.chatroom = chat_room
1✔
453
                match.save()
1✔
454

455
                Trip.objects.filter(id__in=[match.trip1.id, match.trip2.id]).update(
1✔
456
                    chatroom=chat_room
457
                )
458

459
            # Now clean up other pending requests from matched users
460
            other_pending = Match.objects.filter(
1✔
461
                (Q(trip1=match.trip1) | Q(trip1=match.trip2)), status="PENDING"
462
            )
463

464
            # Get affected trips before deletion
465
            affected_trip_ids = set(other_pending.values_list("trip2_id", flat=True))
1✔
466
            other_pending.delete()
1✔
467

468
            # Add other exisitng potential matches to affected trips
469
            time_min = match.trip1.planned_departure - timedelta(minutes=30)
1✔
470
            time_max = match.trip1.planned_departure + timedelta(minutes=30)
1✔
471

472
            potential_matches = Trip.objects.filter(
1✔
473
                status="SEARCHING",
474
                planned_departure__range=(time_min, time_max),
475
                desired_companions=match.trip1.desired_companions,
476
            ).exclude(user__in=[match.trip1.user, match.trip2.user])
477

478
            affected_trip_ids.update(potential_matches.values_list("id", flat=True))
1✔
479

480
            # Notify affected users who received requests and exisitng potential matches
481
            for trip_id in affected_trip_ids:
1✔
482
                broadcast_trip_update(
1✔
483
                    trip_id,
484
                    "SEARCHING",
485
                    f"{match.trip1.user.username} is no longer available",
486
                )
487

488
            for trip in [match.trip1, match.trip2]:
1✔
489
                broadcast_trip_update(
1✔
490
                    trip.id,
491
                    "MATCHED",
492
                    (
493
                        f"Match accepted between {match.trip1.user.username} and "
494
                        f"{match.trip2.user.username}"
495
                    ),
496
                )
497
        else:
498
            match.status = "DECLINED"
1✔
499
            match.save()
1✔
500

501
            # Notify the requester their request was declined
502
            broadcast_trip_update(
1✔
503
                match.trip1.id,
504
                "SEARCHING",
505
                f"{request.user.username} declined your request",
506
            )
507
        return redirect("current_trip")
1✔
508
    except Match.DoesNotExist:
1✔
509
        return JsonResponse(
1✔
510
            {"success": False, "error": "Match request not found or already handled"},
511
            status=404,
512
        )
513
    except ValueError as e:
1✔
514
        return JsonResponse({"success": False, "error": str(e)}, status=400)
1✔
515
    except Exception as e:
1✔
516
        return JsonResponse({"success": False, "error": str(e)}, status=500)
1✔
517

518

519
def send_system_message(chat_room, message):
1✔
520
    Message.objects.create(chat_room=chat_room, message=message, message_type="SYSTEM")
1✔
521

522
    pusher_client.trigger(
1✔
523
        f"chat-{chat_room.id}", "message-event", {"message": message, "type": "system"}
524
    )
525

526

527
def send_ems_message(chat_room, sytem_message, chat_message, user):
1✔
528
    Message.objects.create(
1✔
529
        chat_room=chat_room, message=sytem_message, message_type="EMS_SYSTEM"
530
    )
531
    Message.objects.create(
1✔
532
        chat_room=chat_room,
533
        user=user,
534
        message=chat_message,
535
        message_type="EMS_PANIC_MESSAGE",
536
    )
537

538
    # Send messages separately
539
    pusher_client.trigger(
1✔
540
        f"chat-{chat_room.id}",
541
        "message-event",
542
        {"message": sytem_message, "type": "ems_system"},
543
    )
544

545
    pusher_client.trigger(
1✔
546
        f"chat-{chat_room.id}",
547
        "message-event",
548
        {
549
            "message": chat_message,
550
            "user": {"username": user.username, "id": user.id},
551
            "type": "ems_panic_message",
552
        },
553
    )
554

555

556
@login_required
1✔
557
@verification_required
1✔
558
@active_trip_required
1✔
559
@require_http_methods(["POST"])
1✔
560
def start_trip(request):
1✔
561
    trip = Trip.objects.get(user=request.user, status__in=["MATCHED", "READY"])
1✔
562

563
    # Set to READY if not already
564
    if trip.status == "MATCHED":
1✔
565
        trip.status = "READY"
1✔
566
        trip.save()
1✔
567

568
        # Broadcast update
569
        broadcast_trip_update(
1✔
570
            trip.id, "READY", f"{request.user.username} is ready to start"
571
        )
572

573
        if trip.chatroom:
1✔
574
            send_system_message(
1✔
575
                trip.chatroom,
576
                f"{request.user.username} is ready to start the trip.",
577
            )
578

579
    # Get all matched trips in one query, including both directions of matches
580
    matched_trips = (
1✔
581
        Trip.objects.filter(
582
            (
583
                Q(matches__trip2=trip, matches__status="ACCEPTED")
584
                | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
585
                | Q(matches__trip1=trip, matches__status="ACCEPTED")
586
                | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
587
            )
588
        )
589
        .exclude(id=trip.id)
590
        .distinct()
591
    )
592

593
    # If all trips (including current one) are READY, update all to IN_PROGRESS
594
    if matched_trips.exists() and all(t.status == "READY" for t in matched_trips):
1✔
595
        # Update all trips to IN_PROGRESS atomically
596
        matched_trips.update(status="IN_PROGRESS")
1✔
597
        trip.status = "IN_PROGRESS"  # Update the current trip too
1✔
598
        trip.save()
1✔
599

600
        broadcast_trip_update(trip.id, "IN_PROGRESS", "Trip is now in progress")
1✔
601

602
        for matched_trip in matched_trips:
1✔
603
            family_members = FamilyMembers.objects.filter(user=matched_trip.user)
1✔
604
            if family_members.exists():
1✔
NEW
605
                companions = [trip.user.username] + [
×
606
                    t.user.username
607
                    for t in matched_trips
608
                    if t.user != matched_trip.user
609
                ]
NEW
610
                html_message = render_to_string(
×
611
                    "emails/trip_start_fam_email.html",
612
                    {
613
                        "username": matched_trip.user.username,
614
                        "start": matched_trip.start_address,
615
                        "end": matched_trip.end_address,
616
                        "departure": matched_trip.planned_departure,
617
                        "companions": list(companions),
618
                    },
619
                )
620

NEW
621
                FamilyMemberEmails(
×
622
                    [member.email for member in family_members],
623
                    html_message,
624
                    f"{matched_trip.user.username}'s Trip Started",
625
                )
626

627
            # Broadcast update to all participants
628
            broadcast_trip_update(
1✔
629
                matched_trip.id, "IN_PROGRESS", "Trip is now in progress"
630
            )
631

632
        family_members = FamilyMembers.objects.filter(user=request.user)
1✔
633
        if family_members.exists():
1✔
NEW
634
            companions = [t.user.username for t in matched_trips]
×
NEW
635
            html_message = render_to_string(
×
636
                "emails/trip_start_fam_email.html",
637
                {
638
                    "username": request.user.username,
639
                    "start": trip.start_address,
640
                    "end": trip.end_address,
641
                    "departure": trip.planned_departure,
642
                    "companions": list(companions),
643
                },
644
            )
645

NEW
646
            FamilyMemberEmails(
×
647
                [member.email for member in family_members],
648
                html_message,
649
                f"{request.user.username}'s Trip Started",
650
            )
651

652
        if trip.chatroom:
1✔
653
            send_system_message(
1✔
654
                trip.chatroom, "All members are ready. Trip is now in progress!"
655
            )
656

657
    return redirect("current_trip")
1✔
658

659

660
@login_required
1✔
661
@verification_required
1✔
662
@active_trip_required
1✔
663
@require_http_methods(["POST"])
1✔
664
def cancel_trip(request):
1✔
665
    try:
1✔
666
        trip = Trip.objects.get(
1✔
667
            user=request.user, status__in=["SEARCHING", "MATCHED", "READY"]
668
        )
669

670
        # Update all matched trips' companion counts
671
        affected_trips = list(
1✔
672
            Trip.objects.filter(
673
                (
674
                    Q(matches__trip2=trip, matches__status="ACCEPTED")
675
                    | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
676
                    | Q(matches__trip1=trip, matches__status="ACCEPTED")
677
                    | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
678
                ),
679
                status__in=["MATCHED", "READY"],  # Add this back
680
            )
681
            .exclude(id=trip.id)
682
            .distinct()
683
        )
684

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

688
        for affected_trip in affected_trips:
1✔
689
            affected_trip.accepted_companions_count = affected_trip.matches.filter(
1✔
690
                status="ACCEPTED"
691
            ).count()
692
            affected_trip.status = "SEARCHING"
1✔
693
            affected_trip.save()
1✔
694

695
            # Get family members for all affected users and send emails
696
            family_members = FamilyMembers.objects.filter(user=affected_trip.user)
1✔
697
            if family_members.exists():
1✔
NEW
698
                html_message = render_to_string(
×
699
                    "emails/trip_cancel_fam_email.html",
700
                    {
701
                        "username": affected_trip.user.username,
702
                        "start_address": affected_trip.start_address,
703
                        "end_address": affected_trip.end_address,
704
                    },
705
                )
NEW
706
                FamilyMemberEmails(
×
707
                    [member.email for member in family_members],
708
                    html_message,
709
                    f"{affected_trip.user.username}'s Trip Cancelled",
710
                )
711

712
            broadcast_trip_update(
1✔
713
                affected_trip.id,
714
                "SEARCHING",
715
                f"{request.user.username} has cancelled their trip",
716
            )
717

718
        # Update the cancelled trip
719
        trip.status = "CANCELLED"
1✔
720
        trip.accepted_companions_count = 0
1✔
721
        trip.save()
1✔
722

723
        family_members = FamilyMembers.objects.filter(user=trip.user)
1✔
724
        if family_members.exists():
1✔
NEW
725
            html_message = render_to_string(
×
726
                "emails/trip_cancel_fam_email.html",
727
                {
728
                    "username": trip.user.username,
729
                    "start": trip.start_address,
730
                    "end": trip.end_address,
731
                },
732
            )
NEW
733
            FamilyMemberEmails(
×
734
                [member.email for member in family_members],
735
                html_message,
736
                f"{trip.user.username}'s Trip Cancelled",
737
            )
738

739
        # Also broadcast to potential matches who might now be compatible
740
        time_min = trip.planned_departure - timedelta(minutes=30)
1✔
741
        time_max = trip.planned_departure + timedelta(minutes=30)
1✔
742

743
        potential_matches = Trip.objects.filter(
1✔
744
            status="SEARCHING",
745
            planned_departure__range=(time_min, time_max),
746
            desired_companions=trip.desired_companions,
747
        ).exclude(user=request.user)
748

749
        for tripMatch in potential_matches:
1✔
750
            broadcast_trip_update(
1✔
751
                tripMatch.id, "SEARCHING", "New potential companion available"
752
            )
753

754
        return redirect("home")
1✔
755
    except Exception as e:
1✔
756
        return JsonResponse({"success": False, "error": str(e)})
1✔
757

758

759
@login_required
1✔
760
@verification_required
1✔
761
def previous_trips(request):
1✔
762
    trip_list = (
1✔
763
        Trip.objects.filter(user=request.user, status__in=["COMPLETED", "CANCELLED"])
764
        .prefetch_related("matches__trip1__user", "matches__trip2__user", "chatroom")
765
        .order_by("-created_at")
766
    )
767

768
    paginator = Paginator(trip_list, 3)
1✔
769
    page = request.GET.get("page")
1✔
770
    trips = paginator.get_page(page)
1✔
771

772
    trips_data = [
1✔
773
        {
774
            "id": trip.id,
775
            "start_latitude": float(trip.start_latitude),
776
            "start_longitude": float(trip.start_longitude),
777
            "dest_latitude": float(trip.dest_latitude),
778
            "dest_longitude": float(trip.dest_longitude),
779
            "companions": [
780
                (
781
                    match.trip2.user.username
782
                    if match.trip1 == trip
783
                    else match.trip1.user.username
784
                )
785
                for match in trip.matches.filter(status="ACCEPTED")
786
            ],
787
        }
788
        for trip in trips
789
    ]
790

791
    return render(
1✔
792
        request,
793
        "locations/previous_trips.html",
794
        {"trips": trips, "trips_json": json.dumps(trips_data)},
795
    )
796

797

798
@login_required
1✔
799
@verification_required
1✔
800
@active_trip_required
1✔
801
@require_http_methods(["GET"])
1✔
802
def check_panic_users(request):
1✔
UNCOV
803
    try:
×
UNCOV
804
        trip = Trip.objects.get(user=request.user, status="IN_PROGRESS")
×
UNCOV
805
        has_panic = trip_filters.has_panic_users(trip)
×
UNCOV
806
        return JsonResponse({"success": True, "has_panic": has_panic})
×
UNCOV
807
    except Exception as e:
×
UNCOV
808
        return JsonResponse({"success": False, "error": str(e)})
×
809

810

811
@login_required
1✔
812
@verification_required
1✔
813
@active_trip_required
1✔
814
@require_http_methods(["POST"])
1✔
815
def trigger_panic(request):
1✔
816
    try:
1✔
817
        user_location = UserLocation.objects.get(user=request.user)
1✔
818
        panic_locations = UserLocation.objects.filter(panic=True).select_related("user")
1✔
819

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

822
        user_location.panic = True
1✔
823
        user_location.panic_message = request.POST.get("initial_message")
1✔
824
        user_location.save()
1✔
825

826
        family_members = FamilyMembers.objects.filter(user=request.user)
1✔
827
        if family_members.exists():
1✔
NEW
828
            current_address = reverse_geocode(
×
829
                float(user_location.latitude), float(user_location.longitude)
830
            )
831

NEW
832
            html_message = render_to_string(
×
833
                "emails/panic_trigger_fam_email.html",
834
                {
835
                    "username": request.user.username,
836
                    "message": user_location.panic_message,
837
                    "location": current_address or "Location Unavailable",
838
                    "latitude": user_location.latitude,
839
                    "longitude": user_location.longitude,
840
                },
841
            )
NEW
842
            FamilyMemberEmails(
×
843
                [member.email for member in family_members],
844
                html_message,
845
                f"PANIC ALERT: {request.user.username} Requested Emergency Support",
846
            )
847

848
        # Get all active panic locations for emergency support view
849
        locations_data = [
1✔
850
            {
851
                "id": loc.id,
852
                "username": loc.user.username,
853
                "panic_message": loc.panic_message,
854
                "latitude": float(loc.latitude),
855
                "longitude": float(loc.longitude),
856
            }
857
            for loc in panic_locations
858
        ]
859

860
        pusher_client.trigger(
1✔
861
            "emergency-channel",
862
            "panic-create",
863
            {
864
                "locations": locations_data,
865
                "active_users": UserLocation.objects.count(),
866
                "panic_users": panic_locations.count(),
867
            },
868
        )
869

870
        # Send ems message to their trip's chatroom
871
        if active_trip.chatroom:
1✔
872
            send_ems_message(
1✔
873
                active_trip.chatroom,
874
                f"{request.user.username} has triggered panic mode.",
875
                user_location.panic_message,
876
                request.user,
877
            )
878

879
        return JsonResponse({"success": True, "message": "Panic mode activated."})
1✔
880
    except Exception as e:
1✔
881
        return JsonResponse({"success": False, "error": str(e)})
1✔
882

883

884
@login_required
1✔
885
@verification_required
1✔
886
@emergency_support_required
1✔
887
@require_http_methods(["POST"])
1✔
888
def resolve_panic(request, panic_username):
1✔
889
    try:
1✔
890
        user = User.objects.get(username=panic_username)
1✔
891
        user_location = UserLocation.objects.get(user=user)
1✔
892
        active_trip = Trip.objects.get(user=user, status="IN_PROGRESS")
1✔
893

894
        user_location.panic = False
1✔
895
        user_location.panic_message = None
1✔
896
        user_location.save()
1✔
897

898
        family_members = FamilyMembers.objects.filter(user=user)
1✔
899
        if family_members.exists():
1✔
NEW
900
            html_message = render_to_string(
×
901
                "emails/panic_resolve_fam_email.html", {"username": user.username}
902
            )
NEW
903
            FamilyMemberEmails(
×
904
                [member.email for member in family_members],
905
                html_message,
906
                f"PANIC ALERT: {user.username}'s Emergency Support Request Resolved",
907
            )
908

909
        # Trigger a Pusher event to update the panic button
910
        pusher_client.trigger(
1✔
911
            "emergency-channel", "panic-resolve", {"username": panic_username}
912
        )
913

914
        # Send ems message to their trip's chatroom
915
        if active_trip.chatroom:
1✔
916
            send_system_message(
1✔
917
                active_trip.chatroom,
918
                f"Emergency support has resolved {user.username}'s panic request.",
919
            )
920

921
        return JsonResponse({"success": True, "message": "Panic mode deactivated."})
1✔
922
    except Exception as e:
1✔
923
        return JsonResponse({"success": False, "error": str(e)})
1✔
924

925

926
@login_required
1✔
927
@verification_required
1✔
928
@emergency_support_required
1✔
929
def emergency_support(request):
1✔
930
    # Fetch only the users who have panic mode activated
931
    user_locations = UserLocation.objects.filter(panic=True).select_related("user")
1✔
932

933
    active_user_count = UserLocation.objects.count()
1✔
934
    panic_user_count = user_locations.count()
1✔
935

936
    user_locations_data = [
1✔
937
        {
938
            "id": location.id,
939
            "latitude": float(location.latitude),
940
            "longitude": float(location.longitude),
941
            "username": location.user.username,
942
            "panic": location.panic,
943
            "panic_message": location.panic_message,
944
        }
945
        for location in user_locations
946
    ]
947

948
    return render(
1✔
949
        request,
950
        "locations/emergency_support.html",
951
        {
952
            "active_users": active_user_count,
953
            "panic_users": panic_user_count,
954
            "locations": user_locations,
955
            "locations_json": json.dumps(user_locations_data),
956
            "pusher_key": settings.PUSHER_KEY,
957
            "pusher_cluster": settings.PUSHER_CLUSTER,
958
        },
959
    )
960

961

962
@login_required
1✔
963
@verification_required
1✔
964
@active_trip_required
1✔
965
@require_http_methods(["POST"])
1✔
966
def complete_trip(request):
1✔
967
    trip = Trip.objects.get(user=request.user, status="IN_PROGRESS")
1✔
968

969
    if not trip.completion_requested:
1✔
970
        trip.completion_requested = True
1✔
971
        trip.save()
1✔
972

973
        if trip.chatroom:
1✔
974
            send_system_message(
1✔
975
                trip.chatroom,
976
                f"{request.user.username} has voted to complete the trip.",
977
            )
978

979
        # Get all connected trips in one query
980
        matched_trips = Trip.objects.filter(
1✔
981
            (
982
                Q(matches__trip2=trip, matches__status="ACCEPTED")
983
                | Q(matched_with__trip2=trip, matched_with__status="ACCEPTED")
984
                | Q(matches__trip1=trip, matches__status="ACCEPTED")
985
                | Q(matched_with__trip1=trip, matched_with__status="ACCEPTED")
986
            )
987
        ).distinct()
988

989
        # Count completion votes and total members
990
        completion_votes = matched_trips.filter(completion_requested=True).count()
1✔
991
        total_members = matched_trips.count()
1✔
992

993
        # If majority votes for completion
994
        if completion_votes >= total_members / 2:
1✔
995
            current_time = timezone.now()
1✔
996
            if trip.chatroom:
1✔
997
                send_system_message(
1✔
998
                    trip.chatroom,
999
                    "Majority has voted to complete the trip. Trip is now archived.",
1000
                )
1001

1002
            # Complete all trips in one query
1003
            matched_trips.update(
1✔
1004
                status="COMPLETED",
1005
                completion_requested=True,
1006
                completed_at=current_time,
1007
            )
1008

1009
            for matched_trip in matched_trips:
1✔
1010
                family_members = FamilyMembers.objects.filter(user=matched_trip.user)
1✔
1011
                if family_members.exists():
1✔
NEW
1012
                    html_message = render_to_string(
×
1013
                        "emails/trip_complete_fam_email.html",
1014
                        {"username": matched_trip.user.username, "toa": current_time},
1015
                    )
NEW
1016
                    FamilyMemberEmails(
×
1017
                        [member.email for member in family_members],
1018
                        html_message,
1019
                        f"{matched_trip.user.username}'s Trip Completed",
1020
                    )
1021

1022
            # Delete UserLocations for all users involved
1023
            user_ids = matched_trips.values_list("user", flat=True)
1✔
1024
            UserLocation.objects.filter(user__in=user_ids).delete()
1✔
1025

1026
            # Broadcast completion to all participants
1027
            for matched_trip in matched_trips:
1✔
1028
                broadcast_trip_update(
1✔
1029
                    matched_trip.id, "COMPLETED", "Trip has been completed"
1030
                )
1031
        return redirect("previous_trips")
1✔
1032
    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